diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 60b385403..3471f4f88 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -10,7 +10,13 @@ "ghcr.io/devcontainers/features/node:1": {} }, "onCreateCommand": "./.devcontainer/on-create.sh", - "forwardPorts": [3000, 54320, 9000, 2500, 1100], + "forwardPorts": [ + 3000, + 54320, + 9000, + 2500, + 1100 + ], "customizations": { "vscode": { "extensions": [ @@ -25,8 +31,8 @@ "GitHub.copilot", "GitHub.vscode-pull-request-github", "Prisma.prisma", - "VisualStudioExptTeam.vscodeintellicode", + "VisualStudioExptTeam.vscodeintellicode" ] } } -} +} \ No newline at end of file diff --git a/apps/marketing/src/app/(marketing)/[content]/page.tsx b/apps/marketing/src/app/(marketing)/[content]/page.tsx index ba23e6b81..72941fbc5 100644 --- a/apps/marketing/src/app/(marketing)/[content]/page.tsx +++ b/apps/marketing/src/app/(marketing)/[content]/page.tsx @@ -5,11 +5,10 @@ import { allDocuments } from 'contentlayer/generated'; import type { MDXComponents } from 'mdx/types'; import { useMDXComponent } from 'next-contentlayer/hooks'; -export const generateStaticParams = () => - allDocuments.map((post) => ({ post: post._raw.flattenedPath })); +export const dynamic = 'force-dynamic'; export const generateMetadata = ({ params }: { params: { content: string } }) => { - const document = allDocuments.find((post) => post._raw.flattenedPath === params.content); + const document = allDocuments.find((doc) => doc._raw.flattenedPath === params.content); if (!document) { return { title: 'Not Found' }; diff --git a/apps/marketing/src/app/(marketing)/blog/[post]/page.tsx b/apps/marketing/src/app/(marketing)/blog/[post]/page.tsx index 495b8946e..14b8b2d8f 100644 --- a/apps/marketing/src/app/(marketing)/blog/[post]/page.tsx +++ b/apps/marketing/src/app/(marketing)/blog/[post]/page.tsx @@ -9,9 +9,6 @@ import { useMDXComponent } from 'next-contentlayer/hooks'; export const dynamic = 'force-dynamic'; -export const generateStaticParams = () => - allBlogPosts.map((post) => ({ post: post._raw.flattenedPath })); - export const generateMetadata = ({ params }: { params: { post: string } }) => { const blogPost = allBlogPosts.find((post) => post._raw.flattenedPath === `blog/${params.post}`); diff --git a/apps/marketing/src/app/(marketing)/blog/page.tsx b/apps/marketing/src/app/(marketing)/blog/page.tsx index 2eac963d1..4be1ab694 100644 --- a/apps/marketing/src/app/(marketing)/blog/page.tsx +++ b/apps/marketing/src/app/(marketing)/blog/page.tsx @@ -5,6 +5,7 @@ import { allBlogPosts } from 'contentlayer/generated'; export const metadata: Metadata = { title: 'Blog', }; + export default function BlogPage() { const blogPosts = allBlogPosts.sort((a, b) => { const dateA = new Date(a.date); diff --git a/apps/marketing/src/app/(marketing)/singleplayer/client.tsx b/apps/marketing/src/app/(marketing)/singleplayer/client.tsx index e0b55dbf5..9f1ebb289 100644 --- a/apps/marketing/src/app/(marketing)/singleplayer/client.tsx +++ b/apps/marketing/src/app/(marketing)/singleplayer/client.tsx @@ -256,6 +256,7 @@ export const SinglePlayerClient = () => { fields={fields} onSubmit={onSignSubmit} requireName={Boolean(fields.find((field) => field.type === 'NAME'))} + requireCustomText={Boolean(fields.find((field) => field.type === 'TEXT'))} requireSignature={Boolean(fields.find((field) => field.type === 'SIGNATURE'))} /> diff --git a/apps/marketing/src/app/layout.tsx b/apps/marketing/src/app/layout.tsx index 57da42c3f..99a1a6483 100644 --- a/apps/marketing/src/app/layout.tsx +++ b/apps/marketing/src/app/layout.tsx @@ -2,6 +2,8 @@ import { Suspense } from 'react'; import { Caveat, Inter } from 'next/font/google'; +import { PublicEnvScript } from 'next-runtime-env'; + import { FeatureFlagProvider } from '@documenso/lib/client-only/providers/feature-flag'; import { NEXT_PUBLIC_MARKETING_URL } from '@documenso/lib/constants/app'; import { getAllAnonymousFlags } from '@documenso/lib/universal/get-feature-flag'; @@ -62,6 +64,7 @@ export default async function RootLayout({ children }: { children: React.ReactNo + diff --git a/apps/marketing/src/components/(marketing)/pricing-table.tsx b/apps/marketing/src/components/(marketing)/pricing-table.tsx index 0d9956d86..bdaa9fdf4 100644 --- a/apps/marketing/src/components/(marketing)/pricing-table.tsx +++ b/apps/marketing/src/components/(marketing)/pricing-table.tsx @@ -114,7 +114,9 @@ export const PricingTable = ({ className, ...props }: PricingTableProps) => {

diff --git a/apps/web/package.json b/apps/web/package.json index fd4faa0c1..efd524992 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -14,6 +14,7 @@ "copy:pdfjs": "node ../../scripts/copy-pdfjs.cjs" }, "dependencies": { + "@documenso/api": "*", "@documenso/assets": "*", "@documenso/ee": "*", "@documenso/lib": "*", @@ -42,6 +43,7 @@ "react-hotkeys-hook": "^4.4.1", "react-icons": "^4.11.0", "react-rnd": "^10.4.1", + "remeda": "^1.27.1", "sharp": "0.33.1", "ts-pattern": "^5.0.5", "typescript": "5.2.2", diff --git a/apps/web/src/app/(dashboard)/admin/nav.tsx b/apps/web/src/app/(dashboard)/admin/nav.tsx index 089861069..b0d652283 100644 --- a/apps/web/src/app/(dashboard)/admin/nav.tsx +++ b/apps/web/src/app/(dashboard)/admin/nav.tsx @@ -1,11 +1,11 @@ 'use client'; -import { HTMLAttributes } from 'react'; +import type { HTMLAttributes } from 'react'; import Link from 'next/link'; import { usePathname } from 'next/navigation'; -import { BarChart3, FileStack, User2, Wallet2 } from 'lucide-react'; +import { BarChart3, FileStack, Settings, User2, Wallet2 } from 'lucide-react'; import { cn } from '@documenso/ui/lib/utils'; import { Button } from '@documenso/ui/primitives/button'; @@ -78,6 +78,20 @@ export const AdminNav = ({ className, ...props }: AdminNavProps) => { Subscriptions + +
); }; diff --git a/apps/web/src/app/(dashboard)/admin/site-settings/banner-form.tsx b/apps/web/src/app/(dashboard)/admin/site-settings/banner-form.tsx new file mode 100644 index 000000000..351e146ff --- /dev/null +++ b/apps/web/src/app/(dashboard)/admin/site-settings/banner-form.tsx @@ -0,0 +1,200 @@ +'use client'; + +import { useRouter } from 'next/navigation'; + +import { zodResolver } from '@hookform/resolvers/zod'; +import { useForm } from 'react-hook-form'; +import type { z } from 'zod'; + +import type { TSiteSettingsBannerSchema } from '@documenso/lib/server-only/site-settings/schemas/banner'; +import { + SITE_SETTINGS_BANNER_ID, + ZSiteSettingsBannerSchema, +} from '@documenso/lib/server-only/site-settings/schemas/banner'; +import { TRPCClientError } from '@documenso/trpc/client'; +import { trpc as trpcReact } from '@documenso/trpc/react'; +import { Button } from '@documenso/ui/primitives/button'; +import { ColorPicker } from '@documenso/ui/primitives/color-picker'; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@documenso/ui/primitives/form/form'; +import { Switch } from '@documenso/ui/primitives/switch'; +import { Textarea } from '@documenso/ui/primitives/textarea'; +import { useToast } from '@documenso/ui/primitives/use-toast'; + +const ZBannerFormSchema = ZSiteSettingsBannerSchema; + +type TBannerFormSchema = z.infer; + +export type BannerFormProps = { + banner?: TSiteSettingsBannerSchema; +}; + +export function BannerForm({ banner }: BannerFormProps) { + const router = useRouter(); + const { toast } = useToast(); + + const form = useForm({ + resolver: zodResolver(ZBannerFormSchema), + defaultValues: { + id: SITE_SETTINGS_BANNER_ID, + enabled: banner?.enabled ?? false, + data: { + content: banner?.data?.content ?? '', + bgColor: banner?.data?.bgColor ?? '#000000', + textColor: banner?.data?.textColor ?? '#FFFFFF', + }, + }, + }); + + const enabled = form.watch('enabled'); + + const { mutateAsync: updateSiteSetting, isLoading: isUpdateSiteSettingLoading } = + trpcReact.admin.updateSiteSetting.useMutation(); + + const onBannerUpdate = async ({ id, enabled, data }: TBannerFormSchema) => { + try { + await updateSiteSetting({ + id, + enabled, + data, + }); + + toast({ + title: 'Banner Updated', + description: 'Your banner has been updated successfully.', + duration: 5000, + }); + + router.refresh(); + } catch (err) { + if (err instanceof TRPCClientError && err.data?.code === 'BAD_REQUEST') { + toast({ + title: 'An error occurred', + description: err.message, + variant: 'destructive', + }); + } else { + toast({ + title: 'An unknown error occurred', + variant: 'destructive', + description: + 'We encountered an unknown error while attempting to update the banner. Please try again later.', + }); + } + } + }; + + return ( +
+

Site Banner

+

+ The site banner is a message that is shown at the top of the site. It can be used to display + important information to your users. +

+ +
+ +
+ ( + + Enabled + + +
+ +
+
+
+ )} + /> + +
+ ( + + Background Color + + +
+ +
+
+ + +
+ )} + /> + + ( + + Text Color + + +
+ +
+
+ + +
+ )} + /> +
+
+ +
+ ( + + Content + + +