♻️ Rename space folder (#1666)

This commit is contained in:
Luke Vella 2025-04-14 17:50:17 +01:00 committed by GitHub
parent aa721d9369
commit 5f76285f10
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
48 changed files with 58 additions and 448 deletions

View file

@ -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>
);
}

View file

@ -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",
};
}
}

View file

@ -3,11 +3,12 @@
import { getCoreRowModel, useReactTable } from "@tanstack/react-table";
import dayjs from "dayjs";
import type { ScheduledEvent } from "@/app/[locale]/(admin)/events/types";
import { Trans } from "@/components/trans";
import { generateGradient } from "@/utils/color-hash";
import { useDayjs } from "@/utils/dayjs";
import type { ScheduledEvent } from "./types";
export function EventList({ data }: { data: ScheduledEvent[] }) {
const table = useReactTable({
data,

View file

@ -1,4 +1,3 @@
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 {
@ -11,6 +10,8 @@ import {
import { Trans } from "@/components/trans";
import { getTranslation } from "@/i18n/server";
import { UserScheduledEvents } from "./user-scheduled-events";
export default async function Page({ params }: { params: Params }) {
await getTranslation(params.locale);
return (

View file

@ -1,7 +1,6 @@
"use client";
import { CalendarPlusIcon } from "lucide-react";
import { EventList } from "@/app/[locale]/(admin)/events/event-list";
import {
EmptyState,
EmptyStateDescription,
@ -12,6 +11,8 @@ import { Spinner } from "@/components/spinner";
import { Trans } from "@/components/trans";
import { trpc } from "@/trpc/client";
import { EventList } from "./event-list";
export function PastEvents() {
const { data } = trpc.scheduledEvents.list.useQuery({
period: "past",

View file

@ -1,7 +1,6 @@
"use client";
import { CalendarPlusIcon } from "lucide-react";
import { EventList } from "@/app/[locale]/(admin)/events/event-list";
import {
EmptyState,
EmptyStateDescription,
@ -12,6 +11,8 @@ import { Spinner } from "@/components/spinner";
import { Trans } from "@/components/trans";
import { trpc } from "@/trpc/client";
import { EventList } from "./event-list";
export function UpcomingEvents() {
const { data } = trpc.scheduledEvents.list.useQuery({ period: "upcoming" });

View file

@ -4,10 +4,11 @@ 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 { PastEvents } from "./past-events";
import { UpcomingEvents } from "./upcoming-events";
const eventPeriodSchema = z.enum(["upcoming", "past"]).catch("upcoming");
export function UserScheduledEvents() {

View file

@ -3,12 +3,12 @@ import { Button } from "@rallly/ui/button";
import { SidebarInset, SidebarTrigger } from "@rallly/ui/sidebar";
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 { getUser } from "@/data/get-user";
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";
export default async function Layout({

View file

@ -15,10 +15,6 @@ import {
import Link from "next/link";
import { notFound, redirect } from "next/navigation";
import {
SettingsContent,
SettingsSection,
} from "@/app/[locale]/(admin)/settings/components/settings-layout";
import {
DescriptionDetails,
DescriptionList,
@ -37,6 +33,10 @@ import { Trans } from "@/components/trans";
import { requireUser } from "@/next-auth";
import { isSelfHosted } from "@/utils/constants";
import {
SettingsContent,
SettingsSection,
} from "../components/settings-layout";
import { PaymentMethod } from "./components/payment-method";
import { SubscriptionPrice } from "./components/subscription-price";
import { SubscriptionStatus } from "./components/subscription-status";

View file

@ -1,12 +1,13 @@
"use client";
import { DateTimePreferences } from "@/app/[locale]/(admin)/settings/components/date-time-preferences";
import { LanguagePreference } from "@/app/[locale]/(admin)/settings/components/language-preference";
import { Trans } from "@/components/trans";
import { DateTimePreferences } from "../components/date-time-preferences";
import { LanguagePreference } from "../components/language-preference";
import {
SettingsContent,
SettingsSection,
} from "@/app/[locale]/(admin)/settings/components/settings-layout";
import { Trans } from "@/components/trans";
} from "../components/settings-layout";
export function PreferencesPage() {
return (

View file

@ -3,16 +3,16 @@ import { Button } from "@rallly/ui/button";
import { DialogTrigger } from "@rallly/ui/dialog";
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 { 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 { ProfileSettings } from "./profile-settings";
export const ProfilePage = () => {
const { user } = useUser();

View file

@ -12,11 +12,12 @@ import { Input } from "@rallly/ui/input";
import { useForm } from "react-hook-form";
import { z } from "zod";
import { ProfilePicture } from "@/app/[locale]/(admin)/settings/profile/profile-picture";
import { Trans } from "@/components/trans";
import { useUser } from "@/components/user-provider";
import { trpc } from "@/trpc/client";
import { ProfilePicture } from "./profile-picture";
const profileSettingsFormData = z.object({
name: z.string().min(1).max(100),
});

View file

@ -17,30 +17,29 @@ import {
} from "lucide-react";
import React from "react";
const pageIconVariants = cva(
"inline-flex size-7 items-center justify-center rounded-lg",
{
variants: {
color: {
darkGray: "bg-gray-700 text-white",
indigo: "bg-indigo-500 text-white",
gray: "bg-gray-200 text-gray-600",
lime: "bg-lime-500 text-white",
blue: "bg-blue-500 text-white",
rose: "bg-rose-500 text-white",
purple: "bg-purple-500 text-white",
},
size: {
sm: "size-5",
md: "size-7",
},
const pageIconVariants = cva("inline-flex items-center justify-center", {
variants: {
color: {
darkGray: "bg-gray-700 text-white",
indigo: "bg-indigo-500 text-white",
gray: "bg-gray-200 text-gray-600",
lime: "bg-lime-500 text-white",
blue: "bg-blue-500 text-white",
rose: "bg-rose-500 text-white",
purple: "bg-purple-500 text-white",
},
defaultVariants: {
color: "gray",
size: "md",
size: {
sm: "size-6 [&_svg]:size-3 rounded-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>;
@ -53,7 +52,7 @@ export function PageIcon({
} & PageIconVariantProps) {
return (
<span className={pageIconVariants({ color, size })}>
<Slot className="size-4">{children}</Slot>
<Slot>{children}</Slot>
</span>
);
}
@ -111,9 +110,9 @@ export function CreatePageIcon() {
);
}
export function PollPageIcon() {
export function PollPageIcon(props: PageIconVariantProps) {
return (
<PageIcon color="purple" size="md">
<PageIcon color="purple" size="md" {...props}>
<BarChart2Icon />
</PageIcon>
);

View file

@ -13,8 +13,8 @@ import Link from "next/link";
import { useParams, usePathname } from "next/navigation";
import React from "react";
import { GroupPollIcon } from "@/app/[locale]/(admin)/app-card";
import { LogoutButton } from "@/app/components/logout-button";
import { PollPageIcon } from "@/app/components/page-icons";
import { InviteDialog } from "@/components/invite-dialog";
import { LoginLink } from "@/components/login-link";
import {
@ -67,7 +67,7 @@ const Layout = ({ children }: React.PropsWithChildren) => {
</Button>
)}
<div>
<GroupPollIcon size="xs" />
<PollPageIcon size="sm" />
</div>
<h1 className="truncate text-sm font-semibold">{poll.title}</h1>
</div>

View file

@ -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;
};

View file

@ -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>
);
}

View file

@ -4,7 +4,7 @@ import { CheckIcon, PlusIcon, ZapIcon } from "lucide-react";
import Link from "next/link";
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 { getTranslation } from "@/i18n/server";
@ -50,10 +50,10 @@ export async function QuickCreateWidget() {
<li key={poll.id}>
<Link
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>
<GroupPollIcon size="lg" />
<PollPageIcon size="xl" />
</div>
<div className="min-w-0 flex-1">
<div className="truncate font-medium">{poll.title}</div>