From 72ca1d4c384c3b0b0651db3442cd756ed142389a Mon Sep 17 00:00:00 2001 From: Luke Vella Date: Mon, 14 Apr 2025 15:11:59 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Updated=20sidebar=20layout=20(#1661?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .vscode/settings.json | 3 +- .windsurfrules | 39 +++ apps/web/public/locales/en/app.json | 27 +- .../sidebar/app-sidebar-provider.tsx | 22 ++ .../components/sidebar/app-sidebar.tsx | 134 +++++++++ .../(admin)/components/sidebar/nav-item.tsx | 24 ++ .../(admin)/components/sidebar/nav-user.tsx | 33 +++ .../[locale]/(admin)/components/top-bar.tsx | 48 ++++ .../(admin)/components/upgrade-button.tsx | 13 + .../[locale]/(admin)/events/event-list.tsx | 76 +++--- .../app/[locale]/(admin)/events/layout.tsx | 3 - .../app/[locale]/(admin)/events/loading.tsx | 5 - .../src/app/[locale]/(admin)/events/page.tsx | 30 ++- .../(admin)/events/user-scheduled-events.tsx | 40 +-- apps/web/src/app/[locale]/(admin)/layout.tsx | 73 ++--- apps/web/src/app/[locale]/(admin)/loading.tsx | 10 +- .../app/[locale]/(admin)/menu/menu-button.tsx | 42 --- .../src/app/[locale]/(admin)/menu/page.tsx | 27 -- apps/web/src/app/[locale]/(admin)/page.tsx | 134 ++++++--- .../src/app/[locale]/(admin)/polls/actions.ts | 49 ++++ .../app/[locale]/(admin)/polls/loading.tsx | 5 - .../src/app/[locale]/(admin)/polls/page.tsx | 245 ++++++++++++++--- .../(admin)/polls/polls-tabbed-view.tsx | 45 ++++ .../[locale]/(admin)/polls/search-input.tsx | 78 ++++++ .../app/[locale]/(admin)/polls/user-polls.tsx | 255 ------------------ .../src/app/[locale]/(admin)/pro-badge.tsx | 20 -- .../(admin)/recently-updated-polls-grid.tsx | 82 ++++++ .../(admin)/settings/billing/loading.tsx | 5 - .../(admin)/settings/billing/page.tsx | 17 +- .../components}/date-time-preferences.tsx | 0 .../components}/language-preference.tsx | 0 .../settings/components/settings-layout.tsx} | 35 +-- .../settings/components/sign-out-button.tsx | 25 ++ .../app/[locale]/(admin)/settings/layout.tsx | 25 +- .../app/[locale]/(admin)/settings/loading.tsx | 8 +- .../settings/preferences/preferences-page.tsx | 66 ++--- .../(admin)/settings/profile/loading.tsx | 5 - .../(admin)/settings/profile/profile-page.tsx | 130 ++++----- .../(admin)/settings/settings-menu.tsx | 58 ++-- apps/web/src/app/[locale]/(admin)/sidebar.tsx | 199 -------------- apps/web/src/app/[locale]/layout.tsx | 13 +- .../new/{close-button.tsx => back-button.tsx} | 9 +- apps/web/src/app/[locale]/new/page.tsx | 13 +- .../src/app/api/auth/invalid-session/route.ts | 7 + apps/web/src/app/components/page-icons.tsx | 152 +++++++++++ apps/web/src/app/components/page-layout.tsx | 75 ++++-- apps/web/src/app/components/tab-menu.tsx | 34 --- apps/web/src/components/clock.tsx | 1 + apps/web/src/components/copy-link-button.tsx | 52 ++++ apps/web/src/components/formatted-date.tsx | 7 +- .../src/components/forms/poll-settings.tsx | 4 +- .../src/components/optimized-avatar-image.tsx | 14 +- apps/web/src/components/pagination.tsx | 105 ++++++++ .../src/components/participant-avatar-bar.tsx | 65 +++-- apps/web/src/components/pay-wall-dialog.tsx | 8 +- apps/web/src/components/poll-status-icon.tsx | 62 +++++ apps/web/src/components/poll-status.tsx | 19 +- apps/web/src/components/poll/manage-poll.tsx | 6 +- apps/web/src/components/poll/mutations.ts | 20 +- .../poll/scheduled-event-display.tsx | 57 ++++ apps/web/src/components/pro-badge.tsx | 2 +- apps/web/src/components/pro-feature-badge.tsx | 12 - apps/web/src/components/relative-date.tsx | 6 + .../components/router-loading-indicator.tsx | 72 +++++ apps/web/src/components/stacked-list.tsx | 35 +++ apps/web/src/components/table.tsx | 183 ------------- .../time-zone-picker/time-zone-select.tsx | 5 +- apps/web/src/components/trans.tsx | 10 +- apps/web/src/components/upgrade-button.tsx | 12 +- apps/web/src/contexts/plan.tsx | 7 +- apps/web/src/data/get-poll-count-by-status.ts | 19 ++ apps/web/src/data/get-polls.ts | 101 +++++++ .../src/data/get-recently-updated-polls.ts | 99 +++++++ apps/web/src/data/get-user.ts | 42 +++ .../command-menu/command-global-shortcut.tsx | 35 +++ .../navigation/command-menu/command-menu.tsx | 115 ++++++++ .../features/navigation/command-menu/index.ts | 1 + .../src/features/poll-selection/context.tsx | 130 +++++++++ .../poll-selection-action-bar.tsx | 142 ++++++++++ apps/web/src/features/timezone/index.ts | 3 + .../features/timezone/timezone-context.tsx | 104 +++++++ .../features/timezone/timezone-display.tsx | 31 +++ .../src/features/timezone/timezone-utils.ts | 110 ++++++++ apps/web/src/middleware.ts | 7 +- apps/web/src/next-auth.ts | 2 +- apps/web/src/trpc/routers/polls.ts | 19 +- .../web/tests/guest-to-user-migration.spec.ts | 2 +- packages/database/prisma/schema.prisma | 2 +- packages/tailwind-config/tailwind.config.js | 7 +- packages/ui/package.json | 1 + packages/ui/src/action-bar.tsx | 84 ++++++ packages/ui/src/badge.tsx | 2 +- packages/ui/src/button.tsx | 8 +- packages/ui/src/command.tsx | 45 +++- packages/ui/src/dialog.tsx | 101 ++++--- packages/ui/src/dropdown-menu.tsx | 4 +- packages/ui/src/hooks/use-platform.ts | 5 + packages/ui/src/icon.tsx | 8 +- packages/ui/src/page-tabs.tsx | 53 ++++ packages/ui/src/progress.tsx | 28 ++ packages/ui/src/sidebar.tsx | 4 +- packages/ui/src/table.tsx | 2 +- packages/ui/src/tile.tsx | 73 +++++ yarn.lock | 8 + 104 files changed, 3268 insertions(+), 1331 deletions(-) create mode 100644 .windsurfrules create mode 100644 apps/web/src/app/[locale]/(admin)/components/sidebar/app-sidebar-provider.tsx create mode 100644 apps/web/src/app/[locale]/(admin)/components/sidebar/app-sidebar.tsx create mode 100644 apps/web/src/app/[locale]/(admin)/components/sidebar/nav-item.tsx create mode 100644 apps/web/src/app/[locale]/(admin)/components/sidebar/nav-user.tsx create mode 100644 apps/web/src/app/[locale]/(admin)/components/top-bar.tsx create mode 100644 apps/web/src/app/[locale]/(admin)/components/upgrade-button.tsx delete mode 100644 apps/web/src/app/[locale]/(admin)/events/layout.tsx delete mode 100644 apps/web/src/app/[locale]/(admin)/events/loading.tsx delete mode 100644 apps/web/src/app/[locale]/(admin)/menu/menu-button.tsx delete mode 100644 apps/web/src/app/[locale]/(admin)/menu/page.tsx create mode 100644 apps/web/src/app/[locale]/(admin)/polls/actions.ts delete mode 100644 apps/web/src/app/[locale]/(admin)/polls/loading.tsx create mode 100644 apps/web/src/app/[locale]/(admin)/polls/polls-tabbed-view.tsx create mode 100644 apps/web/src/app/[locale]/(admin)/polls/search-input.tsx delete mode 100644 apps/web/src/app/[locale]/(admin)/polls/user-polls.tsx delete mode 100644 apps/web/src/app/[locale]/(admin)/pro-badge.tsx create mode 100644 apps/web/src/app/[locale]/(admin)/recently-updated-polls-grid.tsx delete mode 100644 apps/web/src/app/[locale]/(admin)/settings/billing/loading.tsx rename apps/web/src/{components/settings => app/[locale]/(admin)/settings/components}/date-time-preferences.tsx (100%) rename apps/web/src/{components/settings => app/[locale]/(admin)/settings/components}/language-preference.tsx (100%) rename apps/web/src/{components/settings/settings.tsx => app/[locale]/(admin)/settings/components/settings-layout.tsx} (70%) create mode 100644 apps/web/src/app/[locale]/(admin)/settings/components/sign-out-button.tsx delete mode 100644 apps/web/src/app/[locale]/(admin)/settings/profile/loading.tsx delete mode 100644 apps/web/src/app/[locale]/(admin)/sidebar.tsx rename apps/web/src/app/[locale]/new/{close-button.tsx => back-button.tsx} (62%) create mode 100644 apps/web/src/app/api/auth/invalid-session/route.ts create mode 100644 apps/web/src/app/components/page-icons.tsx delete mode 100644 apps/web/src/app/components/tab-menu.tsx create mode 100644 apps/web/src/components/copy-link-button.tsx create mode 100644 apps/web/src/components/pagination.tsx create mode 100644 apps/web/src/components/poll-status-icon.tsx create mode 100644 apps/web/src/components/poll/scheduled-event-display.tsx delete mode 100644 apps/web/src/components/pro-feature-badge.tsx create mode 100644 apps/web/src/components/relative-date.tsx create mode 100644 apps/web/src/components/router-loading-indicator.tsx create mode 100644 apps/web/src/components/stacked-list.tsx delete mode 100644 apps/web/src/components/table.tsx create mode 100644 apps/web/src/data/get-poll-count-by-status.ts create mode 100644 apps/web/src/data/get-polls.ts create mode 100644 apps/web/src/data/get-recently-updated-polls.ts create mode 100644 apps/web/src/data/get-user.ts create mode 100644 apps/web/src/features/navigation/command-menu/command-global-shortcut.tsx create mode 100644 apps/web/src/features/navigation/command-menu/command-menu.tsx create mode 100644 apps/web/src/features/navigation/command-menu/index.ts create mode 100644 apps/web/src/features/poll-selection/context.tsx create mode 100644 apps/web/src/features/poll-selection/poll-selection-action-bar.tsx create mode 100644 apps/web/src/features/timezone/index.ts create mode 100644 apps/web/src/features/timezone/timezone-context.tsx create mode 100644 apps/web/src/features/timezone/timezone-display.tsx create mode 100644 apps/web/src/features/timezone/timezone-utils.ts create mode 100644 packages/ui/src/action-bar.tsx create mode 100644 packages/ui/src/hooks/use-platform.ts create mode 100644 packages/ui/src/page-tabs.tsx create mode 100644 packages/ui/src/progress.tsx create mode 100644 packages/ui/src/tile.tsx diff --git a/.vscode/settings.json b/.vscode/settings.json index c1630592e..d2276158e 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -10,5 +10,6 @@ "editor.formatOnSave": true, "typescript.preferences.preferTypeOnlyAutoImports": true, "typescript.tsserver.log": "verbose", - "typescript.tsserver.trace": "messages" + "typescript.tsserver.trace": "messages", + "references.preferredLocation": "view" } diff --git a/.windsurfrules b/.windsurfrules new file mode 100644 index 000000000..01d784b5a --- /dev/null +++ b/.windsurfrules @@ -0,0 +1,39 @@ +1. Use yarn for package management +2. Use dayjs for date handling +3. Use tailwindcss for styling +4. Use react-query for data fetching +5. Use react-hook-form for form handling +6. Prefer implicit return values over explicit return values +7. Use zod for form validation +8. Create separate import statements for types +9. All text in the UI should be translated using either the Trans component or the useTranslation hook +10. Prefer composable components in the style of shadcn UI over large monolithic components +11. DropdownMenuItem is a flex container with a preset gap so there is no need to add margins to the children +12. The size and colour of an icon should be set by wrapping it with the component from @rallly/ui/icon which will give it the correct colour and size. +13. Keep the props of a component as minimal as possible. Pass only the bare minimum amount of information needed to it. +14. All text in the UI should be translatable. +15. i18n keys are in camelCase. +16. Use the component in client components from @/components/trans. Use the `defaults` prop to provide the default text. Example: + +```tsx + +``` + +17. On the server use the `getTranslations` function from @/i18n/server to get the translations. Example: + +```ts +const { t } = await getTranslations(); + +t("menu", { defaultValue: "Menu" }); +``` + +18. shadcn-ui components should be added to packages/ui +19. Always use a composable patterns when building components +20. Use `cn()` from @rallly/ui to compose classes +21. Prefer using the React module APIs (e.g. React.useState) instead of standalone hooks (e.g. useState) +22. Do not attempt to fix typescript issues related to missing translations. This will be handled by our tooling. +23. Never manually add translations to .json files. This will be handled by our tooling. +24. Add the "use client" directive to the top of any .tsx file that requires client-side javascript +25. i18nKeys should describe the message in camelCase. Ex. "lastUpdated": "Last Updated" +26. Keep i18nKeys up to 25 characters +27. If the i18nKey is not intended to be reused, prefix it with the component name in camelCase diff --git a/apps/web/public/locales/en/app.json b/apps/web/public/locales/en/app.json index 7e3c12b87..c6845a44b 100644 --- a/apps/web/public/locales/en/app.json +++ b/apps/web/public/locales/en/app.json @@ -168,11 +168,9 @@ "hideScoresLabel": "Hide scores until after a participant has voted", "continueAs": "Continue as", "pageMovedDescription": "Redirecting to {newUrl}", - "unlockFeatures": "Unlock all Pro features.", "pollStatusFinalized": "Finalized", "share": "Share", "noParticipants": "No participants", - "logoutDescription": "Sign out of your existing session", "events": "Events", "inviteParticipantsDescription": "Copy and share the invite link to start gathering responses from your participants.", "inviteLink": "Invite Link", @@ -303,5 +301,28 @@ "needToMakeChanges": "Need to make changes?", "billingPortalDescription": "Visit the billing portal to manage your subscription, update payment methods, or view billing history.", "priceFree": "Free", - "signUp": "Sign Up" + "signUp": "Sign Up", + "upgradeToPro": "Upgrade to Pro", + "moreParticipants": "{count} more…", + "noDates": "No dates", + "commandMenuNoResults": "No results", + "selectedPolls": "{count} {count, plural, one {poll} other {polls}} selected", + "unselectAll": "Unselect All", + "deletePolls": "Delete Polls", + "deletePollsDescription": "Are you sure you want to delete these {count} polls? This action cannot be undone.", + "commandMenu": "Command Menu", + "commandMenuDescription": "Select a command", + "eventsPageDesc": "View and manage your scheduled events", + "homeDashboardDesc": "Manage your polls, events, and account settings", + "homeNavTitle": "Navigation", + "account": "Account", + "pollsPageDesc": "View and manage all your scheduling polls", + "signOut": "Sign Out", + "paginationItems": "Showing {startItem}–{endItem} of {totalItems}", + "paginationPrevious": "Previous", + "paginationPage": "Page {currentPage} of {totalPages}", + "paginationNext": "Next", + "upgradeToProDesc": "Unlock all Pro features", + "searchPollsPlaceholder": "Search polls by title...", + "poll": "Poll" } diff --git a/apps/web/src/app/[locale]/(admin)/components/sidebar/app-sidebar-provider.tsx b/apps/web/src/app/[locale]/(admin)/components/sidebar/app-sidebar-provider.tsx new file mode 100644 index 000000000..f7c9e0f6f --- /dev/null +++ b/apps/web/src/app/[locale]/(admin)/components/sidebar/app-sidebar-provider.tsx @@ -0,0 +1,22 @@ +"use client"; + +import { SidebarProvider } from "@rallly/ui/sidebar"; +import { useLocalStorage } from "react-use"; + +export function AppSidebarProvider({ + children, +}: { + children: React.ReactNode; +}) { + const [value, setValue] = useLocalStorage("sidebar_state", "expanded"); + return ( + { + setValue(open ? "expanded" : "collapsed"); + }} + > + {children} + + ); +} diff --git a/apps/web/src/app/[locale]/(admin)/components/sidebar/app-sidebar.tsx b/apps/web/src/app/[locale]/(admin)/components/sidebar/app-sidebar.tsx new file mode 100644 index 000000000..d790cec95 --- /dev/null +++ b/apps/web/src/app/[locale]/(admin)/components/sidebar/app-sidebar.tsx @@ -0,0 +1,134 @@ +import { Button } from "@rallly/ui/button"; +import { Icon } from "@rallly/ui/icon"; +import { + Sidebar, + SidebarContent, + SidebarFooter, + SidebarGroup, + SidebarHeader, + SidebarMenu, + SidebarSeparator, +} from "@rallly/ui/sidebar"; +import { Tooltip, TooltipContent, TooltipTrigger } from "@rallly/ui/tooltip"; +import { + BarChart2Icon, + CalendarIcon, + HomeIcon, + PlusIcon, + SparklesIcon, +} from "lucide-react"; +import Link from "next/link"; +import * as React from "react"; + +import { LogoLink } from "@/app/components/logo-link"; +import { Trans } from "@/components/trans"; +import { getUser } from "@/data/get-user"; +import { getTranslation } from "@/i18n/server"; + +import { UpgradeButton } from "../upgrade-button"; +import { NavItem } from "./nav-item"; +import { NavUser } from "./nav-user"; + +export async function AppSidebar({ + ...props +}: React.ComponentProps) { + const user = await getUser(); + const { t } = await getTranslation(); + return ( + + +
+
+ +
+
+ + + + + Create + +
+
+
+ + + + {/* + + P + + Personal + */} + + + {t("home")} + + + + {t("polls")} + + + + {t("events")} + + {/* + + {t("teams", { defaultValue: "Teams" })} + + + + {t("settings", { defaultValue: "Settings" })} + */} + {/* */} + {/* */} + {/* */} + + + + + {!user.isPro ? ( + <> +
+ +
+

+ +

+

+ +

+ + + +
+
+ + + ) : null} + + ) : ( + + ) + } + /> +
+
+ ); +} diff --git a/apps/web/src/app/[locale]/(admin)/components/sidebar/nav-item.tsx b/apps/web/src/app/[locale]/(admin)/components/sidebar/nav-item.tsx new file mode 100644 index 000000000..5212f11bd --- /dev/null +++ b/apps/web/src/app/[locale]/(admin)/components/sidebar/nav-item.tsx @@ -0,0 +1,24 @@ +"use client"; + +import { SidebarMenuButton, SidebarMenuItem } from "@rallly/ui/sidebar"; +import Link from "next/link"; +import { usePathname } from "next/navigation"; + +export function NavItem({ + href, + children, +}: { + href: string; + children: React.ReactNode; +}) { + const pathname = usePathname(); + const isActive = pathname === href; + + return ( + + + {children} + + + ); +} diff --git a/apps/web/src/app/[locale]/(admin)/components/sidebar/nav-user.tsx b/apps/web/src/app/[locale]/(admin)/components/sidebar/nav-user.tsx new file mode 100644 index 000000000..522503c01 --- /dev/null +++ b/apps/web/src/app/[locale]/(admin)/components/sidebar/nav-user.tsx @@ -0,0 +1,33 @@ +"use client"; + +import Link from "next/link"; +import { usePathname } from "next/navigation"; + +import { OptimizedAvatarImage } from "@/components/optimized-avatar-image"; + +export function NavUser({ + name, + image, + plan, +}: { + name: string; + image?: string; + plan?: React.ReactNode; +}) { + const pathname = usePathname(); + return ( + + +
+
{name}
+
+ {plan} +
+
+ + ); +} diff --git a/apps/web/src/app/[locale]/(admin)/components/top-bar.tsx b/apps/web/src/app/[locale]/(admin)/components/top-bar.tsx new file mode 100644 index 000000000..d88c13dcf --- /dev/null +++ b/apps/web/src/app/[locale]/(admin)/components/top-bar.tsx @@ -0,0 +1,48 @@ +import { cn } from "@rallly/ui"; + +export function TopBar({ + children, + className, +}: { + children: React.ReactNode; + className?: string; +}) { + return ( +
+ {children} +
+ ); +} + +export function TopBarLeft({ children }: { children: React.ReactNode }) { + return
{children}
; +} + +export function TopBarRight({ children }: { children: React.ReactNode }) { + return ( +
+ {children} +
+ ); +} + +export function TopBarGroup({ + children, + className, +}: { + children: React.ReactNode; + className?: string; +}) { + return ( +
{children}
+ ); +} + +export function TopBarSeparator() { + return
; +} diff --git a/apps/web/src/app/[locale]/(admin)/components/upgrade-button.tsx b/apps/web/src/app/[locale]/(admin)/components/upgrade-button.tsx new file mode 100644 index 000000000..a01de3aac --- /dev/null +++ b/apps/web/src/app/[locale]/(admin)/components/upgrade-button.tsx @@ -0,0 +1,13 @@ +"use client"; + +import { DialogTrigger } from "@rallly/ui/dialog"; + +import { PayWallDialog } from "@/components/pay-wall-dialog"; + +export function UpgradeButton({ children }: React.PropsWithChildren) { + return ( + + {children} + + ); +} diff --git a/apps/web/src/app/[locale]/(admin)/events/event-list.tsx b/apps/web/src/app/[locale]/(admin)/events/event-list.tsx index f6f8a6fc5..c56a23ec2 100644 --- a/apps/web/src/app/[locale]/(admin)/events/event-list.tsx +++ b/apps/web/src/app/[locale]/(admin)/events/event-list.tsx @@ -1,6 +1,5 @@ "use client"; -import { Card, CardContent } from "@rallly/ui/card"; import { getCoreRowModel, useReactTable } from "@tanstack/react-table"; import dayjs from "dayjs"; @@ -18,7 +17,7 @@ export function EventList({ data }: { data: ScheduledEvent[] }) { const { adjustTimeZone } = useDayjs(); return ( - +
    {table.getRowModel().rows.map((row) => { const start = adjustTimeZone( @@ -31,49 +30,44 @@ export function EventList({ data }: { data: ScheduledEvent[] }) { !row.original.timeZone, ); return ( -
  • - -
    -
    - - -
    -
    -
    - -

    - {row.original.title} -

    -
    -

    - {row.original.duration === 0 ? ( - - ) : ( - {`${start.format("LT")} - ${end.format("LT")}`} - )} -

    -
    +
  • +
    +
    + +
    - +
    +
    + +

    + {row.original.title} +

    +
    +

    + {row.original.duration === 0 ? ( + + ) : ( + {`${start.format("LT")} - ${end.format("LT")}`} + )} +

    +
    +
  • ); })}
- +
); } diff --git a/apps/web/src/app/[locale]/(admin)/events/layout.tsx b/apps/web/src/app/[locale]/(admin)/events/layout.tsx deleted file mode 100644 index 626ea6e8e..000000000 --- a/apps/web/src/app/[locale]/(admin)/events/layout.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export default function Layout({ children }: { children?: React.ReactNode }) { - return
{children}
; -} diff --git a/apps/web/src/app/[locale]/(admin)/events/loading.tsx b/apps/web/src/app/[locale]/(admin)/events/loading.tsx deleted file mode 100644 index f6c4a2fbc..000000000 --- a/apps/web/src/app/[locale]/(admin)/events/loading.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { Spinner } from "@/components/spinner"; - -export default async function Loading() { - return ; -} diff --git a/apps/web/src/app/[locale]/(admin)/events/page.tsx b/apps/web/src/app/[locale]/(admin)/events/page.tsx index 79422818e..b6f58521d 100644 --- a/apps/web/src/app/[locale]/(admin)/events/page.tsx +++ b/apps/web/src/app/[locale]/(admin)/events/page.tsx @@ -1,25 +1,31 @@ import { UserScheduledEvents } from "@/app/[locale]/(admin)/events/user-scheduled-events"; import type { Params } from "@/app/[locale]/types"; +import { EventPageIcon } from "@/app/components/page-icons"; import { PageContainer, PageContent, + PageDescription, PageHeader, PageTitle, } from "@/app/components/page-layout"; +import { Trans } from "@/components/trans"; import { getTranslation } from "@/i18n/server"; export default async function Page({ params }: { params: Params }) { - const { t } = await getTranslation(params.locale); + await getTranslation(params.locale); return ( -
- - {t("events", { - defaultValue: "Events", - })} - -
+ + + + + + +
@@ -28,12 +34,8 @@ export default async function Page({ params }: { params: Params }) { ); } -export async function generateMetadata({ - params, -}: { - params: { locale: string }; -}) { - const { t } = await getTranslation(params.locale); +export async function generateMetadata() { + const { t } = await getTranslation(); return { title: t("events", { defaultValue: "Events", diff --git a/apps/web/src/app/[locale]/(admin)/events/user-scheduled-events.tsx b/apps/web/src/app/[locale]/(admin)/events/user-scheduled-events.tsx index 87ed31642..3fd0fad71 100644 --- a/apps/web/src/app/[locale]/(admin)/events/user-scheduled-events.tsx +++ b/apps/web/src/app/[locale]/(admin)/events/user-scheduled-events.tsx @@ -1,39 +1,41 @@ "use client"; -import { RadioCards, RadioCardsItem } from "@rallly/ui/radio-pills"; -import { useSearchParams } from "next/navigation"; +import { Tabs, TabsList, TabsTrigger } from "@rallly/ui/page-tabs"; +import { useRouter, useSearchParams } from "next/navigation"; import { z } from "zod"; import { PastEvents } from "@/app/[locale]/(admin)/events/past-events"; +import { UpcomingEvents } from "@/app/[locale]/(admin)/events/upcoming-events"; import { Trans } from "@/components/trans"; -import { UpcomingEvents } from "./upcoming-events"; - const eventPeriodSchema = z.enum(["upcoming", "past"]).catch("upcoming"); export function UserScheduledEvents() { const searchParams = useSearchParams(); const period = eventPeriodSchema.parse(searchParams?.get("period")); + const router = useRouter(); return (
-
- { - const newParams = new URLSearchParams(searchParams?.toString()); - newParams.set("period", value); - window.history.pushState(null, "", `?${newParams.toString()}`); - }} - > - + { + const params = new URLSearchParams(searchParams); + params.set("period", value); + const newUrl = `?${params.toString()}`; + router.replace(newUrl); + }} + aria-label="Event period" + > + + - - + + - - -
+ + +
{period === "upcoming" && } {period === "past" && } diff --git a/apps/web/src/app/[locale]/(admin)/layout.tsx b/apps/web/src/app/[locale]/(admin)/layout.tsx index 9c9c696c7..a7ead2083 100644 --- a/apps/web/src/app/[locale]/(admin)/layout.tsx +++ b/apps/web/src/app/[locale]/(admin)/layout.tsx @@ -1,42 +1,53 @@ -import { cn } from "@rallly/ui"; -import { dehydrate, Hydrate } from "@tanstack/react-query"; -import React from "react"; +import { ActionBar } from "@rallly/ui/action-bar"; +import { Button } from "@rallly/ui/button"; +import { SidebarInset, SidebarTrigger } from "@rallly/ui/sidebar"; +import Link from "next/link"; -import { MobileNavigation } from "@/app/[locale]/(admin)/mobile-navigation"; -import { ProBadge } from "@/app/[locale]/(admin)/pro-badge"; -import { Sidebar } from "@/app/[locale]/(admin)/sidebar"; -import { LogoLink } from "@/app/components/logo-link"; -import { createSSRHelper } from "@/trpc/server/create-ssr-helper"; +import { AppSidebar } from "@/app/[locale]/(admin)/components/sidebar/app-sidebar"; +import { AppSidebarProvider } from "@/app/[locale]/(admin)/components/sidebar/app-sidebar-provider"; +import { OptimizedAvatarImage } from "@/components/optimized-avatar-image"; +import { getUser } from "@/data/get-user"; +import { CommandMenu } from "@/features/navigation/command-menu"; + +import { TopBar, TopBarLeft, TopBarRight } from "./components/top-bar"; export default async function Layout({ children, }: { children: React.ReactNode; }) { - const helpers = await createSSRHelper(); - await helpers.user.subscription.prefetch(); - const dehydratedState = dehydrate(helpers.queryClient); + const user = await getUser(); return ( - -
- - + + + ); } diff --git a/apps/web/src/app/[locale]/(admin)/loading.tsx b/apps/web/src/app/[locale]/(admin)/loading.tsx index f6c4a2fbc..fdf5e4f64 100644 --- a/apps/web/src/app/[locale]/(admin)/loading.tsx +++ b/apps/web/src/app/[locale]/(admin)/loading.tsx @@ -1,5 +1,11 @@ -import { Spinner } from "@/components/spinner"; +import { PageSkeleton } from "@/app/components/page-layout"; +import { RouterLoadingIndicator } from "@/components/router-loading-indicator"; export default async function Loading() { - return ; + return ( + <> + + + + ); } diff --git a/apps/web/src/app/[locale]/(admin)/menu/menu-button.tsx b/apps/web/src/app/[locale]/(admin)/menu/menu-button.tsx deleted file mode 100644 index 1e5fb5b5d..000000000 --- a/apps/web/src/app/[locale]/(admin)/menu/menu-button.tsx +++ /dev/null @@ -1,42 +0,0 @@ -"use client"; - -import { Button } from "@rallly/ui/button"; -import { Icon } from "@rallly/ui/icon"; -import { ArrowLeftIcon, MenuIcon } from "lucide-react"; -import Link from "next/link"; -import { useRouter } from "next/navigation"; - -import { Trans } from "@/components/trans"; - -export function BackButton() { - const router = useRouter(); - return ( - - ); -} - -export function MobileMenuButton({ open }: { open?: boolean }) { - if (open) { - return ; - } - - return ( - - ); -} diff --git a/apps/web/src/app/[locale]/(admin)/menu/page.tsx b/apps/web/src/app/[locale]/(admin)/menu/page.tsx deleted file mode 100644 index 7cc192be2..000000000 --- a/apps/web/src/app/[locale]/(admin)/menu/page.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { Trans } from "react-i18next/TransWithoutContext"; - -import { Sidebar } from "@/app/[locale]/(admin)/sidebar"; -import type { Params } from "@/app/[locale]/types"; -import { - PageContainer, - PageContent, - PageHeader, - PageTitle, -} from "@/app/components/page-layout"; -import { getTranslation } from "@/i18n/server"; - -export default async function Page({ params }: { params: Params }) { - const { t } = await getTranslation(params.locale); - return ( - - - - - - - - - - - ); -} diff --git a/apps/web/src/app/[locale]/(admin)/page.tsx b/apps/web/src/app/[locale]/(admin)/page.tsx index 7ca0aac04..4a2c7bb8a 100644 --- a/apps/web/src/app/[locale]/(admin)/page.tsx +++ b/apps/web/src/app/[locale]/(admin)/page.tsx @@ -1,52 +1,118 @@ -import { dehydrate, Hydrate } from "@tanstack/react-query"; -import { HomeIcon } from "lucide-react"; -import { Trans } from "react-i18next/TransWithoutContext"; +import { Tile, TileGrid, TileTitle } from "@rallly/ui/tile"; -import Dashboard from "@/app/[locale]/(admin)/dashboard"; import type { Params } from "@/app/[locale]/types"; +import { + BillingPageIcon, + EventPageIcon, + HomePageIcon, + PollPageIcon, + PreferencesPageIcon, + ProfilePageIcon, +} from "@/app/components/page-icons"; import { PageContainer, PageContent, + PageDescription, PageHeader, - PageIcon, PageTitle, } from "@/app/components/page-layout"; +import { Trans } from "@/components/trans"; import { getTranslation } from "@/i18n/server"; -import { createSSRHelper } from "@/trpc/server/create-ssr-helper"; export default async function Page({ params }: { params: Params }) { - const { t } = await getTranslation(params.locale); - const helpers = await createSSRHelper(); - await helpers.dashboard.info.prefetch(); + await getTranslation(params.locale); + return ( - -
- - -
- - - - - - -
-
- - - -
-
-
+ + + + + + + + + + + + {/*
+

+ +

+ + + + + + + + +
*/} + +
+

+ +

+ + + + + + + + + + + + + + + + {/* + + + + + */} + +
+ +
+

+ +

+ + + + + + + + + + + + + + + + + + + + + + +
+
+
); } -export async function generateMetadata({ - params, -}: { - params: { locale: string }; -}) { - const { t } = await getTranslation(params.locale); +export async function generateMetadata() { + const { t } = await getTranslation(); return { title: t("home", { defaultValue: "Home", diff --git a/apps/web/src/app/[locale]/(admin)/polls/actions.ts b/apps/web/src/app/[locale]/(admin)/polls/actions.ts new file mode 100644 index 000000000..f5df9007f --- /dev/null +++ b/apps/web/src/app/[locale]/(admin)/polls/actions.ts @@ -0,0 +1,49 @@ +"use server"; + +import { prisma } from "@rallly/database"; +import { revalidatePath } from "next/cache"; + +import { auth } from "@/next-auth"; + +/** + * Deletes multiple polls by their IDs + * Only allows deletion of polls owned by the current user + */ +export async function deletePolls(pollIds: string[]) { + try { + // Get the current user session + const session = await auth(); + + if (!session?.user?.id) { + throw new Error("Unauthorized"); + } + + // Delete polls that belong to the current user + const result = await prisma.poll.updateMany({ + where: { + id: { + in: pollIds, + }, + userId: session.user.id, + }, + data: { + deleted: true, + deletedAt: new Date(), + }, + }); + + // Revalidate the polls page to reflect the changes + revalidatePath("/polls"); + + return { + success: true, + count: result.count, + }; + } catch (error) { + console.error("Failed to delete polls:", error); + return { + success: false, + error: "Failed to delete polls", + }; + } +} diff --git a/apps/web/src/app/[locale]/(admin)/polls/loading.tsx b/apps/web/src/app/[locale]/(admin)/polls/loading.tsx deleted file mode 100644 index f6c4a2fbc..000000000 --- a/apps/web/src/app/[locale]/(admin)/polls/loading.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { Spinner } from "@/components/spinner"; - -export default async function Loading() { - return ; -} diff --git a/apps/web/src/app/[locale]/(admin)/polls/page.tsx b/apps/web/src/app/[locale]/(admin)/polls/page.tsx index a663896b1..c4b158b48 100644 --- a/apps/web/src/app/[locale]/(admin)/polls/page.tsx +++ b/apps/web/src/app/[locale]/(admin)/polls/page.tsx @@ -1,53 +1,230 @@ -import { BarChart2Icon } from "lucide-react"; +import type { PollStatus } from "@rallly/database"; +import { Button } from "@rallly/ui/button"; +import { shortUrl } from "@rallly/utils/absolute-url"; +import { InboxIcon } from "lucide-react"; +import Link from "next/link"; +import { z } from "zod"; -import { UserPolls } from "@/app/[locale]/(admin)/polls/user-polls"; -import type { Params } from "@/app/[locale]/types"; +import { PollPageIcon } from "@/app/components/page-icons"; import { PageContainer, PageContent, + PageDescription, PageHeader, - PageIcon, PageTitle, } from "@/app/components/page-layout"; +import { CopyLinkButton } from "@/components/copy-link-button"; +import { + EmptyState, + EmptyStateDescription, + EmptyStateFooter, + EmptyStateIcon, + EmptyStateTitle, +} from "@/components/empty-state"; +import { Pagination } from "@/components/pagination"; +import { ParticipantAvatarBar } from "@/components/participant-avatar-bar"; +import { PollStatusIcon } from "@/components/poll-status-icon"; +import { + StackedList, + StackedListItem, + StackedListItemContent, +} from "@/components/stacked-list"; +import { Trans } from "@/components/trans"; +import { getPolls } from "@/data/get-polls"; import { getTranslation } from "@/i18n/server"; +import { requireUser } from "@/next-auth"; -export default async function Page({ - params, +import { PollsTabbedView } from "./polls-tabbed-view"; +import { SearchInput } from "./search-input"; + +const DEFAULT_PAGE_SIZE = 20; + +const pageSchema = z + .string() + .nullish() + .transform((val) => { + if (!val) return 1; + const parsed = parseInt(val, 10); + return isNaN(parsed) || parsed < 1 ? 1 : parsed; + }); + +const querySchema = z + .string() + .nullish() + .transform((val) => val?.trim() || undefined); + +const statusSchema = z + .enum(["live", "paused", "finalized"]) + .nullish() + .transform((val) => val || "live"); + +const pageSizeSchema = z + .string() + .nullish() + .transform((val) => { + if (!val) return DEFAULT_PAGE_SIZE; + const parsed = parseInt(val, 10); + return isNaN(parsed) || parsed < 1 + ? DEFAULT_PAGE_SIZE + : Math.min(parsed, 100); + }); + +// Combined schema for type inference +async function loadData({ + userId, + status = "live", + page = 1, + pageSize = DEFAULT_PAGE_SIZE, + q, }: { - params: Params; - children?: React.ReactNode; + userId: string; + status?: PollStatus; + page?: number; + pageSize?: number; + q?: string; }) { - const { t } = await getTranslation(params.locale); - return ( - - -
- - - - - {t("polls", { - defaultValue: "Polls", - })} - -
-
- - - -
- ); + const [{ total, data: polls }] = await Promise.all([ + getPolls({ userId, status, page, pageSize, q }), + ]); + + return { + polls, + total, + }; } -export async function generateMetadata({ - params, -}: { - params: { locale: string }; -}) { - const { t } = await getTranslation(params.locale); +export async function generateMetadata() { + const { t } = await getTranslation(); return { title: t("polls", { defaultValue: "Polls", }), }; } + +function PollsEmptyState() { + return ( + + + + + + + + + + + + + + + ); +} + +export default async function Page({ + searchParams, +}: { + searchParams: { [key: string]: string | string[] | undefined }; +}) { + const { userId } = await requireUser(); + + const parsedStatus = statusSchema.parse(searchParams.status); + const parsedPage = pageSchema.parse(searchParams.page); + const parsedPageSize = pageSizeSchema.parse(searchParams.pageSize); + const parsedQuery = querySchema.parse(searchParams.q); + + const { polls, total } = await loadData({ + userId, + status: parsedStatus, + page: parsedPage, + pageSize: parsedPageSize, + q: parsedQuery, + }); + + const totalPages = Math.ceil(total / parsedPageSize); + + return ( + +
+ + + + + + + + + +
+ +
+
+ + +
+ + {polls.length === 0 ? ( + + ) : ( + <> + + {polls.map((poll) => ( + +
+ + + + + {poll.title} + + + + + + +
+
+ ))} +
+ {totalPages > 1 ? ( + + ) : null} + + )} +
+
+
+
+ ); +} diff --git a/apps/web/src/app/[locale]/(admin)/polls/polls-tabbed-view.tsx b/apps/web/src/app/[locale]/(admin)/polls/polls-tabbed-view.tsx new file mode 100644 index 000000000..761c74a09 --- /dev/null +++ b/apps/web/src/app/[locale]/(admin)/polls/polls-tabbed-view.tsx @@ -0,0 +1,45 @@ +"use client"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@rallly/ui/page-tabs"; +import { useRouter, useSearchParams } from "next/navigation"; +import React from "react"; + +import { Trans } from "@/components/trans"; + +export function PollsTabbedView({ children }: { children: React.ReactNode }) { + const searchParams = useSearchParams(); + const name = "status"; + const router = useRouter(); + const handleTabChange = React.useCallback( + (value: string) => { + const params = new URLSearchParams(searchParams); + params.set(name, value); + + params.delete("page"); + + const newUrl = `?${params.toString()}`; + router.replace(newUrl, { scroll: false }); + }, + [name, router, searchParams], + ); + + const value = searchParams.get(name) ?? "live"; + + return ( + + + + + + + + + + + + + + {children} + + + ); +} diff --git a/apps/web/src/app/[locale]/(admin)/polls/search-input.tsx b/apps/web/src/app/[locale]/(admin)/polls/search-input.tsx new file mode 100644 index 000000000..e278ecf43 --- /dev/null +++ b/apps/web/src/app/[locale]/(admin)/polls/search-input.tsx @@ -0,0 +1,78 @@ +"use client"; + +import { Icon } from "@rallly/ui/icon"; +import { Input } from "@rallly/ui/input"; +import debounce from "lodash/debounce"; +import { SearchIcon } from "lucide-react"; +import { usePathname, useRouter, useSearchParams } from "next/navigation"; +import React from "react"; + +import { useTranslation } from "@/i18n/client"; + +export function SearchInput() { + const { t } = useTranslation(); + const searchParams = useSearchParams(); + const pathname = usePathname(); + const router = useRouter(); + + // Create a ref for the input element to maintain focus + const inputRef = React.useRef(null); + + // Get current search value from URL + const currentSearchValue = searchParams.get("q") || ""; + + // Track input value in state + const [inputValue, setInputValue] = React.useState(currentSearchValue); + + // Create a debounced function to update the URL + // eslint-disable-next-line react-hooks/exhaustive-deps + const debouncedUpdateUrl = React.useCallback( + debounce((value: string) => { + const params = new URLSearchParams(searchParams); + if (value) { + params.set("q", value); + } else { + params.delete("q"); + } + + params.delete("page"); + + router.replace(`${pathname}?${params.toString()}`, { scroll: false }); + }, 500), + [pathname, router, searchParams], + ); + + // Handle input changes + const handleChange = (e: React.ChangeEvent) => { + const newValue = e.target.value; + setInputValue(newValue); + debouncedUpdateUrl(newValue); + }; + + return ( +
{ + e.preventDefault(); + debouncedUpdateUrl.flush(); + }} + > +
+ + + +
+ +
+ ); +} diff --git a/apps/web/src/app/[locale]/(admin)/polls/user-polls.tsx b/apps/web/src/app/[locale]/(admin)/polls/user-polls.tsx deleted file mode 100644 index 3a34c8128..000000000 --- a/apps/web/src/app/[locale]/(admin)/polls/user-polls.tsx +++ /dev/null @@ -1,255 +0,0 @@ -"use client"; -import type { PollStatus } from "@rallly/database"; -import { cn } from "@rallly/ui"; -import { Badge } from "@rallly/ui/badge"; -import { Button } from "@rallly/ui/button"; -import { Icon } from "@rallly/ui/icon"; -import { RadioCards, RadioCardsItem } from "@rallly/ui/radio-pills"; -import { getCoreRowModel, useReactTable } from "@tanstack/react-table"; -import { CalendarPlusIcon, CheckIcon, LinkIcon, UserIcon } from "lucide-react"; -import Link from "next/link"; -import { useSearchParams } from "next/navigation"; -import React from "react"; -import useCopyToClipboard from "react-use/lib/useCopyToClipboard"; -import { z } from "zod"; - -import { GroupPollIcon } from "@/app/[locale]/(admin)/app-card"; -import { - EmptyState, - EmptyStateDescription, - EmptyStateIcon, - EmptyStateTitle, -} from "@/components/empty-state"; -import { PollStatusBadge } from "@/components/poll-status"; -import { Spinner } from "@/components/spinner"; -import { Trans } from "@/components/trans"; -import { VisibilityTrigger } from "@/components/visibility-trigger"; -import { trpc } from "@/trpc/client"; - -function PollCount({ count }: { count?: number }) { - return {count || 0}; -} - -function FilteredPolls({ status }: { status: PollStatus }) { - const { data, fetchNextPage, hasNextPage } = - trpc.polls.infiniteList.useInfiniteQuery( - { - status, - limit: 30, - }, - { - getNextPageParam: (lastPage) => lastPage.nextCursor, - keepPreviousData: true, - }, - ); - - if (!data) { - return ; - } - - return ( -
-
    - {data.pages.map((page, i) => ( -
  1. - -
  2. - ))} -
- {hasNextPage ? ( - - - - ) : null} -
- ); -} - -function PollStatusMenu({ - status, - onStatusChange, -}: { - status?: PollStatus; - onStatusChange?: (status: PollStatus) => void; -}) { - const { data: countByStatus, isFetching } = - trpc.polls.getCountByStatus.useQuery(); - - if (!countByStatus) { - return null; - } - - return ( - - - - - - - - - - - - - - {isFetching && } - - ); -} - -function useQueryParam(name: string) { - const searchParams = useSearchParams(); - return [ - searchParams?.get(name), - function (value: string) { - const newParams = new URLSearchParams(searchParams?.toString()); - newParams.set(name, value); - window.history.replaceState(null, "", `?${newParams.toString()}`); - }, - ] as const; -} - -const pollStatusSchema = z.enum(["live", "paused", "finalized"]).catch("live"); - -const pollStatusQueryKey = "status"; - -export function UserPolls() { - const [pollStatus, setPollStatus] = useQueryParam(pollStatusQueryKey); - const parsedPollStatus = pollStatusSchema.parse(pollStatus); - - return ( -
- - -
- ); -} - -function CopyLinkButton({ pollId }: { pollId: string }) { - const [, copy] = useCopyToClipboard(); - const [didCopy, setDidCopy] = React.useState(false); - - return ( - - ); -} - -function ParticipantCount({ count }: { count: number }) { - return ( -
- - - - {count} -
- ); -} - -function PollsListView({ - data, -}: { - data: { - id: string; - status: PollStatus; - title: string; - createdAt: Date; - user: { - id: string; - name: string; - } | null; - guestId?: string | null; - participants: { - id: string; - name: string; - }[]; - }[]; -}) { - const table = useReactTable({ - columns: [], - data, - getCoreRowModel: getCoreRowModel(), - }); - if (data?.length === 0) { - return ( - - - - - - - - - - - - ); - } - - return ( -
- {table.getRowModel().rows.map((row) => ( -
-
-
-
- -
-

- - {row.original.title} -

-
-
- - - - - - -
-
-
- -
-
- ))} -
- ); -} diff --git a/apps/web/src/app/[locale]/(admin)/pro-badge.tsx b/apps/web/src/app/[locale]/(admin)/pro-badge.tsx deleted file mode 100644 index 1f3715ef5..000000000 --- a/apps/web/src/app/[locale]/(admin)/pro-badge.tsx +++ /dev/null @@ -1,20 +0,0 @@ -"use client"; - -import { Badge } from "@rallly/ui/badge"; - -import { Trans } from "@/components/trans"; -import { useUser } from "@/components/user-provider"; - -export function ProBadge() { - const { user } = useUser(); - - if (user.tier !== "pro") { - return null; - } - - return ( - - - - ); -} diff --git a/apps/web/src/app/[locale]/(admin)/recently-updated-polls-grid.tsx b/apps/web/src/app/[locale]/(admin)/recently-updated-polls-grid.tsx new file mode 100644 index 000000000..0192464cd --- /dev/null +++ b/apps/web/src/app/[locale]/(admin)/recently-updated-polls-grid.tsx @@ -0,0 +1,82 @@ +import type { PollStatus } from "@rallly/database"; +import { Icon } from "@rallly/ui/icon"; +import { UsersIcon } from "lucide-react"; +import Link from "next/link"; + +import { ScheduledEventDisplay } from "@/components/poll/scheduled-event-display"; +import { PollStatusIcon } from "@/components/poll-status-icon"; +import { RelativeDate } from "@/components/relative-date"; +import { Trans } from "@/components/trans"; + +type Poll = { + id: string; + title: string; + status: PollStatus; + createdAt: Date; + updatedAt: Date; + participants: { + id: string; + name: string; + image?: string; + }[]; + dateOptions: { + first?: Date; + last?: Date; + count: number; + duration: number | number[]; + }; + event?: { + start: Date; + duration: number; + }; +}; + +type PollCardProps = { + poll: Poll; +}; + +export const PollCard = ({ poll }: PollCardProps) => { + return ( + +
+ +
+ +
+
+

+ {poll.title} +

+
+ +
+ + + + + {poll.participants.length}{" "} + + +
+
+ + ); +}; + +export const RecentlyUpdatedPollsGrid = ({ + children, +}: { + children: React.ReactNode; +}) => { + return ( +
+ {children} +
+ ); +}; diff --git a/apps/web/src/app/[locale]/(admin)/settings/billing/loading.tsx b/apps/web/src/app/[locale]/(admin)/settings/billing/loading.tsx deleted file mode 100644 index f6c4a2fbc..000000000 --- a/apps/web/src/app/[locale]/(admin)/settings/billing/loading.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { Spinner } from "@/components/spinner"; - -export default async function Loading() { - return ; -} diff --git a/apps/web/src/app/[locale]/(admin)/settings/billing/page.tsx b/apps/web/src/app/[locale]/(admin)/settings/billing/page.tsx index fa3e80a15..f8c0dce60 100644 --- a/apps/web/src/app/[locale]/(admin)/settings/billing/page.tsx +++ b/apps/web/src/app/[locale]/(admin)/settings/billing/page.tsx @@ -13,8 +13,12 @@ import { SparklesIcon, } from "lucide-react"; import Link from "next/link"; -import { notFound } from "next/navigation"; +import { notFound, redirect } from "next/navigation"; +import { + SettingsContent, + SettingsSection, +} from "@/app/[locale]/(admin)/settings/components/settings-layout"; import { DescriptionDetails, DescriptionList, @@ -29,7 +33,6 @@ import { } from "@/components/empty-state"; import { FormattedDate } from "@/components/formatted-date"; import { PayWallDialog } from "@/components/pay-wall-dialog"; -import { Settings, SettingsSection } from "@/components/settings/settings"; import { Trans } from "@/components/trans"; import { requireUser } from "@/next-auth"; import { isSelfHosted } from "@/utils/constants"; @@ -39,10 +42,10 @@ import { SubscriptionPrice } from "./components/subscription-price"; import { SubscriptionStatus } from "./components/subscription-status"; async function getData() { - const user = await requireUser(); + const { userId } = await requireUser(); const data = await prisma.user.findUnique({ - where: { id: user.id }, + where: { id: userId }, select: { customerId: true, subscription: { @@ -70,7 +73,7 @@ async function getData() { }); if (!data) { - throw new Error("User not found"); + redirect("/api/auth/invalid-session"); } return data; @@ -86,7 +89,7 @@ export default async function Page() { const { subscription } = data; return ( - + @@ -302,6 +305,6 @@ export default async function Page() {
- + ); } diff --git a/apps/web/src/components/settings/date-time-preferences.tsx b/apps/web/src/app/[locale]/(admin)/settings/components/date-time-preferences.tsx similarity index 100% rename from apps/web/src/components/settings/date-time-preferences.tsx rename to apps/web/src/app/[locale]/(admin)/settings/components/date-time-preferences.tsx diff --git a/apps/web/src/components/settings/language-preference.tsx b/apps/web/src/app/[locale]/(admin)/settings/components/language-preference.tsx similarity index 100% rename from apps/web/src/components/settings/language-preference.tsx rename to apps/web/src/app/[locale]/(admin)/settings/components/language-preference.tsx diff --git a/apps/web/src/components/settings/settings.tsx b/apps/web/src/app/[locale]/(admin)/settings/components/settings-layout.tsx similarity index 70% rename from apps/web/src/components/settings/settings.tsx rename to apps/web/src/app/[locale]/(admin)/settings/components/settings-layout.tsx index 244c58bf9..07eb8db77 100644 --- a/apps/web/src/components/settings/settings.tsx +++ b/apps/web/src/app/[locale]/(admin)/settings/components/settings-layout.tsx @@ -1,26 +1,7 @@ import { cn } from "@rallly/ui"; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@rallly/ui/card"; import { Tooltip, TooltipContent, TooltipTrigger } from "@rallly/ui/tooltip"; import { InfoIcon } from "lucide-react"; -export const Settings = ({ children }: React.PropsWithChildren) => { - return
{children}
; -}; - -export const SettingsHeader = ({ children }: React.PropsWithChildren) => { - return ( -
-

{children}

-
- ); -}; - export const SettingsContent = ({ children }: React.PropsWithChildren) => { return
{children}
; }; @@ -31,13 +12,15 @@ export const SettingsSection = (props: { children: React.ReactNode; }) => { return ( - - - {props.title} - {props.description} - - {props.children} - +
+
+

+ {props.title} +

+

{props.description}

+
+
{props.children}
+
); }; diff --git a/apps/web/src/app/[locale]/(admin)/settings/components/sign-out-button.tsx b/apps/web/src/app/[locale]/(admin)/settings/components/sign-out-button.tsx new file mode 100644 index 000000000..0efb4c2c8 --- /dev/null +++ b/apps/web/src/app/[locale]/(admin)/settings/components/sign-out-button.tsx @@ -0,0 +1,25 @@ +"use client"; + +import { Button } from "@rallly/ui/button"; +import { Icon } from "@rallly/ui/icon"; +import { LogOutIcon } from "lucide-react"; +import { signOut } from "next-auth/react"; + +import { Trans } from "@/components/trans"; + +export const SignOutButton = () => { + return ( + + ); +}; diff --git a/apps/web/src/app/[locale]/(admin)/settings/layout.tsx b/apps/web/src/app/[locale]/(admin)/settings/layout.tsx index 2e99fb1d8..a8c2a1658 100644 --- a/apps/web/src/app/[locale]/(admin)/settings/layout.tsx +++ b/apps/web/src/app/[locale]/(admin)/settings/layout.tsx @@ -1,5 +1,6 @@ import React from "react"; +import { SettingsPageIcon } from "@/app/components/page-icons"; import { PageContainer, PageContent, @@ -8,7 +9,8 @@ import { } from "@/app/components/page-layout"; import { getTranslation } from "@/i18n/server"; -import { SettingsMenu } from "./settings-menu"; +import { SignOutButton } from "./components/sign-out-button"; +import { SettingsLayout } from "./settings-menu"; export default async function ProfileLayout({ children, @@ -20,13 +22,22 @@ export default async function ProfileLayout({ return ( - {t("settings")} - - -
- +
+
+ + + {t("settings", { + defaultValue: "Settings", + })} + +
+
+ +
-
{children}
+ + + {children} ); diff --git a/apps/web/src/app/[locale]/(admin)/settings/loading.tsx b/apps/web/src/app/[locale]/(admin)/settings/loading.tsx index f6c4a2fbc..5b9b84a92 100644 --- a/apps/web/src/app/[locale]/(admin)/settings/loading.tsx +++ b/apps/web/src/app/[locale]/(admin)/settings/loading.tsx @@ -1,5 +1,11 @@ +import { RouterLoadingIndicator } from "@/components/router-loading-indicator"; import { Spinner } from "@/components/spinner"; export default async function Loading() { - return ; + return ( + <> + + + + ); } 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 3b48cb2cc..1ccec7e05 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 @@ -1,49 +1,39 @@ "use client"; -import Head from "next/head"; -import { DateTimePreferences } from "@/components/settings/date-time-preferences"; -import { LanguagePreference } from "@/components/settings/language-preference"; +import { DateTimePreferences } from "@/app/[locale]/(admin)/settings/components/date-time-preferences"; +import { LanguagePreference } from "@/app/[locale]/(admin)/settings/components/language-preference"; import { - Settings, SettingsContent, SettingsSection, -} from "@/components/settings/settings"; +} from "@/app/[locale]/(admin)/settings/components/settings-layout"; import { Trans } from "@/components/trans"; -import { useTranslation } from "@/i18n/client"; export function PreferencesPage() { - const { t } = useTranslation(); - return ( - - - - {t("settings")} - - } - description={ - - } - > - - -
- } - description={ - - } - > - - -
-
+ + } + description={ + + } + > + + +
+ } + 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 deleted file mode 100644 index f6c4a2fbc..000000000 --- a/apps/web/src/app/[locale]/(admin)/settings/profile/loading.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { Spinner } from "@/components/spinner"; - -export default async function Loading() { - return ; -} 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 4996ddef1..92bc67ee2 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,97 +1,71 @@ "use client"; import { Button } from "@rallly/ui/button"; import { DialogTrigger } from "@rallly/ui/dialog"; -import { LogOutIcon, TrashIcon } from "lucide-react"; -import Head from "next/head"; +import { TrashIcon } from "lucide-react"; -import { DeleteAccountDialog } from "@/app/[locale]/(admin)/settings/profile/delete-account-dialog"; -import { ProfileSettings } from "@/app/[locale]/(admin)/settings/profile/profile-settings"; -import { LogoutButton } from "@/app/components/logout-button"; import { - Settings, SettingsContent, SettingsSection, -} from "@/components/settings/settings"; +} from "@/app/[locale]/(admin)/settings/components/settings-layout"; +import { DeleteAccountDialog } from "@/app/[locale]/(admin)/settings/profile/delete-account-dialog"; +import { ProfileSettings } from "@/app/[locale]/(admin)/settings/profile/profile-settings"; import { Trans } from "@/components/trans"; import { useUser } from "@/components/user-provider"; -import { useTranslation } from "@/i18n/client"; import { ProfileEmailAddress } from "./profile-email-address"; export const ProfilePage = () => { - const { t } = useTranslation(); const { user } = useUser(); return ( - - - {t("profile")} - - - } - description={ - - } - > - - - - } - description={ - - } - > - - -
+ + } + description={ + + } + > + + + } + description={ + + } + > + + +
- } - description={ - - } - > - - - - - - {user.email ? ( - <> -
- } - description={ - - } - > - - - - - - - - ) : null} -
-
+ {user.email ? ( + <> +
+ } + description={ + + } + > + + + + + + + + ) : null} + ); }; diff --git a/apps/web/src/app/[locale]/(admin)/settings/settings-menu.tsx b/apps/web/src/app/[locale]/(admin)/settings/settings-menu.tsx index a49dd8a10..a88332152 100644 --- a/apps/web/src/app/[locale]/(admin)/settings/settings-menu.tsx +++ b/apps/web/src/app/[locale]/(admin)/settings/settings-menu.tsx @@ -1,35 +1,39 @@ "use client"; -import { Icon } from "@rallly/ui/icon"; -import { CreditCardIcon, Settings2Icon, UserIcon } from "lucide-react"; -import { Trans } from "react-i18next"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@rallly/ui/page-tabs"; +import Link from "next/link"; +import { usePathname } from "next/navigation"; -import { TabMenu, TabMenuItem } from "@/app/components/tab-menu"; +import { Trans } from "@/components/trans"; import { IfCloudHosted } from "@/contexts/environment"; -export function SettingsMenu() { +export function SettingsLayout({ children }: { children: React.ReactNode }) { + const pathname = usePathname(); + return ( - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + {children} + + ); } diff --git a/apps/web/src/app/[locale]/(admin)/sidebar.tsx b/apps/web/src/app/[locale]/(admin)/sidebar.tsx deleted file mode 100644 index 2262e7bff..000000000 --- a/apps/web/src/app/[locale]/(admin)/sidebar.tsx +++ /dev/null @@ -1,199 +0,0 @@ -"use client"; - -import { usePostHog } from "@rallly/posthog/client"; -import { cn } from "@rallly/ui"; -import { Badge } from "@rallly/ui/badge"; -import { Button } from "@rallly/ui/button"; -import { DialogTrigger } from "@rallly/ui/dialog"; -import { Icon } from "@rallly/ui/icon"; -import { - ArrowUpRightIcon, - BarChart2Icon, - CalendarIcon, - ChevronRightIcon, - HomeIcon, - LifeBuoyIcon, - LogInIcon, - PlusIcon, - Settings2Icon, - SparklesIcon, -} from "lucide-react"; -import Link from "next/link"; -import { usePathname } from "next/navigation"; - -import { OptimizedAvatarImage } from "@/components/optimized-avatar-image"; -import { PayWallDialog } from "@/components/pay-wall-dialog"; -import { Trans } from "@/components/trans"; -import { IfGuest, useUser } from "@/components/user-provider"; -import { IfFreeUser } from "@/contexts/plan"; -import type { IconComponent } from "@/types"; - -function NavItem({ - href, - children, - target, - icon: Icon, - current, -}: { - href: string; - target?: string; - icon: IconComponent; - children: React.ReactNode; - current?: boolean; -}) { - return ( - -