mirror of
https://github.com/documenso/documenso.git
synced 2025-11-23 05:01:54 +10:00
Merge branch 'main' into feat/save-data-on-blur
This commit is contained in:
35
packages/ui/components/call-to-action.tsx
Normal file
35
packages/ui/components/call-to-action.tsx
Normal file
@ -0,0 +1,35 @@
|
||||
import Link from 'next/link';
|
||||
|
||||
import { Button } from '../primitives/button';
|
||||
import { Card, CardContent } from '../primitives/card';
|
||||
|
||||
type CallToActionProps = {
|
||||
className?: string;
|
||||
utmSource?: string;
|
||||
};
|
||||
|
||||
export const CallToAction = ({ className, utmSource = 'generic-cta' }: CallToActionProps) => {
|
||||
return (
|
||||
<Card spotlight className={className}>
|
||||
<CardContent className="flex flex-col items-center justify-center p-12">
|
||||
<h2 className="text-center text-2xl font-bold">Looking for the managed solution?</h2>
|
||||
|
||||
<p className="text-muted-foreground mt-4 max-w-[55ch] text-center leading-normal">
|
||||
You can get started with Documenso in minutes. We handle the infrastructure, so you can
|
||||
focus on signing documents.
|
||||
</p>
|
||||
|
||||
<Button
|
||||
className="focus-visible:ring-ring ring-offset-background bg-primary text-primary-foreground hover:bg-primary/90text-sm mt-8 inline-flex items-center justify-center rounded-full border font-medium no-underline transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50"
|
||||
variant="default"
|
||||
size="lg"
|
||||
asChild
|
||||
>
|
||||
<Link href={`https://app.documenso.com/signup?utm_source=${utmSource}`} target="_blank">
|
||||
Get started
|
||||
</Link>
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
57
packages/ui/components/common/language-switcher-dialog.tsx
Normal file
57
packages/ui/components/common/language-switcher-dialog.tsx
Normal file
@ -0,0 +1,57 @@
|
||||
import { msg } from '@lingui/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { CheckIcon } from 'lucide-react';
|
||||
|
||||
import { SUPPORTED_LANGUAGES } from '@documenso/lib/constants/i18n';
|
||||
import { switchI18NLanguage } from '@documenso/lib/server-only/i18n/switch-i18n-language';
|
||||
import { dynamicActivate } from '@documenso/lib/utils/i18n';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
import {
|
||||
CommandDialog,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from '@documenso/ui/primitives/command';
|
||||
|
||||
type LanguageSwitcherDialogProps = {
|
||||
open: boolean;
|
||||
setOpen: (_open: boolean) => void;
|
||||
};
|
||||
|
||||
export const LanguageSwitcherDialog = ({ open, setOpen }: LanguageSwitcherDialogProps) => {
|
||||
const { i18n, _ } = useLingui();
|
||||
|
||||
const setLanguage = async (lang: string) => {
|
||||
setOpen(false);
|
||||
|
||||
await dynamicActivate(i18n, lang);
|
||||
await switchI18NLanguage(lang);
|
||||
};
|
||||
|
||||
return (
|
||||
<CommandDialog open={open} onOpenChange={setOpen}>
|
||||
<CommandInput placeholder={_(msg`Search languages...`)} />
|
||||
|
||||
<CommandList>
|
||||
<CommandGroup>
|
||||
{Object.values(SUPPORTED_LANGUAGES).map((language) => (
|
||||
<CommandItem
|
||||
key={language.short}
|
||||
value={language.full}
|
||||
onSelect={async () => setLanguage(language.short)}
|
||||
>
|
||||
<CheckIcon
|
||||
className={cn(
|
||||
'mr-2 h-4 w-4',
|
||||
i18n.locale === language.short ? 'opacity-100' : 'opacity-0',
|
||||
)}
|
||||
/>
|
||||
{SUPPORTED_LANGUAGES[language.short].full}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</CommandDialog>
|
||||
);
|
||||
};
|
||||
@ -155,17 +155,7 @@ export const FieldAdvancedSettings = forwardRef<HTMLDivElement, FieldAdvancedSet
|
||||
|
||||
const doesFieldExist = (!!document || !!template) && field.nativeId !== undefined;
|
||||
|
||||
const { data: fieldData } = trpc.field.getField.useQuery(
|
||||
{
|
||||
fieldId: Number(field.nativeId),
|
||||
teamId,
|
||||
},
|
||||
{
|
||||
enabled: doesFieldExist,
|
||||
},
|
||||
);
|
||||
|
||||
const fieldMeta = fieldData?.fieldMeta;
|
||||
const fieldMeta = field?.fieldMeta;
|
||||
|
||||
const localStorageKey = `field_${field.formId}_${field.type}`;
|
||||
|
||||
|
||||
@ -9,6 +9,13 @@ import type { StrokeOptions } from 'perfect-freehand';
|
||||
import { getStroke } from 'perfect-freehand';
|
||||
|
||||
import { unsafe_useEffectOnce } from '@documenso/lib/client-only/hooks/use-effect-once';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@documenso/ui/primitives/select';
|
||||
|
||||
import { cn } from '../../lib/utils';
|
||||
import { getSvgPathFromStroke } from './helper';
|
||||
@ -36,6 +43,7 @@ export const SignaturePad = ({
|
||||
const [isPressed, setIsPressed] = useState(false);
|
||||
const [lines, setLines] = useState<Point[][]>([]);
|
||||
const [currentLine, setCurrentLine] = useState<Point[]>([]);
|
||||
const [selectedColor, setSelectedColor] = useState('black');
|
||||
|
||||
const perfectFreehandOptions = useMemo(() => {
|
||||
const size = $el.current ? Math.min($el.current.height, $el.current.width) * 0.03 : 10;
|
||||
@ -85,6 +93,7 @@ export const SignaturePad = ({
|
||||
ctx.restore();
|
||||
ctx.imageSmoothingEnabled = true;
|
||||
ctx.imageSmoothingQuality = 'high';
|
||||
ctx.fillStyle = selectedColor;
|
||||
|
||||
lines.forEach((line) => {
|
||||
const pathData = new Path2D(
|
||||
@ -129,6 +138,7 @@ export const SignaturePad = ({
|
||||
|
||||
ctx.imageSmoothingEnabled = true;
|
||||
ctx.imageSmoothingQuality = 'high';
|
||||
ctx.fillStyle = selectedColor;
|
||||
|
||||
newLines.forEach((line) => {
|
||||
const pathData = new Path2D(
|
||||
@ -237,7 +247,13 @@ export const SignaturePad = ({
|
||||
>
|
||||
<canvas
|
||||
ref={$el}
|
||||
className={cn('relative block dark:invert', className)}
|
||||
className={cn(
|
||||
'relative block',
|
||||
{
|
||||
'dark:hue-rotate-180 dark:invert': selectedColor === 'black',
|
||||
},
|
||||
className,
|
||||
)}
|
||||
style={{ touchAction: 'none' }}
|
||||
onPointerMove={(event) => onMouseMove(event)}
|
||||
onPointerDown={(event) => onMouseDown(event)}
|
||||
@ -247,6 +263,44 @@ export const SignaturePad = ({
|
||||
{...props}
|
||||
/>
|
||||
|
||||
<div className="text-foreground absolute right-2 top-2 filter">
|
||||
<Select defaultValue={selectedColor} onValueChange={(value) => setSelectedColor(value)}>
|
||||
<SelectTrigger className="h-auto w-auto border-none p-1">
|
||||
<SelectValue placeholder="" />
|
||||
</SelectTrigger>
|
||||
|
||||
<SelectContent className="w-[150px]" align="end">
|
||||
<SelectItem value="black">
|
||||
<div className="text-muted-foreground flex items-center px-1 text-sm">
|
||||
<div className="border-border mr-2 h-5 w-5 rounded-full border-2 bg-black shadow-sm" />
|
||||
<Trans>Black</Trans>
|
||||
</div>
|
||||
</SelectItem>
|
||||
|
||||
<SelectItem value="red">
|
||||
<div className="text-muted-foreground flex items-center px-1 text-sm">
|
||||
<div className="border-border mr-2 h-5 w-5 rounded-full border-2 bg-[red] shadow-sm" />
|
||||
<Trans>Red</Trans>
|
||||
</div>
|
||||
</SelectItem>
|
||||
|
||||
<SelectItem value="blue">
|
||||
<div className="text-muted-foreground flex items-center px-1 text-sm">
|
||||
<div className="border-border mr-2 h-5 w-5 rounded-full border-2 bg-[blue] shadow-sm" />
|
||||
<Trans>Blue</Trans>
|
||||
</div>
|
||||
</SelectItem>
|
||||
|
||||
<SelectItem value="green">
|
||||
<div className="text-muted-foreground flex items-center px-1 text-sm">
|
||||
<div className="border-border mr-2 h-5 w-5 rounded-full border-2 bg-[green] shadow-sm" />
|
||||
<Trans>Green</Trans>
|
||||
</div>
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="absolute bottom-4 right-4 flex gap-2">
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@ -157,6 +157,38 @@ export const AddTemplateFieldsFormPartial = ({
|
||||
selectedSignerIndex === -1 ? 0 : selectedSignerIndex,
|
||||
);
|
||||
|
||||
const filterFieldsWithEmptyValues = (fields: typeof localFields, fieldType: string) =>
|
||||
fields
|
||||
.filter((field) => field.type === fieldType)
|
||||
.filter((field) => {
|
||||
if (field.fieldMeta && 'values' in field.fieldMeta) {
|
||||
return field.fieldMeta.values?.length === 0;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
const emptyCheckboxFields = useMemo(
|
||||
() => filterFieldsWithEmptyValues(localFields, FieldType.CHECKBOX),
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[localFields],
|
||||
);
|
||||
|
||||
const emptyRadioFields = useMemo(
|
||||
() => filterFieldsWithEmptyValues(localFields, FieldType.RADIO),
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[localFields],
|
||||
);
|
||||
|
||||
const emptySelectFields = useMemo(
|
||||
() => filterFieldsWithEmptyValues(localFields, FieldType.DROPDOWN),
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[localFields],
|
||||
);
|
||||
|
||||
const hasErrors =
|
||||
emptyCheckboxFields.length > 0 || emptyRadioFields.length > 0 || emptySelectFields.length > 0;
|
||||
|
||||
const [isFieldWithinBounds, setIsFieldWithinBounds] = useState(false);
|
||||
const [coords, setCoords] = useState({
|
||||
x: 0,
|
||||
@ -789,6 +821,24 @@ export const AddTemplateFieldsFormPartial = ({
|
||||
</div>
|
||||
</DocumentFlowFormContainerContent>
|
||||
|
||||
{hasErrors && (
|
||||
<div className="mt-4">
|
||||
<ul>
|
||||
<li className="text-sm text-red-500">
|
||||
<Trans>
|
||||
To proceed further, please set at least one value for the{' '}
|
||||
{emptyCheckboxFields.length > 0
|
||||
? 'Checkbox'
|
||||
: emptyRadioFields.length > 0
|
||||
? 'Radio'
|
||||
: 'Select'}{' '}
|
||||
field.
|
||||
</Trans>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<DocumentFlowFormContainerFooter>
|
||||
<DocumentFlowFormContainerStep step={currentStep} maxStep={totalSteps} />
|
||||
|
||||
@ -796,6 +846,7 @@ export const AddTemplateFieldsFormPartial = ({
|
||||
loading={isSubmitting}
|
||||
disabled={isSubmitting}
|
||||
goNextLabel={msg`Save Template`}
|
||||
disableNextStep={hasErrors}
|
||||
onGoBackClick={() => {
|
||||
previousStep();
|
||||
remove();
|
||||
|
||||
Reference in New Issue
Block a user