diff --git a/.env.development b/.env.development
index 0bdb018f4..a2ada3c7e 100644
--- a/.env.development
+++ b/.env.development
@@ -5,8 +5,11 @@ SECRET_PASSWORD=abcdef1234567890abcdef1234567890
# Example: https://example.com
NEXT_PUBLIC_BASE_URL=http://localhost:3000
+# NEXTAUTH_URL should be the same as NEXT_PUBLIC_BASE_URL
+NEXTAUTH_URL=http://localhost:3000
+
# A connection string to your Postgres database
-DATABASE_URL="postgres://postgres:postgres@rallly_db:5450/rallly"
+DATABASE_URL="postgres://postgres:postgres@localhost:5450/rallly"
# Required to be able to send emails
SUPPORT_EMAIL=support@rallly.co
diff --git a/.gitignore b/.gitignore
index a7b4e7411..4a6e49685 100644
--- a/.gitignore
+++ b/.gitignore
@@ -26,10 +26,7 @@ yarn-error.log*
# local env files
.env
-.env.local
-.env.development.local
-.env.test.local
-.env.production.local
+.env*.local
# ts
tsconfig.tsbuildinfo
diff --git a/apps/web/next.config.js b/apps/web/next.config.js
index 1c9288d57..4f2cb73a0 100644
--- a/apps/web/next.config.js
+++ b/apps/web/next.config.js
@@ -46,11 +46,6 @@ const nextConfig = {
destination: "/settings/profile",
permanent: true,
},
- {
- source: "/",
- destination: "/polls",
- permanent: false,
- },
];
},
};
diff --git a/apps/web/package.json b/apps/web/package.json
index b57070904..a903aa954 100644
--- a/apps/web/package.json
+++ b/apps/web/package.json
@@ -12,7 +12,7 @@
"lint:tsc": "tsc --noEmit",
"i18n:scan": "i18next-scanner --config i18next-scanner.config.js",
"prettier": "prettier --write ./src",
- "test:integration": "playwright test",
+ "test:integration": "NODE_ENV=test playwright test",
"test:unit": "vitest run",
"test": "yarn test:unit && yarn test:e2e",
"test:codegen": "playwright codegen http://localhost:3000",
diff --git a/apps/web/playwright.config.ts b/apps/web/playwright.config.ts
index 0219183cf..66a69c5fb 100644
--- a/apps/web/playwright.config.ts
+++ b/apps/web/playwright.config.ts
@@ -1,12 +1,11 @@
+import { loadEnvConfig } from "@next/env";
import { devices, PlaywrightTestConfig } from "@playwright/test";
-import dotenv from "dotenv";
-import path from "path";
const ci = process.env.CI === "true";
-dotenv.config({ path: path.resolve(__dirname, ".env.test") });
+loadEnvConfig(process.cwd());
-const port = process.env.PORT || 3000;
+const port = process.env.PORT || 3002;
// Set webServer.url and use.baseURL with the location of the WebServer respecting the correct set port
const baseURL = `http://localhost:${port}`;
diff --git a/apps/web/public/locales/en/app.json b/apps/web/public/locales/en/app.json
index 886d145d3..1ef1f4117 100644
--- a/apps/web/public/locales/en/app.json
+++ b/apps/web/public/locales/en/app.json
@@ -15,7 +15,6 @@
"copied": "Copied",
"createAnAccount": "Create an account",
"createdBy": "by {name}",
- "createPoll": "Create poll",
"delete": "Delete",
"deleteDate": "Delete date",
"deletedPoll": "Deleted poll",
@@ -209,9 +208,6 @@
"continueAs": "Continue as",
"pageMovedDescription": "Redirecting to {newUrl}",
"notRegistered": "Don't have an account? Register",
- "comingSoon": "Coming Soon",
- "integrations": "Integrations",
- "contacts": "Contacts",
"unlockFeatures": "Unlock all Pro features.",
"pollStatusFinalized": "Finalized",
"share": "Share",
@@ -222,7 +218,6 @@
"aboutGuestDescription": "Profile settings are not available for guest users. <0>Sign 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.",
@@ -233,14 +228,8 @@
"autoTimeZoneHelp": "Enable this setting to automatically adjust event times to each participant's local time zone.",
"commentsDisabled": "Comments have been disabled",
"allParticipants": "All Participants",
- "host": "Host",
- "created": "Created",
- "pollStatus": "Status",
"pollsListAll": "All",
- "pollsListMine": "Mine",
- "pollsListOther": "Other",
"noParticipantsDescription": "Click Share to invite participants",
- "back": "Back",
"timeShownIn": "Times shown in {timeZone}",
"pollStatusPausedDescription": "Votes cannot be submitted or edited at this time",
"eventHostTitle": "Manage Access",
@@ -261,5 +250,18 @@
"advancedSettingsDescription": "Hide participants, hide scores, require participant email address.",
"keepPollsIndefinitely": "Keep Polls Indefinitely",
"keepPollsIndefinitelyDescription": "Inactive polls will not be auto-deleted.",
- "verificationCodeSentTo": "We sent a verification code to {{ email }}"
+ "verificationCodeSentTo": "We sent a verification code to {{ email }}",
+ "home": "Home",
+ "groupPoll": "Group Poll",
+ "groupPollDescription": "Share your availability with a group of people and find the best time to meet.",
+ "create": "Create",
+ "upcoming": "Upcoming",
+ "past": "Past",
+ "copyLink": "Copy Link",
+ "upcomingEventsEmptyStateTitle": "No Upcoming Events",
+ "upcomingEventsEmptyStateDescription": "When you schedule events, they will appear here.",
+ "pastEventsEmptyStateTitle": "No Past Events",
+ "pastEventsEmptyStateDescription": "When you schedule events, they will appear here.",
+ "activePollCount": "{{activePollCount}} Live",
+ "createPoll": "Create poll"
}
diff --git a/apps/web/src/app/[locale]/(admin)/app-card.tsx b/apps/web/src/app/[locale]/(admin)/app-card.tsx
new file mode 100644
index 000000000..7e6b58290
--- /dev/null
+++ b/apps/web/src/app/[locale]/(admin)/app-card.tsx
@@ -0,0 +1,115 @@
+import { cn } from "@rallly/ui";
+import { BarChart2Icon } from "lucide-react";
+import React from "react";
+
+import { Squircle } from "@/app/components/squircle";
+
+export function AppCard({
+ children,
+ className,
+}: {
+ children?: React.ReactNode;
+ className?: string;
+}) {
+ return (
+
+ {children}
+
+ );
+}
+
+export function AppCardContent({ children }: { children?: React.ReactNode }) {
+ return {children}
;
+}
+
+export function GroupPollIcon({
+ size = "md",
+}: {
+ size?: "xs" | "sm" | "md" | "lg";
+}) {
+ return (
+
+
+
+ );
+}
+
+export function AppCardIcon({
+ children,
+ className,
+}: {
+ children?: React.ReactNode;
+ className?: string;
+}) {
+ return (
+
+ {children}
+
+ );
+}
+
+export function AppCardName({
+ children,
+ className,
+}: {
+ children?: React.ReactNode;
+ className?: string;
+}) {
+ return {children}
;
+}
+
+export function AppCardDescription({
+ children,
+ className,
+}: {
+ children?: React.ReactNode;
+ className?: string;
+}) {
+ return (
+
+ {children}
+
+ );
+}
+
+export function AppCardFooter({
+ children,
+ className,
+}: {
+ children?: React.ReactNode;
+ className?: string;
+}) {
+ return {children}
;
+}
diff --git a/apps/web/src/app/[locale]/(admin)/dashboard.tsx b/apps/web/src/app/[locale]/(admin)/dashboard.tsx
new file mode 100644
index 000000000..94432c595
--- /dev/null
+++ b/apps/web/src/app/[locale]/(admin)/dashboard.tsx
@@ -0,0 +1,75 @@
+"use client";
+import { Button } from "@rallly/ui/button";
+import { Icon } from "@rallly/ui/icon";
+import { PlusIcon } from "lucide-react";
+import Link from "next/link";
+
+import {
+ AppCard,
+ AppCardContent,
+ AppCardDescription,
+ AppCardFooter,
+ AppCardIcon,
+ AppCardName,
+ GroupPollIcon,
+} from "@/app/[locale]/(admin)/app-card";
+import { Spinner } from "@/components/spinner";
+import { Trans } from "@/components/trans";
+import { trpc } from "@/utils/trpc/client";
+
+export default function Dashboard() {
+ const { data } = trpc.dashboard.info.useQuery();
+
+ if (!data) {
+ return ;
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/apps/web/src/app/[locale]/(admin)/events/event-list.tsx b/apps/web/src/app/[locale]/(admin)/events/event-list.tsx
new file mode 100644
index 000000000..fdc35632c
--- /dev/null
+++ b/apps/web/src/app/[locale]/(admin)/events/event-list.tsx
@@ -0,0 +1,79 @@
+"use client";
+
+import { Card, CardContent } from "@rallly/ui/card";
+import { getCoreRowModel, useReactTable } from "@tanstack/react-table";
+import dayjs from "dayjs";
+
+import { ScheduledEvent } from "@/app/[locale]/(admin)/events/types";
+import { Trans } from "@/components/trans";
+import { generateGradient } from "@/utils/color-hash";
+import { useDayjs } from "@/utils/dayjs";
+
+export function EventList({ data }: { data: ScheduledEvent[] }) {
+ const table = useReactTable({
+ data,
+ columns: [],
+ getCoreRowModel: getCoreRowModel(),
+ });
+
+ const { adjustTimeZone } = useDayjs();
+ return (
+
+
+ {table.getRowModel().rows.map((row) => {
+ const start = adjustTimeZone(
+ row.original.start,
+ !row.original.timeZone,
+ );
+
+ const end = adjustTimeZone(
+ dayjs(row.original.start).add(row.original.duration, "minutes"),
+ !row.original.timeZone,
+ );
+ return (
+ -
+
+
+
+
+
+
+
+
+
+
+ {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
new file mode 100644
index 000000000..626ea6e8e
--- /dev/null
+++ b/apps/web/src/app/[locale]/(admin)/events/layout.tsx
@@ -0,0 +1,3 @@
+export default function Layout({ children }: { children?: React.ReactNode }) {
+ return {children}
;
+}
diff --git a/apps/web/src/app/[locale]/(admin)/new/page.tsx b/apps/web/src/app/[locale]/(admin)/events/page.tsx
similarity index 52%
rename from apps/web/src/app/[locale]/(admin)/new/page.tsx
rename to apps/web/src/app/[locale]/(admin)/events/page.tsx
index f37b7f5fe..901e0cbd0 100644
--- a/apps/web/src/app/[locale]/(admin)/new/page.tsx
+++ b/apps/web/src/app/[locale]/(admin)/events/page.tsx
@@ -1,34 +1,28 @@
-import { Button } from "@rallly/ui/button";
-import { Icon } from "@rallly/ui/icon";
-import { ArrowLeftIcon } from "lucide-react";
-import Link from "next/link";
-import { Trans } from "react-i18next/TransWithoutContext";
-
+import { UserScheduledEvents } from "@/app/[locale]/(admin)/events/user-scheduled-events";
import { Params } from "@/app/[locale]/types";
import {
PageContainer,
PageContent,
PageHeader,
+ PageTitle,
} from "@/app/components/page-layout";
import { getTranslation } from "@/app/i18n";
-import { CreatePoll } from "@/components/create-poll";
export default async function Page({ params }: { params: Params }) {
const { t } = await getTranslation(params.locale);
return (
-
+
+
+ {t("events", {
+ defaultValue: "Events",
+ })}
+
+
-
+
);
@@ -41,6 +35,8 @@ export async function generateMetadata({
}) {
const { t } = await getTranslation(params.locale);
return {
- title: t("newPoll"),
+ title: t("events", {
+ defaultValue: "Events",
+ }),
};
}
diff --git a/apps/web/src/app/[locale]/(admin)/events/past-events.tsx b/apps/web/src/app/[locale]/(admin)/events/past-events.tsx
new file mode 100644
index 000000000..7bbac0727
--- /dev/null
+++ b/apps/web/src/app/[locale]/(admin)/events/past-events.tsx
@@ -0,0 +1,47 @@
+"use client";
+import { CalendarPlusIcon } from "lucide-react";
+
+import { EventList } from "@/app/[locale]/(admin)/events/event-list";
+import {
+ EmptyState,
+ EmptyStateDescription,
+ EmptyStateIcon,
+ EmptyStateTitle,
+} from "@/app/components/empty-state";
+import { Spinner } from "@/components/spinner";
+import { Trans } from "@/components/trans";
+import { trpc } from "@/utils/trpc/client";
+
+export function PastEvents() {
+ const { data } = trpc.scheduledEvents.list.useQuery({
+ period: "past",
+ });
+
+ if (!data) {
+ return ;
+ }
+
+ if (data.length === 0) {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+
+ return ;
+}
diff --git a/apps/web/src/app/[locale]/(admin)/events/types.ts b/apps/web/src/app/[locale]/(admin)/events/types.ts
new file mode 100644
index 000000000..92f1411b7
--- /dev/null
+++ b/apps/web/src/app/[locale]/(admin)/events/types.ts
@@ -0,0 +1,8 @@
+export type ScheduledEvent = {
+ id: string;
+ title: string;
+ start: Date;
+ duration: number;
+ timeZone: string | null;
+ participants: { name: string }[];
+};
diff --git a/apps/web/src/app/[locale]/(admin)/events/upcoming-events.tsx b/apps/web/src/app/[locale]/(admin)/events/upcoming-events.tsx
new file mode 100644
index 000000000..437212b1c
--- /dev/null
+++ b/apps/web/src/app/[locale]/(admin)/events/upcoming-events.tsx
@@ -0,0 +1,45 @@
+"use client";
+import { CalendarPlusIcon } from "lucide-react";
+
+import { EventList } from "@/app/[locale]/(admin)/events/event-list";
+import {
+ EmptyState,
+ EmptyStateDescription,
+ EmptyStateIcon,
+ EmptyStateTitle,
+} from "@/app/components/empty-state";
+import { Spinner } from "@/components/spinner";
+import { Trans } from "@/components/trans";
+import { trpc } from "@/utils/trpc/client";
+
+export function UpcomingEvents() {
+ const { data } = trpc.scheduledEvents.list.useQuery({ period: "upcoming" });
+
+ if (!data) {
+ return ;
+ }
+
+ if (data.length === 0) {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+
+ return ;
+}
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
new file mode 100644
index 000000000..87ed31642
--- /dev/null
+++ b/apps/web/src/app/[locale]/(admin)/events/user-scheduled-events.tsx
@@ -0,0 +1,43 @@
+"use client";
+
+import { RadioCards, RadioCardsItem } from "@rallly/ui/radio-pills";
+import { useSearchParams } from "next/navigation";
+import { z } from "zod";
+
+import { PastEvents } from "@/app/[locale]/(admin)/events/past-events";
+import { Trans } from "@/components/trans";
+
+import { UpcomingEvents } from "./upcoming-events";
+
+const eventPeriodSchema = z.enum(["upcoming", "past"]).catch("upcoming");
+
+export function UserScheduledEvents() {
+ const searchParams = useSearchParams();
+ const period = eventPeriodSchema.parse(searchParams?.get("period"));
+
+ return (
+
+
+ {
+ const newParams = new URLSearchParams(searchParams?.toString());
+ newParams.set("period", value);
+ window.history.pushState(null, "", `?${newParams.toString()}`);
+ }}
+ >
+
+
+
+
+
+
+
+
+
+ {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 b68bd73f8..cb36d708a 100644
--- a/apps/web/src/app/[locale]/(admin)/layout.tsx
+++ b/apps/web/src/app/[locale]/(admin)/layout.tsx
@@ -11,11 +11,10 @@ export default async function Layout({
children: React.ReactNode;
}) {
return (
-
-
+
@@ -23,9 +22,12 @@ export default async function Layout({
-
);
}
diff --git a/apps/web/src/app/[locale]/(admin)/menu/page.tsx b/apps/web/src/app/[locale]/(admin)/menu/page.tsx
index c1f69cc82..7623c0f99 100644
--- a/apps/web/src/app/[locale]/(admin)/menu/page.tsx
+++ b/apps/web/src/app/[locale]/(admin)/menu/page.tsx
@@ -1,9 +1,27 @@
-import { Sidebar } from "@/app/[locale]/(admin)/sidebar";
+import { Trans } from "react-i18next/TransWithoutContext";
-export default function Page() {
+import { Sidebar } from "@/app/[locale]/(admin)/sidebar";
+import { Params } from "@/app/[locale]/types";
+import {
+ PageContainer,
+ PageContent,
+ PageHeader,
+ PageTitle,
+} from "@/app/components/page-layout";
+import { getTranslation } from "@/app/i18n";
+
+export default async function Page({ params }: { params: Params }) {
+ const { t } = await getTranslation(params.locale);
return (
-
-
-
+
+
+
+
+
+
+
+
+
+
);
}
diff --git a/apps/web/src/app/[locale]/(admin)/mobile-navigation.tsx b/apps/web/src/app/[locale]/(admin)/mobile-navigation.tsx
index a2890c7ff..14d98faad 100644
--- a/apps/web/src/app/[locale]/(admin)/mobile-navigation.tsx
+++ b/apps/web/src/app/[locale]/(admin)/mobile-navigation.tsx
@@ -1,27 +1,77 @@
"use client";
+import { Slot } from "@radix-ui/react-slot";
+import { cn } from "@rallly/ui";
import { Button } from "@rallly/ui/button";
+import {
+ BarChart2Icon,
+ CalendarIcon,
+ HomeIcon,
+ MenuIcon,
+ PlusIcon,
+} from "lucide-react";
import Link from "next/link";
import { usePathname } from "next/navigation";
-import { MobileMenuButton } from "@/app/[locale]/(admin)/menu/menu-button";
-import { CurrentUserAvatar } from "@/components/user";
+function MobileNavigationIcon({ children }: { children: React.ReactNode }) {
+ return (
+
+ {children}
+
+ );
+}
+
+function MobileNavigationItem({
+ children,
+ href,
+}: {
+ href: string;
+ children?: React.ReactNode;
+}) {
+ const pathname = usePathname();
+ return (
+
+ {children}
+
+ );
+}
export function MobileNavigation() {
- const pathname = usePathname();
-
- const isOpen = pathname === "/menu";
-
return (
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
);
}
diff --git a/apps/web/src/app/[locale]/(admin)/page.tsx b/apps/web/src/app/[locale]/(admin)/page.tsx
new file mode 100644
index 000000000..fd7aab034
--- /dev/null
+++ b/apps/web/src/app/[locale]/(admin)/page.tsx
@@ -0,0 +1,49 @@
+import { HomeIcon } from "lucide-react";
+import { Trans } from "react-i18next/TransWithoutContext";
+
+import Dashboard from "@/app/[locale]/(admin)/dashboard";
+import { Params } from "@/app/[locale]/types";
+import {
+ PageContainer,
+ PageContent,
+ PageHeader,
+ PageIcon,
+ PageTitle,
+} from "@/app/components/page-layout";
+import { getTranslation } from "@/app/i18n";
+
+export default async function Page({ params }: { params: Params }) {
+ const { t } = await getTranslation(params.locale);
+ return (
+
+ );
+}
+
+export async function generateMetadata({
+ params,
+}: {
+ params: { locale: string };
+}) {
+ const { t } = await getTranslation(params.locale);
+ return {
+ title: t("home", {
+ defaultValue: "Home",
+ }),
+ };
+}
diff --git a/apps/web/src/app/[locale]/(admin)/polls/[[...list]]/columns.tsx b/apps/web/src/app/[locale]/(admin)/polls/[[...list]]/columns.tsx
deleted file mode 100644
index 87e6e4f5e..000000000
--- a/apps/web/src/app/[locale]/(admin)/polls/[[...list]]/columns.tsx
+++ /dev/null
@@ -1,145 +0,0 @@
-import { PollStatus } from "@rallly/database";
-import { Button } from "@rallly/ui/button";
-import { Icon } from "@rallly/ui/icon";
-import { Tooltip, TooltipContent, TooltipTrigger } from "@rallly/ui/tooltip";
-import { createColumnHelper } from "@tanstack/react-table";
-import dayjs from "dayjs";
-import { BarChart2Icon } from "lucide-react";
-import Link from "next/link";
-import React from "react";
-import { useTranslation } from "react-i18next";
-
-import { PollStatusBadge } from "@/components/poll-status";
-import { Trans } from "@/components/trans";
-import { UserAvatar } from "@/components/user";
-import { useUser } from "@/components/user-provider";
-import { useDayjs } from "@/utils/dayjs";
-
-export type PollData = {
- id: string;
- status: PollStatus;
- title: string;
- createdAt: Date;
- participants: { name: string }[];
- timeZone: string | null;
- userId: string;
- user: {
- name: string;
- id: string;
- } | null;
- event: {
- start: Date;
- duration: number;
- } | null;
-};
-
-const columnHelper = createColumnHelper
();
-
-export const usePollColumns = () => {
- const { t } = useTranslation();
- const { adjustTimeZone } = useDayjs();
- const { user } = useUser();
- return React.useMemo(
- () => [
- columnHelper.accessor("title", {
- id: "title",
- header: t("title"),
- size: 400,
- cell: ({ row }) => {
- return (
-
-
-
-
- {row.original.title}
-
- );
- },
- }),
- columnHelper.accessor("user", {
- header: () => (
-
- {t("host", { defaultValue: "Host" })}
-
- ),
- size: 75,
- cell: ({ getValue }) => {
- const isYou = getValue()?.id === user.id;
- return (
-
-
-
-
-
-
- {isYou ? t("you") : getValue()?.name ?? t("guest")}
-
-
-
- );
- },
- }),
- columnHelper.accessor("createdAt", {
- header: () => ,
- cell: ({ row }) => {
- const { createdAt } = row.original;
- return (
-
-
-
- );
- },
- }),
- columnHelper.accessor("status", {
- header: t("pollStatus", { defaultValue: "Status" }),
- cell: ({ row }) => {
- return (
-
- {row.original.event ? (
-
-
-
-
-
- {adjustTimeZone(
- row.original.event.start,
- !row.original.timeZone,
- ).format(row.original.event.duration === 0 ? "LL" : "LLLL")}
-
-
- ) : (
-
- )}
-
- );
- },
- }),
-
- columnHelper.accessor("participants", {
- header: () => null,
- cell: ({ row }) => {
- if (row.original.userId !== user.id) {
- return null;
- }
-
- return (
-
- );
- },
- }),
- ],
- [adjustTimeZone, t, user.id],
- );
-};
diff --git a/apps/web/src/app/[locale]/(admin)/polls/[[...list]]/loading.tsx b/apps/web/src/app/[locale]/(admin)/polls/[[...list]]/loading.tsx
deleted file mode 100644
index 351081a7f..000000000
--- a/apps/web/src/app/[locale]/(admin)/polls/[[...list]]/loading.tsx
+++ /dev/null
@@ -1,3 +0,0 @@
-export default function Loader() {
- return null;
-}
diff --git a/apps/web/src/app/[locale]/(admin)/polls/[[...list]]/page.tsx b/apps/web/src/app/[locale]/(admin)/polls/[[...list]]/page.tsx
deleted file mode 100644
index fc3dd94a5..000000000
--- a/apps/web/src/app/[locale]/(admin)/polls/[[...list]]/page.tsx
+++ /dev/null
@@ -1,23 +0,0 @@
-import { PollsList } from "@/app/[locale]/(admin)/polls/[[...list]]/polls-list";
-import { Params } from "@/app/[locale]/types";
-import { getTranslation } from "@/app/i18n";
-
-interface PageParams extends Params {
- list?: string;
-}
-
-export default async function Page({ params }: { params: PageParams }) {
- const list = params.list ? params.list[0] : "all";
- return ;
-}
-
-export async function generateMetadata({
- params,
-}: {
- params: { locale: string };
-}) {
- const { t } = await getTranslation(params.locale);
- return {
- title: t("polls"),
- };
-}
diff --git a/apps/web/src/app/[locale]/(admin)/polls/[[...list]]/polls-folders.tsx b/apps/web/src/app/[locale]/(admin)/polls/[[...list]]/polls-folders.tsx
deleted file mode 100644
index e19e95bdb..000000000
--- a/apps/web/src/app/[locale]/(admin)/polls/[[...list]]/polls-folders.tsx
+++ /dev/null
@@ -1,23 +0,0 @@
-"use client";
-
-import {
- ResponsiveMenu,
- ResponsiveMenuItem,
-} from "@/app/components/responsive-menu";
-import { Trans } from "@/components/trans";
-
-export function PollFolders() {
- return (
-
-
-
-
-
-
-
-
-
-
-
- );
-}
diff --git a/apps/web/src/app/[locale]/(admin)/polls/[[...list]]/polls-list.tsx b/apps/web/src/app/[locale]/(admin)/polls/[[...list]]/polls-list.tsx
deleted file mode 100644
index aef979858..000000000
--- a/apps/web/src/app/[locale]/(admin)/polls/[[...list]]/polls-list.tsx
+++ /dev/null
@@ -1,137 +0,0 @@
-"use client";
-import { Button } from "@rallly/ui/button";
-import { Card } from "@rallly/ui/card";
-import { Icon } from "@rallly/ui/icon";
-import { PaginationState } from "@tanstack/react-table";
-import { BarChart2Icon, PlusIcon } from "lucide-react";
-import Link from "next/link";
-import { usePathname, useRouter, useSearchParams } from "next/navigation";
-import React from "react";
-import { useTranslation } from "react-i18next";
-
-import {
- EmptyState,
- EmptyStateDescription,
- EmptyStateFooter,
- EmptyStateIcon,
- EmptyStateTitle,
-} from "@/app/components/empty-state";
-import { Spinner } from "@/components/spinner";
-import { Table } from "@/components/table";
-import { Trans } from "@/components/trans";
-import { trpc } from "@/utils/trpc/client";
-
-import { PollData, usePollColumns } from "./columns";
-
-function PollsEmptyState() {
- const { t } = useTranslation();
- return (
-
-
-
-
-
- {t("noPolls", { defaultValue: "No Polls" })}
-
- {t("noPollsDescription")}
-
-
-
-
- );
-}
-
-export function PollsList({ list }: { list?: string }) {
- const searchParams = useSearchParams();
- const pathname = usePathname();
-
- const router = useRouter();
- const pagination = React.useMemo(
- () => ({
- pageIndex: (Number(searchParams?.get("page")) || 1) - 1,
- pageSize: Number(searchParams?.get("pageSize")) || 10,
- }),
- [searchParams],
- );
-
- // const sorting = React.useMemo(() => {
- // const id = searchParams?.get("sort");
- // const desc = searchParams?.get("desc");
- // if (!id) {
- // return [{ id: "createdAt", desc: true }];
- // }
- // return [{ id, desc: desc === "desc" }];
- // }, [searchParams]);
-
- const { data, isFetching } = trpc.polls.paginatedList.useQuery(
- { list, pagination },
- {
- staleTime: Infinity,
- cacheTime: Infinity,
- keepPreviousData: true,
- },
- );
- const columns = usePollColumns();
-
- if (!data) {
- // return a table using components
- return (
-
-
-
- );
- }
-
- return (
-
- {data.total ? (
-
- {
- // const newSorting =
- // typeof updater === "function" ? updater(sorting) : updater;
-
- // const current = new URLSearchParams(searchParams ?? undefined);
- // const sortColumn = newSorting[0];
- // if (sortColumn === undefined) {
- // current.delete("sort");
- // current.delete("desc");
- // } else {
- // current.set("sort", sortColumn.id);
- // current.set("desc", sortColumn.desc ? "desc" : "asc");
- // }
- // // current.set("pageSize", String(newPagination.pageSize));
- // router.replace(`${pathname}?${current.toString()}`);
- // }}
- onPaginationChange={(updater) => {
- const newPagination =
- typeof updater === "function" ? updater(pagination) : updater;
-
- const current = new URLSearchParams(searchParams ?? undefined);
- current.set("page", String(newPagination.pageIndex + 1));
- // current.set("pageSize", String(newPagination.pageSize));
- router.replace(`${pathname}?${current.toString()}`);
- }}
- columns={columns}
- />
-
- ) : (
-
- )}
-
- );
-}
diff --git a/apps/web/src/app/[locale]/(admin)/polls/layout.tsx b/apps/web/src/app/[locale]/(admin)/polls/layout.tsx
deleted file mode 100644
index 245d57936..000000000
--- a/apps/web/src/app/[locale]/(admin)/polls/layout.tsx
+++ /dev/null
@@ -1,48 +0,0 @@
-import { Button } from "@rallly/ui/button";
-import { Icon } from "@rallly/ui/icon";
-import { PlusIcon } from "lucide-react";
-import Link from "next/link";
-
-import { PollFolders } from "@/app/[locale]/(admin)/polls/[[...list]]/polls-folders";
-import { Params } from "@/app/[locale]/types";
-import {
- PageContainer,
- PageContent,
- PageHeader,
- PageTitle,
-} from "@/app/components/page-layout";
-import { getTranslation } from "@/app/i18n";
-
-interface PageParams extends Params {
- list?: string;
-}
-
-export default async function Layout({
- children,
- params,
-}: {
- children?: React.ReactNode;
- params: PageParams;
-}) {
- const { t } = await getTranslation(params.locale);
- return (
-
-
-
-
{t("polls")}
-
-
-
-
-
- {children}
-
-
- );
-}
diff --git a/apps/web/src/app/[locale]/(admin)/polls/page.tsx b/apps/web/src/app/[locale]/(admin)/polls/page.tsx
new file mode 100644
index 000000000..497bcd49a
--- /dev/null
+++ b/apps/web/src/app/[locale]/(admin)/polls/page.tsx
@@ -0,0 +1,53 @@
+import { BarChart2Icon } from "lucide-react";
+
+import { UserPolls } from "@/app/[locale]/(admin)/polls/user-polls";
+import { Params } from "@/app/[locale]/types";
+import {
+ PageContainer,
+ PageContent,
+ PageHeader,
+ PageIcon,
+ PageTitle,
+} from "@/app/components/page-layout";
+import { getTranslation } from "@/app/i18n";
+
+export default async function Page({
+ params,
+}: {
+ params: Params;
+ children?: React.ReactNode;
+}) {
+ const { t } = await getTranslation(params.locale);
+ return (
+
+
+
+
+
+
+
+ {t("polls", {
+ defaultValue: "Polls",
+ })}
+
+
+
+
+
+
+
+ );
+}
+
+export async function generateMetadata({
+ params,
+}: {
+ params: { locale: string };
+}) {
+ const { t } = await getTranslation(params.locale);
+ return {
+ title: t("polls", {
+ defaultValue: "Polls",
+ }),
+ };
+}
diff --git a/apps/web/src/app/[locale]/(admin)/polls/user-polls.tsx b/apps/web/src/app/[locale]/(admin)/polls/user-polls.tsx
new file mode 100644
index 000000000..795c53f9a
--- /dev/null
+++ b/apps/web/src/app/[locale]/(admin)/polls/user-polls.tsx
@@ -0,0 +1,237 @@
+"use client";
+import { PollStatus } from "@rallly/database";
+import { cn } from "@rallly/ui";
+import { Icon } from "@rallly/ui/icon";
+import { RadioCards, RadioCardsItem } from "@rallly/ui/radio-pills";
+import { getCoreRowModel, useReactTable } from "@tanstack/react-table";
+import dayjs from "dayjs";
+import { CalendarPlusIcon, CheckIcon, LinkIcon, UserIcon } from "lucide-react";
+import Link from "next/link";
+import { useSearchParams } from "next/navigation";
+import React from "react";
+import useCopyToClipboard from "react-use/lib/useCopyToClipboard";
+import { z } from "zod";
+
+import { GroupPollIcon } from "@/app/[locale]/(admin)/app-card";
+import {
+ EmptyState,
+ EmptyStateDescription,
+ EmptyStateIcon,
+ EmptyStateTitle,
+} from "@/app/components/empty-state";
+import { PollStatusBadge } from "@/components/poll-status";
+import { Spinner } from "@/components/spinner";
+import { Trans } from "@/components/trans";
+import { trpc } from "@/utils/trpc/client";
+
+function PollCount({ count }: { count?: number }) {
+ return {count || 0};
+}
+
+function FilteredPolls({ status }: { status: PollStatus }) {
+ const { data, isFetching } = trpc.polls.list.useQuery(
+ {
+ status,
+ },
+ {
+ keepPreviousData: true,
+ },
+ );
+
+ if (!data) {
+ return ;
+ }
+
+ return (
+
+ );
+}
+
+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);
+
+ if (didCopy) {
+ return (
+
+
+
+
+ );
+ }
+
+ return (
+
+ );
+}
+
+function ParticipantCount({ count }: { count: number }) {
+ return (
+
+
+
+
+ {count}
+
+ );
+}
+
+function PollsListView({
+ data,
+}: {
+ data: {
+ id: string;
+ status: PollStatus;
+ title: string;
+ createdAt: Date;
+ userId: string;
+ participants: {
+ id: string;
+ name: string;
+ }[];
+ }[];
+}) {
+ const table = useReactTable({
+ columns: [],
+ data,
+ getCoreRowModel: getCoreRowModel(),
+ });
+ if (data?.length === 0) {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+
+ return (
+
+ {table.getRowModel().rows.map((row) => (
+
+
+
+
+
+
+ {row.original.title}
+
+
+
+
+
+
+ ))}
+
+ );
+}
diff --git a/apps/web/src/app/[locale]/(admin)/settings/layout.tsx b/apps/web/src/app/[locale]/(admin)/settings/layout.tsx
index 9fe77296d..3d621b6ae 100644
--- a/apps/web/src/app/[locale]/(admin)/settings/layout.tsx
+++ b/apps/web/src/app/[locale]/(admin)/settings/layout.tsx
@@ -22,8 +22,8 @@ export default async function ProfileLayout({
{t("settings")}
-
-
+
+
{children}
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 17edc69e4..a49dd8a10 100644
--- a/apps/web/src/app/[locale]/(admin)/settings/settings-menu.tsx
+++ b/apps/web/src/app/[locale]/(admin)/settings/settings-menu.tsx
@@ -4,35 +4,32 @@ import { Icon } from "@rallly/ui/icon";
import { CreditCardIcon, Settings2Icon, UserIcon } from "lucide-react";
import { Trans } from "react-i18next";
-import {
- ResponsiveMenu,
- ResponsiveMenuItem,
-} from "@/app/components/responsive-menu";
+import { TabMenu, TabMenuItem } from "@/app/components/tab-menu";
import { IfCloudHosted } from "@/contexts/environment";
export function SettingsMenu() {
return (
-
-
+
+
-
-
+
+
-
+
-
+
-
+
-
+
);
}
diff --git a/apps/web/src/app/[locale]/(admin)/sidebar.tsx b/apps/web/src/app/[locale]/(admin)/sidebar.tsx
index 99c52af75..056362422 100644
--- a/apps/web/src/app/[locale]/(admin)/sidebar.tsx
+++ b/apps/web/src/app/[locale]/(admin)/sidebar.tsx
@@ -6,16 +6,14 @@ import { Icon } from "@rallly/ui/icon";
import {
ArrowUpRightIcon,
BarChart2Icon,
- BlocksIcon,
- BookMarkedIcon,
CalendarIcon,
ChevronRightIcon,
+ HomeIcon,
LifeBuoyIcon,
LogInIcon,
PlusIcon,
Settings2Icon,
SparklesIcon,
- UsersIcon,
} from "lucide-react";
import Link from "next/link";
import { usePathname } from "next/navigation";
@@ -46,18 +44,12 @@ function NavItem({
target={target}
className={cn(
current
- ? "bg-gray-200 text-gray-800"
- : "text-gray-700 hover:bg-gray-200 active:bg-gray-300",
- "group flex items-center gap-x-2.5 rounded-md px-3 py-2 text-sm font-semibold leading-6",
+ ? "text-foreground bg-gray-200"
+ : "text-muted-foreground border-transparent hover:bg-gray-200 focus:bg-gray-300",
+ "group flex items-center gap-x-3 rounded-md px-3 py-2 text-sm font-semibold leading-6",
)}
>
-
+
{children}
);
@@ -70,56 +62,49 @@ export function Sidebar() {