mirror of
https://github.com/lukevella/rallly.git
synced 2025-07-09 20:47:26 +02:00
✨ Update control panel layout + add new action bar (#1756)
This commit is contained in:
parent
87b8c76492
commit
1146586e14
16 changed files with 307 additions and 185 deletions
|
@ -394,5 +394,6 @@
|
||||||
"disableUserRegistration": "Disable User Registration",
|
"disableUserRegistration": "Disable User Registration",
|
||||||
"disableUserRegistrationDescription": "Prevent new users from registering an account.",
|
"disableUserRegistrationDescription": "Prevent new users from registering an account.",
|
||||||
"authenticationAndSecurity": "Authentication & Security",
|
"authenticationAndSecurity": "Authentication & Security",
|
||||||
"authenticationAndSecurityDescription": "Manage authentication and security settings"
|
"authenticationAndSecurityDescription": "Manage authentication and security settings",
|
||||||
|
"youHaveUnsavedChanges": "You have unsaved changes"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import { ActionBar } from "@rallly/ui/action-bar";
|
|
||||||
import { Button } from "@rallly/ui/button";
|
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";
|
||||||
|
@ -51,7 +50,6 @@ export default async function Layout({
|
||||||
<div className="flex flex-1 flex-col">
|
<div className="flex flex-1 flex-col">
|
||||||
<div className="flex flex-1 flex-col">{children}</div>
|
<div className="flex flex-1 flex-col">{children}</div>
|
||||||
</div>
|
</div>
|
||||||
<ActionBar />
|
|
||||||
</SidebarInset>
|
</SidebarInset>
|
||||||
</AppSidebarProvider>
|
</AppSidebarProvider>
|
||||||
</TimezoneProvider>
|
</TimezoneProvider>
|
||||||
|
|
|
@ -17,7 +17,9 @@ export default async function AdminLayout({
|
||||||
<ControlPanelSidebar />
|
<ControlPanelSidebar />
|
||||||
<SidebarInset>
|
<SidebarInset>
|
||||||
<LicenseLimitWarning />
|
<LicenseLimitWarning />
|
||||||
<div className="flex min-w-0 flex-1 flex-col">{children}</div>
|
<div className="flex min-w-0 flex-1 flex-col">
|
||||||
|
<div className="flex-1">{children}</div>
|
||||||
|
</div>
|
||||||
</SidebarInset>
|
</SidebarInset>
|
||||||
</ControlPanelSidebarProvider>
|
</ControlPanelSidebarProvider>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,10 +1,4 @@
|
||||||
import { PageIcon } from "@/app/components/page-icons";
|
import { PageIcon } from "@/app/components/page-icons";
|
||||||
import {
|
|
||||||
PageContainer,
|
|
||||||
PageContent,
|
|
||||||
PageHeader,
|
|
||||||
PageTitle,
|
|
||||||
} from "@/app/components/page-layout";
|
|
||||||
import { requireAdmin } from "@/auth/queries";
|
import { requireAdmin } from "@/auth/queries";
|
||||||
import {
|
import {
|
||||||
EmptyState,
|
EmptyState,
|
||||||
|
@ -13,6 +7,12 @@ import {
|
||||||
EmptyStateIcon,
|
EmptyStateIcon,
|
||||||
EmptyStateTitle,
|
EmptyStateTitle,
|
||||||
} from "@/components/empty-state";
|
} from "@/components/empty-state";
|
||||||
|
import {
|
||||||
|
FullWidthLayout,
|
||||||
|
FullWidthLayoutContent,
|
||||||
|
FullWidthLayoutHeader,
|
||||||
|
FullWidthLayoutTitle,
|
||||||
|
} from "@/components/full-width-layout";
|
||||||
import { Trans } from "@/components/trans";
|
import { Trans } from "@/components/trans";
|
||||||
import { LicenseKeyForm } from "@/features/licensing/components/license-key-form";
|
import { LicenseKeyForm } from "@/features/licensing/components/license-key-form";
|
||||||
import { RemoveLicenseButton } from "@/features/licensing/components/remove-license-button";
|
import { RemoveLicenseButton } from "@/features/licensing/components/remove-license-button";
|
||||||
|
@ -59,16 +59,19 @@ function DescriptionListValue({
|
||||||
export default async function LicensePage() {
|
export default async function LicensePage() {
|
||||||
const { license } = await loadData();
|
const { license } = await loadData();
|
||||||
return (
|
return (
|
||||||
<PageContainer>
|
<FullWidthLayout>
|
||||||
<PageHeader>
|
<FullWidthLayoutHeader>
|
||||||
<PageTitle>
|
<FullWidthLayoutTitle
|
||||||
<PageIcon color="darkGray">
|
icon={
|
||||||
|
<PageIcon size="sm" color="darkGray">
|
||||||
<KeySquareIcon />
|
<KeySquareIcon />
|
||||||
</PageIcon>
|
</PageIcon>
|
||||||
|
}
|
||||||
|
>
|
||||||
<Trans i18nKey="license" defaults="License" />
|
<Trans i18nKey="license" defaults="License" />
|
||||||
</PageTitle>
|
</FullWidthLayoutTitle>
|
||||||
</PageHeader>
|
</FullWidthLayoutHeader>
|
||||||
<PageContent>
|
<FullWidthLayoutContent>
|
||||||
{license ? (
|
{license ? (
|
||||||
<div>
|
<div>
|
||||||
<DescriptionList>
|
<DescriptionList>
|
||||||
|
@ -174,8 +177,8 @@ export default async function LicensePage() {
|
||||||
</EmptyStateFooter>
|
</EmptyStateFooter>
|
||||||
</EmptyState>
|
</EmptyState>
|
||||||
)}
|
)}
|
||||||
</PageContent>
|
</FullWidthLayoutContent>
|
||||||
</PageContainer>
|
</FullWidthLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import { PageIcon } from "@/app/components/page-icons";
|
import { PageIcon } from "@/app/components/page-icons";
|
||||||
import {
|
|
||||||
PageContainer,
|
|
||||||
PageContent,
|
|
||||||
PageHeader,
|
|
||||||
PageTitle,
|
|
||||||
} from "@/app/components/page-layout";
|
|
||||||
import { requireAdmin } from "@/auth/queries";
|
import { requireAdmin } from "@/auth/queries";
|
||||||
|
import {
|
||||||
|
FullWidthLayout,
|
||||||
|
FullWidthLayoutContent,
|
||||||
|
FullWidthLayoutHeader,
|
||||||
|
FullWidthLayoutTitle,
|
||||||
|
} from "@/components/full-width-layout";
|
||||||
import { Trans } from "@/components/trans";
|
import { Trans } from "@/components/trans";
|
||||||
import { getLicense } from "@/features/licensing/queries";
|
import { getLicense } from "@/features/licensing/queries";
|
||||||
import { prisma } from "@rallly/database";
|
import { prisma } from "@rallly/database";
|
||||||
|
@ -37,16 +37,19 @@ async function loadData() {
|
||||||
export default async function AdminPage() {
|
export default async function AdminPage() {
|
||||||
const { userCount, userLimit, tier } = await loadData();
|
const { userCount, userLimit, tier } = await loadData();
|
||||||
return (
|
return (
|
||||||
<PageContainer>
|
<FullWidthLayout>
|
||||||
<PageHeader>
|
<FullWidthLayoutHeader>
|
||||||
<PageTitle>
|
<FullWidthLayoutTitle
|
||||||
<PageIcon color="indigo">
|
icon={
|
||||||
|
<PageIcon size="sm" color="indigo">
|
||||||
<GaugeIcon />
|
<GaugeIcon />
|
||||||
</PageIcon>
|
</PageIcon>
|
||||||
|
}
|
||||||
|
>
|
||||||
<Trans i18nKey="controlPanel" defaults="Control Panel" />
|
<Trans i18nKey="controlPanel" defaults="Control Panel" />
|
||||||
</PageTitle>
|
</FullWidthLayoutTitle>
|
||||||
</PageHeader>
|
</FullWidthLayoutHeader>
|
||||||
<PageContent className="space-y-8">
|
<FullWidthLayoutContent>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<h2 className="text-muted-foreground text-sm">
|
<h2 className="text-muted-foreground text-sm">
|
||||||
<Trans i18nKey="homeNavTitle" defaults="Navigation" />
|
<Trans i18nKey="homeNavTitle" defaults="Navigation" />
|
||||||
|
@ -122,8 +125,8 @@ export default async function AdminPage() {
|
||||||
</Tile>
|
</Tile>
|
||||||
</TileGrid>
|
</TileGrid>
|
||||||
</div>
|
</div>
|
||||||
</PageContent>
|
</FullWidthLayoutContent>
|
||||||
</PageContainer>
|
</FullWidthLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,12 @@
|
||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
import { requireAdmin } from "@/auth/queries";
|
import { requireAdmin } from "@/auth/queries";
|
||||||
|
import type { InstanceSettings } from "@/features/instance-settings/schema";
|
||||||
import { prisma } from "@rallly/database";
|
import { prisma } from "@rallly/database";
|
||||||
|
|
||||||
export async function setDisableUserRegistration({
|
export async function updateInstanceSettings({
|
||||||
disableUserRegistration,
|
disableUserRegistration,
|
||||||
}: {
|
}: InstanceSettings) {
|
||||||
disableUserRegistration: boolean;
|
|
||||||
}) {
|
|
||||||
await requireAdmin();
|
await requireAdmin();
|
||||||
|
|
||||||
await prisma.instanceSettings.update({
|
await prisma.instanceSettings.update({
|
||||||
|
|
|
@ -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 (
|
|
||||||
<div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Switch
|
|
||||||
id="disable-user-registration"
|
|
||||||
onCheckedChange={(checked) => {
|
|
||||||
setDisableUserRegistration({ disableUserRegistration: checked });
|
|
||||||
}}
|
|
||||||
defaultChecked={defaultValue}
|
|
||||||
/>
|
|
||||||
<Label htmlFor="disable-user-registration">
|
|
||||||
<Trans
|
|
||||||
i18nKey="disableUserRegistration"
|
|
||||||
defaults="Disable User Registration"
|
|
||||||
/>
|
|
||||||
</Label>
|
|
||||||
</div>
|
|
||||||
<p className="mt-2 text-muted-foreground text-sm">
|
|
||||||
<Trans
|
|
||||||
i18nKey="disableUserRegistrationDescription"
|
|
||||||
defaults="Prevent new users from registering an account."
|
|
||||||
/>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -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<InstanceSettings>({
|
||||||
|
defaultValues: defaultValue,
|
||||||
|
resolver: zodResolver(instanceSettingsSchema),
|
||||||
|
});
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const { toast } = useToast();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form {...form}>
|
||||||
|
<form
|
||||||
|
name="instance-settings-form"
|
||||||
|
onSubmit={form.handleSubmit(async (data) => {
|
||||||
|
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.",
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<SettingsGroup>
|
||||||
|
<SettingsGroupHeader>
|
||||||
|
<SettingsGroupTitle>
|
||||||
|
<Trans
|
||||||
|
i18nKey="authenticationAndSecurity"
|
||||||
|
defaults="Authentication & Security"
|
||||||
|
/>
|
||||||
|
</SettingsGroupTitle>
|
||||||
|
<SettingsGroupDescription>
|
||||||
|
<Trans
|
||||||
|
i18nKey="authenticationAndSecurityDescription"
|
||||||
|
defaults="Manage authentication and security settings"
|
||||||
|
/>
|
||||||
|
</SettingsGroupDescription>
|
||||||
|
</SettingsGroupHeader>
|
||||||
|
<SettingsGroupContent>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="disableUserRegistration"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<FormControl>
|
||||||
|
<Switch
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
checked={field.value}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormLabel>
|
||||||
|
<Trans
|
||||||
|
i18nKey="disableUserRegistration"
|
||||||
|
defaults="Disable User Registration"
|
||||||
|
/>
|
||||||
|
</FormLabel>
|
||||||
|
</div>
|
||||||
|
<FormDescription>
|
||||||
|
<Trans
|
||||||
|
i18nKey="disableUserRegistrationDescription"
|
||||||
|
defaults="Prevent new users from registering an account."
|
||||||
|
/>
|
||||||
|
</FormDescription>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</SettingsGroupContent>
|
||||||
|
</SettingsGroup>
|
||||||
|
<ActionBar open={form.formState.isDirty}>
|
||||||
|
<ActionBarTitle>
|
||||||
|
<Trans
|
||||||
|
i18nKey="youHaveUnsavedChanges"
|
||||||
|
defaults="You have unsaved changes"
|
||||||
|
/>
|
||||||
|
</ActionBarTitle>
|
||||||
|
<ActionBarGroup>
|
||||||
|
<Button
|
||||||
|
variant="actionBar"
|
||||||
|
type="button"
|
||||||
|
onClick={() => form.reset()}
|
||||||
|
>
|
||||||
|
<Trans i18nKey="cancel" defaults="Cancel" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
loading={form.formState.isSubmitting}
|
||||||
|
variant="primary"
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
<Trans i18nKey="save" defaults="Save" />
|
||||||
|
</Button>
|
||||||
|
</ActionBarGroup>
|
||||||
|
</ActionBar>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
}
|
|
@ -8,7 +8,7 @@ import {
|
||||||
import { Trans } from "@/components/trans";
|
import { Trans } from "@/components/trans";
|
||||||
import { getInstanceSettings } from "@/features/instance-settings/queries";
|
import { getInstanceSettings } from "@/features/instance-settings/queries";
|
||||||
import { SettingsIcon } from "lucide-react";
|
import { SettingsIcon } from "lucide-react";
|
||||||
import { DisableUserRegistration } from "./disable-user-registration";
|
import { InstanceSettingsForm } from "./instance-settings-form";
|
||||||
|
|
||||||
async function loadData() {
|
async function loadData() {
|
||||||
const instanceSettings = await getInstanceSettings();
|
const instanceSettings = await getInstanceSettings();
|
||||||
|
@ -35,27 +35,7 @@ export default async function SettingsPage() {
|
||||||
</FullWidthLayoutTitle>
|
</FullWidthLayoutTitle>
|
||||||
</FullWidthLayoutHeader>
|
</FullWidthLayoutHeader>
|
||||||
<FullWidthLayoutContent>
|
<FullWidthLayoutContent>
|
||||||
<div className="flex flex-col gap-6 rounded-lg border p-6 lg:flex-row">
|
<InstanceSettingsForm defaultValue={instanceSettings} />
|
||||||
<div className="lg:w-1/2">
|
|
||||||
<h2 className="font-semibold text-base">
|
|
||||||
<Trans
|
|
||||||
i18nKey="authenticationAndSecurity"
|
|
||||||
defaults="Authentication & Security"
|
|
||||||
/>
|
|
||||||
</h2>
|
|
||||||
<p className="mt-1 text-muted-foreground text-sm">
|
|
||||||
<Trans
|
|
||||||
i18nKey="authenticationAndSecurityDescription"
|
|
||||||
defaults="Manage authentication and security settings"
|
|
||||||
/>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="flex-1">
|
|
||||||
<DisableUserRegistration
|
|
||||||
defaultValue={instanceSettings?.disableUserRegistration}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</FullWidthLayoutContent>
|
</FullWidthLayoutContent>
|
||||||
</FullWidthLayout>
|
</FullWidthLayout>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,10 +1,4 @@
|
||||||
import { PageIcon } from "@/app/components/page-icons";
|
import { PageIcon } from "@/app/components/page-icons";
|
||||||
import {
|
|
||||||
PageContainer,
|
|
||||||
PageContent,
|
|
||||||
PageHeader,
|
|
||||||
PageTitle,
|
|
||||||
} from "@/app/components/page-layout";
|
|
||||||
import { requireAdmin } from "@/auth/queries";
|
import { requireAdmin } from "@/auth/queries";
|
||||||
import {
|
import {
|
||||||
EmptyState,
|
EmptyState,
|
||||||
|
@ -12,6 +6,12 @@ import {
|
||||||
EmptyStateIcon,
|
EmptyStateIcon,
|
||||||
EmptyStateTitle,
|
EmptyStateTitle,
|
||||||
} from "@/components/empty-state";
|
} from "@/components/empty-state";
|
||||||
|
import {
|
||||||
|
FullWidthLayout,
|
||||||
|
FullWidthLayoutContent,
|
||||||
|
FullWidthLayoutHeader,
|
||||||
|
FullWidthLayoutTitle,
|
||||||
|
} from "@/components/full-width-layout";
|
||||||
import { Pagination } from "@/components/pagination";
|
import { Pagination } from "@/components/pagination";
|
||||||
import { StackedList } from "@/components/stacked-list";
|
import { StackedList } from "@/components/stacked-list";
|
||||||
import { Trans } from "@/components/trans";
|
import { Trans } from "@/components/trans";
|
||||||
|
@ -113,18 +113,19 @@ export default async function AdminPage(props: {
|
||||||
const totalItems = allUsers.length;
|
const totalItems = allUsers.length;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContainer>
|
<FullWidthLayout>
|
||||||
<PageHeader>
|
<FullWidthLayoutHeader>
|
||||||
<div className="flex items-center gap-4">
|
<FullWidthLayoutTitle
|
||||||
<PageTitle>
|
icon={
|
||||||
<PageIcon color="darkGray">
|
<PageIcon size="sm" color="darkGray">
|
||||||
<UsersIcon />
|
<UsersIcon />
|
||||||
</PageIcon>
|
</PageIcon>
|
||||||
|
}
|
||||||
|
>
|
||||||
<Trans i18nKey="users" defaults="Users" />
|
<Trans i18nKey="users" defaults="Users" />
|
||||||
</PageTitle>
|
</FullWidthLayoutTitle>
|
||||||
</div>
|
</FullWidthLayoutHeader>
|
||||||
</PageHeader>
|
<FullWidthLayoutContent>
|
||||||
<PageContent>
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<UserSearchInput />
|
<UserSearchInput />
|
||||||
<UsersTabbedView>
|
<UsersTabbedView>
|
||||||
|
@ -167,8 +168,8 @@ export default async function AdminPage(props: {
|
||||||
)}
|
)}
|
||||||
</UsersTabbedView>
|
</UsersTabbedView>
|
||||||
</div>
|
</div>
|
||||||
</PageContent>
|
</FullWidthLayoutContent>
|
||||||
</PageContainer>
|
</FullWidthLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { SidebarTrigger } from "@rallly/ui/sidebar";
|
||||||
|
|
||||||
export function FullWidthLayout({ children }: { children: React.ReactNode }) {
|
export function FullWidthLayout({ children }: { children: React.ReactNode }) {
|
||||||
return <div>{children}</div>;
|
return <div>{children}</div>;
|
||||||
}
|
}
|
||||||
|
@ -6,8 +8,13 @@ export function FullWidthLayoutHeader({
|
||||||
children,
|
children,
|
||||||
}: { children: React.ReactNode }) {
|
}: { children: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<header className="sticky top-0 z-10 rounded-t-lg border-b bg-background/90 px-6 py-4 backdrop-blur-sm">
|
<header className="sticky top-0 z-10 rounded-t-lg border-b bg-background/90 px-3 py-4 backdrop-blur-sm md:px-6">
|
||||||
{children}
|
<div className="flex items-center gap-4">
|
||||||
|
<div className="md:hidden">
|
||||||
|
<SidebarTrigger />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1">{children}</div>
|
||||||
|
</div>
|
||||||
</header>
|
</header>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -15,7 +22,7 @@ export function FullWidthLayoutHeader({
|
||||||
export function FullWidthLayoutContent({
|
export function FullWidthLayoutContent({
|
||||||
children,
|
children,
|
||||||
}: { children: React.ReactNode }) {
|
}: { children: React.ReactNode }) {
|
||||||
return <main className="p-6">{children}</main>;
|
return <main className="p-3 pb-44 md:px-6 md:pt-6">{children}</main>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function FullWidthLayoutTitle({
|
export function FullWidthLayoutTitle({
|
||||||
|
@ -23,9 +30,9 @@ export function FullWidthLayoutTitle({
|
||||||
icon,
|
icon,
|
||||||
}: { children: React.ReactNode; icon?: React.ReactNode }) {
|
}: { children: React.ReactNode; icon?: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2.5">
|
||||||
{icon}
|
{icon}
|
||||||
<h1 className="font-semibold text-xl">{children}</h1>
|
<h1 className="font-bold text-xl">{children}</h1>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
31
apps/web/src/components/settings-group.tsx
Normal file
31
apps/web/src/components/settings-group.tsx
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
export function SettingsGroup({ children }: { children: React.ReactNode }) {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-6 rounded-lg border p-6 lg:flex-row">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SettingsGroupHeader({
|
||||||
|
children,
|
||||||
|
}: { children: React.ReactNode }) {
|
||||||
|
return <div className="lg:w-1/3">{children}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SettingsGroupTitle({
|
||||||
|
children,
|
||||||
|
}: { children: React.ReactNode }) {
|
||||||
|
return <h2 className="font-semibold text-base">{children}</h2>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SettingsGroupDescription({
|
||||||
|
children,
|
||||||
|
}: { children: React.ReactNode }) {
|
||||||
|
return <p className="mt-1 text-muted-foreground text-sm">{children}</p>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SettingsGroupContent({
|
||||||
|
children,
|
||||||
|
}: { children: React.ReactNode }) {
|
||||||
|
return <div className="flex-1">{children}</div>;
|
||||||
|
}
|
7
apps/web/src/features/instance-settings/schema.ts
Normal file
7
apps/web/src/features/instance-settings/schema.ts
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const instanceSettingsSchema = z.object({
|
||||||
|
disableUserRegistration: z.boolean(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type InstanceSettings = z.infer<typeof instanceSettingsSchema>;
|
|
@ -51,7 +51,7 @@ module.exports = {
|
||||||
},
|
},
|
||||||
"action-bar": {
|
"action-bar": {
|
||||||
DEFAULT: colors.gray["800"],
|
DEFAULT: colors.gray["800"],
|
||||||
foreground: colors.white,
|
foreground: colors.gray["50"],
|
||||||
},
|
},
|
||||||
muted: {
|
muted: {
|
||||||
DEFAULT: colors.gray["100"],
|
DEFAULT: colors.gray["100"],
|
||||||
|
|
|
@ -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 * as React from "react";
|
||||||
|
|
||||||
import { cn } from "./lib/utils";
|
import { cn } from "./lib/utils";
|
||||||
|
|
||||||
const ACTION_BAR_PORTAL_ID = "action-bar-portal";
|
interface ActionBarProps
|
||||||
|
extends Omit<
|
||||||
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>,
|
||||||
|
"open" | "onOpenChange"
|
||||||
|
> {
|
||||||
|
open?: boolean;
|
||||||
|
onOpenChange?: (open: boolean) => void;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
const ActionBar = React.forwardRef<
|
const ActionBar = React.forwardRef<
|
||||||
HTMLDivElement,
|
React.ComponentRef<typeof DialogPrimitive.Content>,
|
||||||
React.HTMLAttributes<HTMLDivElement>
|
ActionBarProps
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ open, onOpenChange, children, className, ...props }, ref) => {
|
||||||
<div
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"pointer-events-none sticky bottom-8 flex justify-center pb-5",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
id={ACTION_BAR_PORTAL_ID}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
));
|
|
||||||
ActionBar.displayName = "ActionBar";
|
|
||||||
|
|
||||||
const ActionBarPortal = React.forwardRef<
|
|
||||||
HTMLDivElement,
|
|
||||||
React.HTMLAttributes<HTMLDivElement>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<Portal.Root
|
|
||||||
container={document.getElementById(ACTION_BAR_PORTAL_ID)}
|
|
||||||
ref={ref}
|
|
||||||
className={className}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
));
|
|
||||||
ActionBarPortal.displayName = "ActionBarPortal";
|
|
||||||
|
|
||||||
const ActionBarContainer = React.forwardRef<
|
|
||||||
HTMLDivElement,
|
|
||||||
React.HTMLAttributes<HTMLDivElement>
|
|
||||||
>(({ className, ...props }, ref) => {
|
|
||||||
return (
|
return (
|
||||||
<div
|
<DialogPrimitive.Root modal={false} open={open} onOpenChange={onOpenChange}>
|
||||||
|
<DialogPrimitive.Content
|
||||||
|
forceMount={true}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"pointer-events-auto z-50 mx-auto inline-flex w-full max-w-2xl items-center gap-4 rounded-xl bg-action-bar p-2 text-action-bar-foreground shadow-lg",
|
"-translate-x-1/2 fixed bottom-3 z-50 flex items-start gap-2 rounded-xl bg-action-bar p-2 text-action-bar-foreground shadow-lg transition-transform duration-200 ease-out data-[state=closed]:pointer-events-none data-[state=closed]:translate-y-full data-[state=open]:translate-y-0 data-[state=closed]:opacity-0 data-[state=open]:opacity-100",
|
||||||
|
"left-1/2 md:bottom-16 md:w-fit",
|
||||||
|
"w-[calc(100%-24px)]",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
>
|
||||||
|
{children}
|
||||||
|
</DialogPrimitive.Content>
|
||||||
|
</DialogPrimitive.Root>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
ActionBarContainer.displayName = "ActionBarContainer";
|
ActionBar.displayName = "ActionBar";
|
||||||
|
|
||||||
const ActionBarContent = React.forwardRef<
|
const ActionBarTitle = React.forwardRef<
|
||||||
HTMLDivElement,
|
HTMLHeadingElement,
|
||||||
React.HTMLAttributes<HTMLDivElement>
|
React.HTMLAttributes<HTMLHeadingElement>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, children, ...props }, ref) => (
|
||||||
<div
|
<DialogPrimitive.Title
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn("flex items-center px-2.5", className)}
|
className={cn("flex flex-1 items-center px-2.5 py-2 text-sm", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
>
|
||||||
|
{children}
|
||||||
|
</DialogPrimitive.Title>
|
||||||
));
|
));
|
||||||
ActionBarContent.displayName = "ActionBarContent";
|
ActionBarTitle.displayName = "ActionBarTitle";
|
||||||
|
|
||||||
const ActionBarGroup = React.forwardRef<
|
const ActionBarGroup = React.forwardRef<
|
||||||
HTMLDivElement,
|
HTMLDivElement,
|
||||||
|
@ -69,16 +56,10 @@ const ActionBarGroup = React.forwardRef<
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn("flex items-center gap-2", className)}
|
className={cn("flex justify-end gap-2", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
));
|
));
|
||||||
ActionBarGroup.displayName = "ActionBarGroup";
|
ActionBarGroup.displayName = "ActionBarGroup";
|
||||||
|
|
||||||
export {
|
export { ActionBar, ActionBarTitle, ActionBarGroup };
|
||||||
ActionBar,
|
|
||||||
ActionBarContainer,
|
|
||||||
ActionBarContent,
|
|
||||||
ActionBarGroup,
|
|
||||||
ActionBarPortal,
|
|
||||||
};
|
|
||||||
|
|
|
@ -25,7 +25,7 @@ const buttonVariants = cva(
|
||||||
ghost:
|
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",
|
"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:
|
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",
|
link: "border-transparent text-primary underline-offset-4 hover:underline",
|
||||||
},
|
},
|
||||||
size: {
|
size: {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue