mirror of
https://github.com/lukevella/rallly.git
synced 2025-08-01 15:39:03 +02:00
♻️ Rename space folder (#1666)
This commit is contained in:
parent
aa721d9369
commit
5f76285f10
48 changed files with 58 additions and 448 deletions
|
@ -1,75 +0,0 @@
|
||||||
"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 "@/trpc/client";
|
|
||||||
|
|
||||||
export default function Dashboard() {
|
|
||||||
const { data } = trpc.dashboard.info.useQuery();
|
|
||||||
|
|
||||||
if (!data) {
|
|
||||||
return <Spinner />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div className="grid md:flex">
|
|
||||||
<AppCard className="basis-96">
|
|
||||||
<AppCardIcon>
|
|
||||||
<GroupPollIcon size="lg" />
|
|
||||||
</AppCardIcon>
|
|
||||||
<AppCardContent>
|
|
||||||
<div>
|
|
||||||
<AppCardName>
|
|
||||||
<Trans i18nKey="groupPoll" defaults="Group Poll" />
|
|
||||||
</AppCardName>
|
|
||||||
<AppCardDescription>
|
|
||||||
<Trans
|
|
||||||
i18nKey="groupPollDescription"
|
|
||||||
defaults="Share your availability with a group of people and find the best time to meet."
|
|
||||||
/>
|
|
||||||
</AppCardDescription>
|
|
||||||
</div>
|
|
||||||
</AppCardContent>
|
|
||||||
<AppCardFooter className="flex items-center justify-between gap-4">
|
|
||||||
<div className="inline-flex items-center gap-1 text-sm">
|
|
||||||
<Link
|
|
||||||
className="text-primary font-medium hover:underline"
|
|
||||||
href="/polls?status=live"
|
|
||||||
>
|
|
||||||
<Trans
|
|
||||||
i18nKey="activePollCount"
|
|
||||||
defaults="{activePollCount} Live"
|
|
||||||
values={{
|
|
||||||
activePollCount: data.activePollCount,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
<Button asChild>
|
|
||||||
<Link href="/new">
|
|
||||||
<Icon>
|
|
||||||
<PlusIcon />
|
|
||||||
</Icon>
|
|
||||||
<Trans i18nKey="create" defaults="Create" />
|
|
||||||
</Link>
|
|
||||||
</Button>
|
|
||||||
</AppCardFooter>
|
|
||||||
</AppCard>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,49 +0,0 @@
|
||||||
"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",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -3,11 +3,12 @@
|
||||||
import { getCoreRowModel, useReactTable } from "@tanstack/react-table";
|
import { getCoreRowModel, useReactTable } from "@tanstack/react-table";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
|
|
||||||
import type { ScheduledEvent } from "@/app/[locale]/(admin)/events/types";
|
|
||||||
import { Trans } from "@/components/trans";
|
import { Trans } from "@/components/trans";
|
||||||
import { generateGradient } from "@/utils/color-hash";
|
import { generateGradient } from "@/utils/color-hash";
|
||||||
import { useDayjs } from "@/utils/dayjs";
|
import { useDayjs } from "@/utils/dayjs";
|
||||||
|
|
||||||
|
import type { ScheduledEvent } from "./types";
|
||||||
|
|
||||||
export function EventList({ data }: { data: ScheduledEvent[] }) {
|
export function EventList({ data }: { data: ScheduledEvent[] }) {
|
||||||
const table = useReactTable({
|
const table = useReactTable({
|
||||||
data,
|
data,
|
|
@ -1,4 +1,3 @@
|
||||||
import { UserScheduledEvents } from "@/app/[locale]/(admin)/events/user-scheduled-events";
|
|
||||||
import type { Params } from "@/app/[locale]/types";
|
import type { Params } from "@/app/[locale]/types";
|
||||||
import { EventPageIcon } from "@/app/components/page-icons";
|
import { EventPageIcon } from "@/app/components/page-icons";
|
||||||
import {
|
import {
|
||||||
|
@ -11,6 +10,8 @@ import {
|
||||||
import { Trans } from "@/components/trans";
|
import { Trans } from "@/components/trans";
|
||||||
import { getTranslation } from "@/i18n/server";
|
import { getTranslation } from "@/i18n/server";
|
||||||
|
|
||||||
|
import { UserScheduledEvents } from "./user-scheduled-events";
|
||||||
|
|
||||||
export default async function Page({ params }: { params: Params }) {
|
export default async function Page({ params }: { params: Params }) {
|
||||||
await getTranslation(params.locale);
|
await getTranslation(params.locale);
|
||||||
return (
|
return (
|
|
@ -1,7 +1,6 @@
|
||||||
"use client";
|
"use client";
|
||||||
import { CalendarPlusIcon } from "lucide-react";
|
import { CalendarPlusIcon } from "lucide-react";
|
||||||
|
|
||||||
import { EventList } from "@/app/[locale]/(admin)/events/event-list";
|
|
||||||
import {
|
import {
|
||||||
EmptyState,
|
EmptyState,
|
||||||
EmptyStateDescription,
|
EmptyStateDescription,
|
||||||
|
@ -12,6 +11,8 @@ import { Spinner } from "@/components/spinner";
|
||||||
import { Trans } from "@/components/trans";
|
import { Trans } from "@/components/trans";
|
||||||
import { trpc } from "@/trpc/client";
|
import { trpc } from "@/trpc/client";
|
||||||
|
|
||||||
|
import { EventList } from "./event-list";
|
||||||
|
|
||||||
export function PastEvents() {
|
export function PastEvents() {
|
||||||
const { data } = trpc.scheduledEvents.list.useQuery({
|
const { data } = trpc.scheduledEvents.list.useQuery({
|
||||||
period: "past",
|
period: "past",
|
|
@ -1,7 +1,6 @@
|
||||||
"use client";
|
"use client";
|
||||||
import { CalendarPlusIcon } from "lucide-react";
|
import { CalendarPlusIcon } from "lucide-react";
|
||||||
|
|
||||||
import { EventList } from "@/app/[locale]/(admin)/events/event-list";
|
|
||||||
import {
|
import {
|
||||||
EmptyState,
|
EmptyState,
|
||||||
EmptyStateDescription,
|
EmptyStateDescription,
|
||||||
|
@ -12,6 +11,8 @@ import { Spinner } from "@/components/spinner";
|
||||||
import { Trans } from "@/components/trans";
|
import { Trans } from "@/components/trans";
|
||||||
import { trpc } from "@/trpc/client";
|
import { trpc } from "@/trpc/client";
|
||||||
|
|
||||||
|
import { EventList } from "./event-list";
|
||||||
|
|
||||||
export function UpcomingEvents() {
|
export function UpcomingEvents() {
|
||||||
const { data } = trpc.scheduledEvents.list.useQuery({ period: "upcoming" });
|
const { data } = trpc.scheduledEvents.list.useQuery({ period: "upcoming" });
|
||||||
|
|
|
@ -4,10 +4,11 @@ import { Tabs, TabsList, TabsTrigger } from "@rallly/ui/page-tabs";
|
||||||
import { useRouter, useSearchParams } from "next/navigation";
|
import { useRouter, useSearchParams } from "next/navigation";
|
||||||
import { z } from "zod";
|
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 { Trans } from "@/components/trans";
|
||||||
|
|
||||||
|
import { PastEvents } from "./past-events";
|
||||||
|
import { UpcomingEvents } from "./upcoming-events";
|
||||||
|
|
||||||
const eventPeriodSchema = z.enum(["upcoming", "past"]).catch("upcoming");
|
const eventPeriodSchema = z.enum(["upcoming", "past"]).catch("upcoming");
|
||||||
|
|
||||||
export function UserScheduledEvents() {
|
export function UserScheduledEvents() {
|
|
@ -3,12 +3,12 @@ import { Button } from "@rallly/ui/button";
|
||||||
import { SidebarInset, SidebarTrigger } from "@rallly/ui/sidebar";
|
import { SidebarInset, SidebarTrigger } from "@rallly/ui/sidebar";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|
||||||
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 { OptimizedAvatarImage } from "@/components/optimized-avatar-image";
|
||||||
import { getUser } from "@/data/get-user";
|
import { getUser } from "@/data/get-user";
|
||||||
import { CommandMenu } from "@/features/navigation/command-menu";
|
import { CommandMenu } from "@/features/navigation/command-menu";
|
||||||
|
|
||||||
|
import { AppSidebar } from "./components/sidebar/app-sidebar";
|
||||||
|
import { AppSidebarProvider } from "./components/sidebar/app-sidebar-provider";
|
||||||
import { TopBar, TopBarLeft, TopBarRight } from "./components/top-bar";
|
import { TopBar, TopBarLeft, TopBarRight } from "./components/top-bar";
|
||||||
|
|
||||||
export default async function Layout({
|
export default async function Layout({
|
|
@ -15,10 +15,6 @@ import {
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { notFound, redirect } from "next/navigation";
|
import { notFound, redirect } from "next/navigation";
|
||||||
|
|
||||||
import {
|
|
||||||
SettingsContent,
|
|
||||||
SettingsSection,
|
|
||||||
} from "@/app/[locale]/(admin)/settings/components/settings-layout";
|
|
||||||
import {
|
import {
|
||||||
DescriptionDetails,
|
DescriptionDetails,
|
||||||
DescriptionList,
|
DescriptionList,
|
||||||
|
@ -37,6 +33,10 @@ import { Trans } from "@/components/trans";
|
||||||
import { requireUser } from "@/next-auth";
|
import { requireUser } from "@/next-auth";
|
||||||
import { isSelfHosted } from "@/utils/constants";
|
import { isSelfHosted } from "@/utils/constants";
|
||||||
|
|
||||||
|
import {
|
||||||
|
SettingsContent,
|
||||||
|
SettingsSection,
|
||||||
|
} from "../components/settings-layout";
|
||||||
import { PaymentMethod } from "./components/payment-method";
|
import { PaymentMethod } from "./components/payment-method";
|
||||||
import { SubscriptionPrice } from "./components/subscription-price";
|
import { SubscriptionPrice } from "./components/subscription-price";
|
||||||
import { SubscriptionStatus } from "./components/subscription-status";
|
import { SubscriptionStatus } from "./components/subscription-status";
|
|
@ -1,12 +1,13 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { DateTimePreferences } from "@/app/[locale]/(admin)/settings/components/date-time-preferences";
|
import { Trans } from "@/components/trans";
|
||||||
import { LanguagePreference } from "@/app/[locale]/(admin)/settings/components/language-preference";
|
|
||||||
|
import { DateTimePreferences } from "../components/date-time-preferences";
|
||||||
|
import { LanguagePreference } from "../components/language-preference";
|
||||||
import {
|
import {
|
||||||
SettingsContent,
|
SettingsContent,
|
||||||
SettingsSection,
|
SettingsSection,
|
||||||
} from "@/app/[locale]/(admin)/settings/components/settings-layout";
|
} from "../components/settings-layout";
|
||||||
import { Trans } from "@/components/trans";
|
|
||||||
|
|
||||||
export function PreferencesPage() {
|
export function PreferencesPage() {
|
||||||
return (
|
return (
|
|
@ -3,16 +3,16 @@ import { Button } from "@rallly/ui/button";
|
||||||
import { DialogTrigger } from "@rallly/ui/dialog";
|
import { DialogTrigger } from "@rallly/ui/dialog";
|
||||||
import { TrashIcon } from "lucide-react";
|
import { TrashIcon } from "lucide-react";
|
||||||
|
|
||||||
import {
|
|
||||||
SettingsContent,
|
|
||||||
SettingsSection,
|
|
||||||
} 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 { Trans } from "@/components/trans";
|
||||||
import { useUser } from "@/components/user-provider";
|
import { useUser } from "@/components/user-provider";
|
||||||
|
|
||||||
|
import {
|
||||||
|
SettingsContent,
|
||||||
|
SettingsSection,
|
||||||
|
} from "../components/settings-layout";
|
||||||
|
import { DeleteAccountDialog } from "./delete-account-dialog";
|
||||||
import { ProfileEmailAddress } from "./profile-email-address";
|
import { ProfileEmailAddress } from "./profile-email-address";
|
||||||
|
import { ProfileSettings } from "./profile-settings";
|
||||||
|
|
||||||
export const ProfilePage = () => {
|
export const ProfilePage = () => {
|
||||||
const { user } = useUser();
|
const { user } = useUser();
|
|
@ -12,11 +12,12 @@ import { Input } from "@rallly/ui/input";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { ProfilePicture } from "@/app/[locale]/(admin)/settings/profile/profile-picture";
|
|
||||||
import { Trans } from "@/components/trans";
|
import { Trans } from "@/components/trans";
|
||||||
import { useUser } from "@/components/user-provider";
|
import { useUser } from "@/components/user-provider";
|
||||||
import { trpc } from "@/trpc/client";
|
import { trpc } from "@/trpc/client";
|
||||||
|
|
||||||
|
import { ProfilePicture } from "./profile-picture";
|
||||||
|
|
||||||
const profileSettingsFormData = z.object({
|
const profileSettingsFormData = z.object({
|
||||||
name: z.string().min(1).max(100),
|
name: z.string().min(1).max(100),
|
||||||
});
|
});
|
|
@ -17,30 +17,29 @@ import {
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
const pageIconVariants = cva(
|
const pageIconVariants = cva("inline-flex items-center justify-center", {
|
||||||
"inline-flex size-7 items-center justify-center rounded-lg",
|
variants: {
|
||||||
{
|
color: {
|
||||||
variants: {
|
darkGray: "bg-gray-700 text-white",
|
||||||
color: {
|
indigo: "bg-indigo-500 text-white",
|
||||||
darkGray: "bg-gray-700 text-white",
|
gray: "bg-gray-200 text-gray-600",
|
||||||
indigo: "bg-indigo-500 text-white",
|
lime: "bg-lime-500 text-white",
|
||||||
gray: "bg-gray-200 text-gray-600",
|
blue: "bg-blue-500 text-white",
|
||||||
lime: "bg-lime-500 text-white",
|
rose: "bg-rose-500 text-white",
|
||||||
blue: "bg-blue-500 text-white",
|
purple: "bg-purple-500 text-white",
|
||||||
rose: "bg-rose-500 text-white",
|
|
||||||
purple: "bg-purple-500 text-white",
|
|
||||||
},
|
|
||||||
size: {
|
|
||||||
sm: "size-5",
|
|
||||||
md: "size-7",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
size: {
|
||||||
color: "gray",
|
sm: "size-6 [&_svg]:size-3 rounded-md",
|
||||||
size: "md",
|
md: "size-7 [&_svg]:size-4 rounded-lg",
|
||||||
|
lg: "size-9 [&_svg]:size-5 rounded-xl",
|
||||||
|
xl: "size-10 [&_svg]:size-5 rounded-xl",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
defaultVariants: {
|
||||||
|
color: "gray",
|
||||||
|
size: "md",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
type PageIconVariantProps = VariantProps<typeof pageIconVariants>;
|
type PageIconVariantProps = VariantProps<typeof pageIconVariants>;
|
||||||
|
|
||||||
|
@ -53,7 +52,7 @@ export function PageIcon({
|
||||||
} & PageIconVariantProps) {
|
} & PageIconVariantProps) {
|
||||||
return (
|
return (
|
||||||
<span className={pageIconVariants({ color, size })}>
|
<span className={pageIconVariants({ color, size })}>
|
||||||
<Slot className="size-4">{children}</Slot>
|
<Slot>{children}</Slot>
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -111,9 +110,9 @@ export function CreatePageIcon() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PollPageIcon() {
|
export function PollPageIcon(props: PageIconVariantProps) {
|
||||||
return (
|
return (
|
||||||
<PageIcon color="purple" size="md">
|
<PageIcon color="purple" size="md" {...props}>
|
||||||
<BarChart2Icon />
|
<BarChart2Icon />
|
||||||
</PageIcon>
|
</PageIcon>
|
||||||
);
|
);
|
||||||
|
|
|
@ -13,8 +13,8 @@ import Link from "next/link";
|
||||||
import { useParams, usePathname } from "next/navigation";
|
import { useParams, usePathname } from "next/navigation";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import { GroupPollIcon } from "@/app/[locale]/(admin)/app-card";
|
|
||||||
import { LogoutButton } from "@/app/components/logout-button";
|
import { LogoutButton } from "@/app/components/logout-button";
|
||||||
|
import { PollPageIcon } from "@/app/components/page-icons";
|
||||||
import { InviteDialog } from "@/components/invite-dialog";
|
import { InviteDialog } from "@/components/invite-dialog";
|
||||||
import { LoginLink } from "@/components/login-link";
|
import { LoginLink } from "@/components/login-link";
|
||||||
import {
|
import {
|
||||||
|
@ -67,7 +67,7 @@ const Layout = ({ children }: React.PropsWithChildren) => {
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<div>
|
<div>
|
||||||
<GroupPollIcon size="xs" />
|
<PollPageIcon size="sm" />
|
||||||
</div>
|
</div>
|
||||||
<h1 className="truncate text-sm font-semibold">{poll.title}</h1>
|
<h1 className="truncate text-sm font-semibold">{poll.title}</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,130 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import type { ReactNode } from "react";
|
|
||||||
import { createContext, useCallback, useMemo, useState } from "react";
|
|
||||||
|
|
||||||
import { useRequiredContext } from "@/components/use-required-context";
|
|
||||||
|
|
||||||
import { PollSelectionActionBar } from "./poll-selection-action-bar";
|
|
||||||
|
|
||||||
type RowSelectionState = Record<string, boolean>;
|
|
||||||
|
|
||||||
type PollSelectionContextType = {
|
|
||||||
selectedPolls: RowSelectionState;
|
|
||||||
setSelectedPolls: (selection: RowSelectionState) => void;
|
|
||||||
selectPolls: (pollIds: string[]) => void;
|
|
||||||
unselectPolls: (pollIds: string[]) => void;
|
|
||||||
togglePollSelection: (pollId: string) => void;
|
|
||||||
clearSelection: () => void;
|
|
||||||
isSelected: (pollId: string) => boolean;
|
|
||||||
getSelectedPollIds: () => string[];
|
|
||||||
selectedCount: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
const PollSelectionContext = createContext<PollSelectionContextType | null>(
|
|
||||||
null,
|
|
||||||
);
|
|
||||||
|
|
||||||
type PollSelectionProviderProps = {
|
|
||||||
children: ReactNode;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const PollSelectionProvider = ({
|
|
||||||
children,
|
|
||||||
}: PollSelectionProviderProps) => {
|
|
||||||
const [selectedPolls, setSelectedPolls] = useState<RowSelectionState>({});
|
|
||||||
|
|
||||||
const selectPolls = useCallback((pollIds: string[]) => {
|
|
||||||
setSelectedPolls((prev) => {
|
|
||||||
const newSelection = { ...prev };
|
|
||||||
pollIds.forEach((id) => {
|
|
||||||
newSelection[id] = true;
|
|
||||||
});
|
|
||||||
return newSelection;
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const unselectPolls = useCallback(
|
|
||||||
(pollIds: string[]) =>
|
|
||||||
setSelectedPolls((prev) => {
|
|
||||||
const newSelection = { ...prev };
|
|
||||||
pollIds.forEach((id) => {
|
|
||||||
delete newSelection[id];
|
|
||||||
});
|
|
||||||
return newSelection;
|
|
||||||
}),
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
const togglePollSelection = useCallback(
|
|
||||||
(pollId: string) =>
|
|
||||||
setSelectedPolls((prev) => {
|
|
||||||
const newSelection = { ...prev };
|
|
||||||
if (newSelection[pollId]) {
|
|
||||||
delete newSelection[pollId];
|
|
||||||
} else {
|
|
||||||
newSelection[pollId] = true;
|
|
||||||
}
|
|
||||||
return newSelection;
|
|
||||||
}),
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
const clearSelection = useCallback(() => setSelectedPolls({}), []);
|
|
||||||
|
|
||||||
const isSelected = useCallback(
|
|
||||||
(pollId: string) => Boolean(selectedPolls[pollId]),
|
|
||||||
[selectedPolls],
|
|
||||||
);
|
|
||||||
|
|
||||||
const getSelectedPollIds = useCallback(
|
|
||||||
() => Object.keys(selectedPolls),
|
|
||||||
[selectedPolls],
|
|
||||||
);
|
|
||||||
|
|
||||||
const selectedCount = useMemo(
|
|
||||||
() => Object.keys(selectedPolls).length,
|
|
||||||
[selectedPolls],
|
|
||||||
);
|
|
||||||
|
|
||||||
const value = useMemo(
|
|
||||||
() => ({
|
|
||||||
selectedPolls,
|
|
||||||
setSelectedPolls,
|
|
||||||
selectPolls,
|
|
||||||
unselectPolls,
|
|
||||||
togglePollSelection,
|
|
||||||
clearSelection,
|
|
||||||
isSelected,
|
|
||||||
getSelectedPollIds,
|
|
||||||
selectedCount,
|
|
||||||
}),
|
|
||||||
[
|
|
||||||
selectedPolls,
|
|
||||||
setSelectedPolls,
|
|
||||||
selectPolls,
|
|
||||||
unselectPolls,
|
|
||||||
togglePollSelection,
|
|
||||||
clearSelection,
|
|
||||||
isSelected,
|
|
||||||
getSelectedPollIds,
|
|
||||||
selectedCount,
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PollSelectionContext.Provider value={value}>
|
|
||||||
{children}
|
|
||||||
<PollSelectionActionBar />
|
|
||||||
</PollSelectionContext.Provider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const usePollSelection = () => {
|
|
||||||
const context = useRequiredContext(
|
|
||||||
PollSelectionContext,
|
|
||||||
"usePollSelection must be used within a PollSelectionProvider",
|
|
||||||
);
|
|
||||||
|
|
||||||
return context;
|
|
||||||
};
|
|
|
@ -1,142 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import {
|
|
||||||
ActionBarContainer,
|
|
||||||
ActionBarContent,
|
|
||||||
ActionBarGroup,
|
|
||||||
ActionBarPortal,
|
|
||||||
} from "@rallly/ui/action-bar";
|
|
||||||
import { Button } from "@rallly/ui/button";
|
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from "@rallly/ui/dialog";
|
|
||||||
import { AnimatePresence, motion } from "framer-motion";
|
|
||||||
import { TrashIcon } from "lucide-react";
|
|
||||||
import * as React from "react";
|
|
||||||
|
|
||||||
import { deletePolls } from "@/app/[locale]/(admin)/polls/actions";
|
|
||||||
import { Trans } from "@/components/trans";
|
|
||||||
|
|
||||||
import { usePollSelection } from "./context";
|
|
||||||
|
|
||||||
const MActionBar = motion(ActionBarContainer);
|
|
||||||
|
|
||||||
export function PollSelectionActionBar() {
|
|
||||||
const { selectedCount, clearSelection, getSelectedPollIds } =
|
|
||||||
usePollSelection();
|
|
||||||
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = React.useState(false);
|
|
||||||
const [isDeleting, setIsDeleting] = React.useState(false);
|
|
||||||
|
|
||||||
const handleDelete = async () => {
|
|
||||||
const selectedPollIds = getSelectedPollIds();
|
|
||||||
if (selectedPollIds.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsDeleting(true);
|
|
||||||
try {
|
|
||||||
const result = await deletePolls(selectedPollIds);
|
|
||||||
|
|
||||||
if (result.success) {
|
|
||||||
setIsDeleteDialogOpen(false);
|
|
||||||
clearSelection();
|
|
||||||
} else {
|
|
||||||
// Handle error case
|
|
||||||
console.error("Failed to delete polls:", result.error);
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
setIsDeleting(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ActionBarPortal>
|
|
||||||
<AnimatePresence>
|
|
||||||
{selectedCount > 0 && (
|
|
||||||
<MActionBar
|
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
exit={{ opacity: 0, y: 20 }}
|
|
||||||
transition={{
|
|
||||||
type: "spring",
|
|
||||||
stiffness: 500,
|
|
||||||
damping: 30,
|
|
||||||
mass: 0.5,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ActionBarContent>
|
|
||||||
<span className="text-sm font-medium">
|
|
||||||
<Trans
|
|
||||||
i18nKey="selectedPolls"
|
|
||||||
defaults="{count} {count, plural, one {poll} other {polls}} selected"
|
|
||||||
values={{ count: selectedCount }}
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
</ActionBarContent>
|
|
||||||
<ActionBarGroup>
|
|
||||||
<Button
|
|
||||||
variant="actionBar"
|
|
||||||
onClick={clearSelection}
|
|
||||||
className="text-action-bar-foreground"
|
|
||||||
>
|
|
||||||
<Trans i18nKey="unselectAll" defaults="Unselect All" />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="destructive"
|
|
||||||
onClick={() => setIsDeleteDialogOpen(true)}
|
|
||||||
>
|
|
||||||
<TrashIcon className="size-4" />
|
|
||||||
<Trans i18nKey="delete" defaults="Delete" />
|
|
||||||
</Button>
|
|
||||||
</ActionBarGroup>
|
|
||||||
</MActionBar>
|
|
||||||
)}
|
|
||||||
</AnimatePresence>
|
|
||||||
|
|
||||||
{/* Delete Polls Dialog */}
|
|
||||||
<Dialog open={isDeleteDialogOpen} onOpenChange={setIsDeleteDialogOpen}>
|
|
||||||
<DialogContent size="sm">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>
|
|
||||||
<Trans i18nKey="deletePolls" defaults="Delete Polls" />
|
|
||||||
</DialogTitle>
|
|
||||||
</DialogHeader>
|
|
||||||
<p className="text-sm">
|
|
||||||
{selectedCount === 1 ? (
|
|
||||||
<Trans
|
|
||||||
i18nKey="deletePollDescription"
|
|
||||||
defaults="Are you sure you want to delete this poll? This action cannot be undone."
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Trans
|
|
||||||
i18nKey="deletePollsDescription"
|
|
||||||
defaults="Are you sure you want to delete these {count} polls? This action cannot be undone."
|
|
||||||
values={{ count: selectedCount }}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
<DialogFooter>
|
|
||||||
<Button
|
|
||||||
onClick={() => {
|
|
||||||
setIsDeleteDialogOpen(false);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Trans i18nKey="cancel" defaults="Cancel" />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="destructive"
|
|
||||||
onClick={handleDelete}
|
|
||||||
loading={isDeleting}
|
|
||||||
>
|
|
||||||
<Trans i18nKey="delete" defaults="Delete" />
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
</ActionBarPortal>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -4,7 +4,7 @@ import { CheckIcon, PlusIcon, ZapIcon } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { Trans } from "react-i18next/TransWithoutContext";
|
import { Trans } from "react-i18next/TransWithoutContext";
|
||||||
|
|
||||||
import { GroupPollIcon } from "@/app/[locale]/(admin)/app-card";
|
import { PollPageIcon } from "@/app/components/page-icons";
|
||||||
import { getGuestPolls } from "@/features/quick-create/lib/get-guest-polls";
|
import { getGuestPolls } from "@/features/quick-create/lib/get-guest-polls";
|
||||||
import { getTranslation } from "@/i18n/server";
|
import { getTranslation } from "@/i18n/server";
|
||||||
|
|
||||||
|
@ -50,10 +50,10 @@ export async function QuickCreateWidget() {
|
||||||
<li key={poll.id}>
|
<li key={poll.id}>
|
||||||
<Link
|
<Link
|
||||||
href={`/poll/${poll.id}`}
|
href={`/poll/${poll.id}`}
|
||||||
className="flex items-center gap-3 rounded-xl border bg-white p-3 hover:bg-gray-50 active:bg-gray-100"
|
className="flex items-center gap-3 rounded-2xl border bg-white p-3 hover:bg-gray-50 active:bg-gray-100"
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<GroupPollIcon size="lg" />
|
<PollPageIcon size="xl" />
|
||||||
</div>
|
</div>
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<div className="truncate font-medium">{poll.title}</div>
|
<div className="truncate font-medium">{poll.title}</div>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue