diff --git a/apps/web/public/locales/en/app.json b/apps/web/public/locales/en/app.json index 7c765e1ae..13a8afee6 100644 --- a/apps/web/public/locales/en/app.json +++ b/apps/web/public/locales/en/app.json @@ -394,5 +394,6 @@ "disableUserRegistration": "Disable User Registration", "disableUserRegistrationDescription": "Prevent new users from registering an account.", "authenticationAndSecurity": "Authentication & Security", - "authenticationAndSecurityDescription": "Manage authentication and security settings" + "authenticationAndSecurityDescription": "Manage authentication and security settings", + "youHaveUnsavedChanges": "You have unsaved changes" } diff --git a/apps/web/src/app/[locale]/(space)/layout.tsx b/apps/web/src/app/[locale]/(space)/layout.tsx index 829155351..170b203f8 100644 --- a/apps/web/src/app/[locale]/(space)/layout.tsx +++ b/apps/web/src/app/[locale]/(space)/layout.tsx @@ -1,4 +1,3 @@ -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"; @@ -51,7 +50,6 @@ export default async function Layout({
{children}
- diff --git a/apps/web/src/app/[locale]/control-panel/layout.tsx b/apps/web/src/app/[locale]/control-panel/layout.tsx index 1d6510acb..29d347306 100644 --- a/apps/web/src/app/[locale]/control-panel/layout.tsx +++ b/apps/web/src/app/[locale]/control-panel/layout.tsx @@ -17,7 +17,9 @@ export default async function AdminLayout({ -
{children}
+
+
{children}
+
); diff --git a/apps/web/src/app/[locale]/control-panel/license/page.tsx b/apps/web/src/app/[locale]/control-panel/license/page.tsx index 0ca98b87c..a555a97a2 100644 --- a/apps/web/src/app/[locale]/control-panel/license/page.tsx +++ b/apps/web/src/app/[locale]/control-panel/license/page.tsx @@ -1,10 +1,4 @@ import { PageIcon } from "@/app/components/page-icons"; -import { - PageContainer, - PageContent, - PageHeader, - PageTitle, -} from "@/app/components/page-layout"; import { requireAdmin } from "@/auth/queries"; import { EmptyState, @@ -13,6 +7,12 @@ import { EmptyStateIcon, EmptyStateTitle, } from "@/components/empty-state"; +import { + FullWidthLayout, + FullWidthLayoutContent, + FullWidthLayoutHeader, + FullWidthLayoutTitle, +} from "@/components/full-width-layout"; import { Trans } from "@/components/trans"; import { LicenseKeyForm } from "@/features/licensing/components/license-key-form"; import { RemoveLicenseButton } from "@/features/licensing/components/remove-license-button"; @@ -59,16 +59,19 @@ function DescriptionListValue({ export default async function LicensePage() { const { license } = await loadData(); return ( - - - - - - + + + + + + } + > - - - + + + {license ? (
@@ -174,8 +177,8 @@ export default async function LicensePage() { )} - - + + ); } diff --git a/apps/web/src/app/[locale]/control-panel/page.tsx b/apps/web/src/app/[locale]/control-panel/page.tsx index e13bfc7f6..19542dd04 100644 --- a/apps/web/src/app/[locale]/control-panel/page.tsx +++ b/apps/web/src/app/[locale]/control-panel/page.tsx @@ -1,11 +1,11 @@ import { PageIcon } from "@/app/components/page-icons"; -import { - PageContainer, - PageContent, - PageHeader, - PageTitle, -} from "@/app/components/page-layout"; import { requireAdmin } from "@/auth/queries"; +import { + FullWidthLayout, + FullWidthLayoutContent, + FullWidthLayoutHeader, + FullWidthLayoutTitle, +} from "@/components/full-width-layout"; import { Trans } from "@/components/trans"; import { getLicense } from "@/features/licensing/queries"; import { prisma } from "@rallly/database"; @@ -37,16 +37,19 @@ async function loadData() { export default async function AdminPage() { const { userCount, userLimit, tier } = await loadData(); return ( - - - - - - + + + + + + } + > - - - + + +

@@ -122,8 +125,8 @@ export default async function AdminPage() {

-
-
+ + ); } diff --git a/apps/web/src/app/[locale]/control-panel/settings/actions.ts b/apps/web/src/app/[locale]/control-panel/settings/actions.ts index 3d7310871..a39e0ada5 100644 --- a/apps/web/src/app/[locale]/control-panel/settings/actions.ts +++ b/apps/web/src/app/[locale]/control-panel/settings/actions.ts @@ -1,13 +1,12 @@ "use server"; import { requireAdmin } from "@/auth/queries"; +import type { InstanceSettings } from "@/features/instance-settings/schema"; import { prisma } from "@rallly/database"; -export async function setDisableUserRegistration({ +export async function updateInstanceSettings({ disableUserRegistration, -}: { - disableUserRegistration: boolean; -}) { +}: InstanceSettings) { await requireAdmin(); await prisma.instanceSettings.update({ diff --git a/apps/web/src/app/[locale]/control-panel/settings/disable-user-registration.tsx b/apps/web/src/app/[locale]/control-panel/settings/disable-user-registration.tsx deleted file mode 100644 index f8bf68532..000000000 --- a/apps/web/src/app/[locale]/control-panel/settings/disable-user-registration.tsx +++ /dev/null @@ -1,36 +0,0 @@ -"use client"; - -import { Trans } from "@/components/trans"; -import { Label } from "@rallly/ui/label"; -import { Switch } from "@rallly/ui/switch"; -import { setDisableUserRegistration } from "./actions"; - -export function DisableUserRegistration({ - defaultValue, -}: { defaultValue: boolean }) { - return ( -
-
- { - setDisableUserRegistration({ disableUserRegistration: checked }); - }} - defaultChecked={defaultValue} - /> - -
-

- -

-
- ); -} diff --git a/apps/web/src/app/[locale]/control-panel/settings/instance-settings-form.tsx b/apps/web/src/app/[locale]/control-panel/settings/instance-settings-form.tsx new file mode 100644 index 000000000..e447924fe --- /dev/null +++ b/apps/web/src/app/[locale]/control-panel/settings/instance-settings-form.tsx @@ -0,0 +1,145 @@ +"use client"; + +import { + SettingsGroup, + SettingsGroupContent, + SettingsGroupDescription, + SettingsGroupHeader, + SettingsGroupTitle, +} from "@/components/settings-group"; +import { Trans } from "@/components/trans"; +import { + type InstanceSettings, + instanceSettingsSchema, +} from "@/features/instance-settings/schema"; +import { useTranslation } from "@/i18n/client"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { + ActionBar, + ActionBarGroup, + ActionBarTitle, +} from "@rallly/ui/action-bar"; +import { Button } from "@rallly/ui/button"; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, +} from "@rallly/ui/form"; +import { useToast } from "@rallly/ui/hooks/use-toast"; +import { Switch } from "@rallly/ui/switch"; +import { useForm } from "react-hook-form"; +import { updateInstanceSettings } from "./actions"; + +export function InstanceSettingsForm({ + defaultValue, +}: { + defaultValue: InstanceSettings; +}) { + const form = useForm({ + defaultValues: defaultValue, + resolver: zodResolver(instanceSettingsSchema), + }); + + const { t } = useTranslation(); + + const { toast } = useToast(); + + return ( +
+ { + try { + await updateInstanceSettings(data); + form.reset(data); + } catch (error) { + console.error(error); + toast({ + title: t("unexpectedError", { + defaultValue: "Unexpected Error", + }), + description: t("unexpectedErrorDescription", { + defaultValue: + "There was an unexpected error. Please try again later.", + }), + }); + } + })} + > + + + + + + + + + + + ( + +
+ + + + + + +
+ + + +
+ )} + /> +
+
+ + + + + + + + + +
+ + ); +} diff --git a/apps/web/src/app/[locale]/control-panel/settings/page.tsx b/apps/web/src/app/[locale]/control-panel/settings/page.tsx index cd8a7ae2f..1f4e1e286 100644 --- a/apps/web/src/app/[locale]/control-panel/settings/page.tsx +++ b/apps/web/src/app/[locale]/control-panel/settings/page.tsx @@ -8,7 +8,7 @@ import { import { Trans } from "@/components/trans"; import { getInstanceSettings } from "@/features/instance-settings/queries"; import { SettingsIcon } from "lucide-react"; -import { DisableUserRegistration } from "./disable-user-registration"; +import { InstanceSettingsForm } from "./instance-settings-form"; async function loadData() { const instanceSettings = await getInstanceSettings(); @@ -35,27 +35,7 @@ export default async function SettingsPage() { -
-
-

- -

-

- -

-
-
- -
-
+
); diff --git a/apps/web/src/app/[locale]/control-panel/users/page.tsx b/apps/web/src/app/[locale]/control-panel/users/page.tsx index f2d4d281d..d1485fe8b 100644 --- a/apps/web/src/app/[locale]/control-panel/users/page.tsx +++ b/apps/web/src/app/[locale]/control-panel/users/page.tsx @@ -1,10 +1,4 @@ import { PageIcon } from "@/app/components/page-icons"; -import { - PageContainer, - PageContent, - PageHeader, - PageTitle, -} from "@/app/components/page-layout"; import { requireAdmin } from "@/auth/queries"; import { EmptyState, @@ -12,6 +6,12 @@ import { EmptyStateIcon, EmptyStateTitle, } from "@/components/empty-state"; +import { + FullWidthLayout, + FullWidthLayoutContent, + FullWidthLayoutHeader, + FullWidthLayoutTitle, +} from "@/components/full-width-layout"; import { Pagination } from "@/components/pagination"; import { StackedList } from "@/components/stacked-list"; import { Trans } from "@/components/trans"; @@ -113,18 +113,19 @@ export default async function AdminPage(props: { const totalItems = allUsers.length; return ( - - -
- - + + + - - -
-
- + } + > + + + +
@@ -167,8 +168,8 @@ export default async function AdminPage(props: { )}
-
-
+ + ); } diff --git a/apps/web/src/components/full-width-layout.tsx b/apps/web/src/components/full-width-layout.tsx index e74ffcff1..65da6248d 100644 --- a/apps/web/src/components/full-width-layout.tsx +++ b/apps/web/src/components/full-width-layout.tsx @@ -1,3 +1,5 @@ +import { SidebarTrigger } from "@rallly/ui/sidebar"; + export function FullWidthLayout({ children }: { children: React.ReactNode }) { return
{children}
; } @@ -6,8 +8,13 @@ export function FullWidthLayoutHeader({ children, }: { children: React.ReactNode }) { return ( -
- {children} +
+
+
+ +
+
{children}
+
); } @@ -15,7 +22,7 @@ export function FullWidthLayoutHeader({ export function FullWidthLayoutContent({ children, }: { children: React.ReactNode }) { - return
{children}
; + return
{children}
; } export function FullWidthLayoutTitle({ @@ -23,9 +30,9 @@ export function FullWidthLayoutTitle({ icon, }: { children: React.ReactNode; icon?: React.ReactNode }) { return ( -
+
{icon} -

{children}

+

{children}

); } diff --git a/apps/web/src/components/settings-group.tsx b/apps/web/src/components/settings-group.tsx new file mode 100644 index 000000000..303f2fc2c --- /dev/null +++ b/apps/web/src/components/settings-group.tsx @@ -0,0 +1,31 @@ +export function SettingsGroup({ children }: { children: React.ReactNode }) { + return ( +
+ {children} +
+ ); +} + +export function SettingsGroupHeader({ + children, +}: { children: React.ReactNode }) { + return
{children}
; +} + +export function SettingsGroupTitle({ + children, +}: { children: React.ReactNode }) { + return

{children}

; +} + +export function SettingsGroupDescription({ + children, +}: { children: React.ReactNode }) { + return

{children}

; +} + +export function SettingsGroupContent({ + children, +}: { children: React.ReactNode }) { + return
{children}
; +} diff --git a/apps/web/src/features/instance-settings/schema.ts b/apps/web/src/features/instance-settings/schema.ts new file mode 100644 index 000000000..5ad7891d3 --- /dev/null +++ b/apps/web/src/features/instance-settings/schema.ts @@ -0,0 +1,7 @@ +import { z } from "zod"; + +export const instanceSettingsSchema = z.object({ + disableUserRegistration: z.boolean(), +}); + +export type InstanceSettings = z.infer; diff --git a/packages/tailwind-config/tailwind.config.js b/packages/tailwind-config/tailwind.config.js index 02dfaf7a1..7aaade882 100644 --- a/packages/tailwind-config/tailwind.config.js +++ b/packages/tailwind-config/tailwind.config.js @@ -51,7 +51,7 @@ module.exports = { }, "action-bar": { DEFAULT: colors.gray["800"], - foreground: colors.white, + foreground: colors.gray["50"], }, muted: { DEFAULT: colors.gray["100"], diff --git a/packages/ui/src/action-bar.tsx b/packages/ui/src/action-bar.tsx index ff588a7bf..f868cbdb8 100644 --- a/packages/ui/src/action-bar.tsx +++ b/packages/ui/src/action-bar.tsx @@ -1,67 +1,54 @@ -import * as Portal from "@radix-ui/react-portal"; +import * as DialogPrimitive from "@radix-ui/react-dialog"; import * as React from "react"; - import { cn } from "./lib/utils"; -const ACTION_BAR_PORTAL_ID = "action-bar-portal"; +interface ActionBarProps + extends Omit< + React.ComponentPropsWithoutRef, + "open" | "onOpenChange" + > { + open?: boolean; + onOpenChange?: (open: boolean) => void; + children: React.ReactNode; +} const ActionBar = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -
-)); -ActionBar.displayName = "ActionBar"; - -const ActionBarPortal = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( - -)); -ActionBarPortal.displayName = "ActionBarPortal"; - -const ActionBarContainer = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => { + React.ComponentRef, + ActionBarProps +>(({ open, onOpenChange, children, className, ...props }, ref) => { return ( -
+ + + {children} + + ); }); -ActionBarContainer.displayName = "ActionBarContainer"; +ActionBar.displayName = "ActionBar"; -const ActionBarContent = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -
+>(({ className, children, ...props }, ref) => ( + + > + {children} + )); -ActionBarContent.displayName = "ActionBarContent"; +ActionBarTitle.displayName = "ActionBarTitle"; const ActionBarGroup = React.forwardRef< HTMLDivElement, @@ -69,16 +56,10 @@ const ActionBarGroup = React.forwardRef< >(({ className, ...props }, ref) => (
)); ActionBarGroup.displayName = "ActionBarGroup"; -export { - ActionBar, - ActionBarContainer, - ActionBarContent, - ActionBarGroup, - ActionBarPortal, -}; +export { ActionBar, ActionBarTitle, ActionBarGroup }; diff --git a/packages/ui/src/button.tsx b/packages/ui/src/button.tsx index ad7bcd4f3..584b4a431 100644 --- a/packages/ui/src/button.tsx +++ b/packages/ui/src/button.tsx @@ -25,7 +25,7 @@ const buttonVariants = cva( ghost: "border-transparent bg-transparent text-gray-800 hover:bg-gray-500/10 active:bg-gray-500/20 data-[state=open]:bg-gray-500/20", actionBar: - "border-transparent bg-transparent text-gray-800 hover:bg-gray-700 active:bg-gray-700/50 data-[state=open]:bg-gray-500/20", + "border-transparent bg-action-bar text-action-bar-foreground hover:bg-action-bar-foreground/10 data-[state=open]:bg-action-bar-foreground/20", link: "border-transparent text-primary underline-offset-4 hover:underline", }, size: {