[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col has-[>[data-slot=field]]:rounded-md has-[>[data-slot=field]]:border [&>[data-slot=field]]:p-4",
- "has-data-[state=checked]:bg-primary/5 has-data-[state=checked]:border-primary dark:has-data-[state=checked]:bg-primary/10",
- className
+ 'group/field-label peer/field-label flex w-fit gap-2 leading-snug group-data-[disabled=true]/field:opacity-50 has-data-checked:border-primary/30 has-data-checked:bg-primary/5 has-[>[data-slot=field]]:rounded-lg has-[>[data-slot=field]]:border *:data-[slot=field]:p-2.5 dark:has-data-checked:border-primary/20 dark:has-data-checked:bg-primary/10',
+ 'has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col',
+ className,
)}
{...props}
/>
)
}
-function FieldTitle({ className, ...props }: React.ComponentProps<"div">) {
+function FieldTitle({ className, ...props }: React.ComponentProps<'div'>) {
return (
)
}
-function FieldDescription({ className, ...props }: React.ComponentProps<"p">) {
+function FieldDescription({ className, ...props }: React.ComponentProps<'p'>) {
return (
a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4",
- className
+ 'text-left text-sm leading-normal font-normal text-muted-foreground group-has-data-horizontal/field:text-balance [[data-variant=legend]+&]:-mt-1.5',
+ 'last:mt-0 nth-last-2:-mt-1',
+ '[&>a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary',
+ className,
)}
{...props}
/>
@@ -157,7 +138,7 @@ function FieldSeparator({
children,
className,
...props
-}: React.ComponentProps<"div"> & {
+}: React.ComponentProps<'div'> & {
children?: React.ReactNode
}) {
return (
@@ -165,15 +146,15 @@ function FieldSeparator({
data-slot="field-separator"
data-content={!!children}
className={cn(
- "relative -my-2 h-5 text-sm group-data-[variant=outline]/field-group:-mb-2",
- className
+ 'relative -my-2 h-5 text-sm group-data-[variant=outline]/field-group:-mb-2',
+ className,
)}
{...props}
>
{children && (
{children}
@@ -188,7 +169,7 @@ function FieldError({
children,
errors,
...props
-}: React.ComponentProps<"div"> & {
+}: React.ComponentProps<'div'> & {
errors?: Array<{ message?: string } | undefined>
}) {
const content = useMemo(() => {
@@ -196,20 +177,19 @@ function FieldError({
return children
}
- if (!errors) {
+ if (!errors?.length) {
return null
}
- if (errors?.length === 1 && errors[0]?.message) {
- return errors[0].message
+ const uniqueErrors = [...new Map(errors.map((error) => [error?.message, error])).values()]
+
+ if (uniqueErrors?.length == 1) {
+ return uniqueErrors[0]?.message
}
return (
- {errors.map(
- (error, index) =>
- error?.message && {error.message}
- )}
+ {uniqueErrors.map((error, index) => error?.message && {error.message} )}
)
}, [children, errors])
@@ -222,7 +202,7 @@ function FieldError({
{content}
diff --git a/src/components/ui/input.tsx b/src/components/ui/input.tsx
index 69b64fb2..b5b105a6 100644
--- a/src/components/ui/input.tsx
+++ b/src/components/ui/input.tsx
@@ -1,22 +1,19 @@
-import * as React from "react"
+import * as React from 'react'
-import { cn } from "@/lib/utils"
+import { cn } from '@/lib/utils'
-const Input = React.forwardRef
>(
- ({ className, type, ...props }, ref) => {
- return (
-
- )
- }
-)
-Input.displayName = "Input"
+function Input({ className, type, ...props }: React.ComponentProps<'input'>) {
+ return (
+
+ )
+}
export { Input }
diff --git a/src/components/ui/label.tsx b/src/components/ui/label.tsx
index 53418217..1ac80f70 100644
--- a/src/components/ui/label.tsx
+++ b/src/components/ui/label.tsx
@@ -1,26 +1,24 @@
"use client"
import * as React from "react"
-import * as LabelPrimitive from "@radix-ui/react-label"
-import { cva, type VariantProps } from "class-variance-authority"
+import { Label as LabelPrimitive } from "radix-ui"
import { cn } from "@/lib/utils"
-const labelVariants = cva(
- "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
-)
-
-const Label = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef &
- VariantProps
->(({ className, ...props }, ref) => (
-
-))
-Label.displayName = LabelPrimitive.Root.displayName
+function Label({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
export { Label }
diff --git a/src/components/ui/native-select.tsx b/src/components/ui/native-select.tsx
index 25902b0f..bc53b6b7 100644
--- a/src/components/ui/native-select.tsx
+++ b/src/components/ui/native-select.tsx
@@ -1,22 +1,18 @@
-import * as React from "react"
+import * as React from 'react'
-import { cn } from "@/lib/utils"
-import { ChevronDownIcon } from "lucide-react"
+import { cn } from '@/lib/utils'
+import { ChevronDownIcon } from 'lucide-react'
-type NativeSelectProps = Omit, "size"> & {
- size?: "sm" | "default"
+type NativeSelectProps = Omit, 'size'> & {
+ size?: 'sm' | 'default'
}
-function NativeSelect({
- className,
- size = "default",
- ...props
-}: NativeSelectProps) {
+function NativeSelect({ className, size = 'default', ...props }: NativeSelectProps) {
return (
-
+
)
}
-function NativeSelectOption({ ...props }: React.ComponentProps<"option">) {
+function NativeSelectOption({ ...props }: React.ComponentProps<'option'>) {
return
}
-function NativeSelectOptGroup({
- className,
- ...props
-}: React.ComponentProps<"optgroup">) {
- return (
-
- )
+function NativeSelectOptGroup({ className, ...props }: React.ComponentProps<'optgroup'>) {
+ return
}
export { NativeSelect, NativeSelectOptGroup, NativeSelectOption }
diff --git a/src/components/ui/select.tsx b/src/components/ui/select.tsx
index a44c5390..1a32a077 100644
--- a/src/components/ui/select.tsx
+++ b/src/components/ui/select.tsx
@@ -1,152 +1,185 @@
'use client'
import * as React from 'react'
-import * as SelectPrimitive from '@radix-ui/react-select'
-import { Check, ChevronDown, ChevronUp } from 'lucide-react'
+import { Select as SelectPrimitive } from 'radix-ui'
import { cn } from '@/lib/utils'
+import { ChevronDownIcon, CheckIcon, ChevronUpIcon } from 'lucide-react'
-const Select = SelectPrimitive.Root
+function Select({ ...props }: React.ComponentProps) {
+ return
+}
-const SelectGroup = SelectPrimitive.Group
+function SelectGroup({ className, ...props }: React.ComponentProps) {
+ return (
+
+ )
+}
-const SelectValue = SelectPrimitive.Value
+function SelectValue({ ...props }: React.ComponentProps) {
+ return
+}
-const SelectTrigger = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, children, ...props }, ref) => (
- span]:line-clamp-1',
- className,
- )}
- {...props}
- >
- {children}
-
-
-
-
-))
-SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
-
-const SelectScrollUpButton = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, ...props }, ref) => (
-
-
-
-))
-SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
-
-const SelectScrollDownButton = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, ...props }, ref) => (
-
-
-
-))
-SelectScrollDownButton.displayName = SelectPrimitive.ScrollDownButton.displayName
-
-const SelectContent = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, children, position = 'popper', ...props }, ref) => (
-
- & {
+ size?: 'sm' | 'default'
+}) {
+ return (
+
-
-
+
+
+
+ )
+}
+
+function SelectContent({
+ className,
+ children,
+ position = 'item-aligned',
+ align = 'center',
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
- {children}
-
-
-
-
-))
-SelectContent.displayName = SelectPrimitive.Content.displayName
+
+
+ {children}
+
+
+
+
+ )
+}
-const SelectLabel = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, ...props }, ref) => (
-
-))
-SelectLabel.displayName = SelectPrimitive.Label.displayName
+function SelectLabel({ className, ...props }: React.ComponentProps) {
+ return (
+
+ )
+}
-const SelectItem = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, children, ...props }, ref) => (
-
-
-
-
-
-
- {children}
-
-))
-SelectItem.displayName = SelectPrimitive.Item.displayName
+function SelectItem({
+ className,
+ children,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+
+
+
+ {children}
+
+ )
+}
-const SelectSeparator = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, ...props }, ref) => (
-
-))
-SelectSeparator.displayName = SelectPrimitive.Separator.displayName
+function SelectSeparator({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function SelectScrollUpButton({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+ )
+}
+
+function SelectScrollDownButton({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+ )
+}
export {
Select,
- SelectGroup,
- SelectValue,
- SelectTrigger,
SelectContent,
- SelectLabel,
+ SelectGroup,
SelectItem,
- SelectSeparator,
- SelectScrollUpButton,
+ SelectLabel,
SelectScrollDownButton,
+ SelectScrollUpButton,
+ SelectSeparator,
+ SelectTrigger,
+ SelectValue,
}
diff --git a/src/components/ui/separator.tsx b/src/components/ui/separator.tsx
index 12d81c4a..d4570908 100644
--- a/src/components/ui/separator.tsx
+++ b/src/components/ui/separator.tsx
@@ -1,31 +1,28 @@
"use client"
import * as React from "react"
-import * as SeparatorPrimitive from "@radix-ui/react-separator"
+import { Separator as SeparatorPrimitive } from "radix-ui"
import { cn } from "@/lib/utils"
-const Separator = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(
- (
- { className, orientation = "horizontal", decorative = true, ...props },
- ref
- ) => (
+function Separator({
+ className,
+ orientation = "horizontal",
+ decorative = true,
+ ...props
+}: React.ComponentProps) {
+ return (
)
-)
-Separator.displayName = SeparatorPrimitive.Root.displayName
+}
export { Separator }
diff --git a/src/components/ui/textarea.tsx b/src/components/ui/textarea.tsx
index 7a6aa194..1ade6dc5 100644
--- a/src/components/ui/textarea.tsx
+++ b/src/components/ui/textarea.tsx
@@ -1,14 +1,14 @@
-import * as React from "react"
+import * as React from 'react'
-import { cn } from "@/lib/utils"
+import { cn } from '@/lib/utils'
-function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
+function Textarea({ className, ...props }: React.ComponentProps<'textarea'>) {
return (
diff --git a/src/globals/SiteSettings.ts b/src/globals/SiteSettings.ts
index 100ef311..624c255b 100644
--- a/src/globals/SiteSettings.ts
+++ b/src/globals/SiteSettings.ts
@@ -42,6 +42,7 @@ export const SiteSettings: GlobalConfig = {
{ label: 'Instagram', value: 'instagram' },
{ label: 'Facebook', value: 'facebook' },
{ label: 'YouTube', value: 'youtube' },
+ { label: 'WhatsApp', value: 'whatsapp' },
],
},
{
diff --git a/src/lib/schemas/comment.ts b/src/lib/schemas/comment.ts
new file mode 100644
index 00000000..b97fabee
--- /dev/null
+++ b/src/lib/schemas/comment.ts
@@ -0,0 +1,12 @@
+import { z } from 'zod'
+
+export const commentSchema = z.object({
+ comment: z
+ .string()
+ .trim()
+ .max(1000, 'Comentário muito longo')
+ .refine((v) => !/<\/?[a-z][\s\S]*>/i.test(v), 'O comentário não pode conter HTML')
+ .optional(),
+})
+
+export type Comment = z.infer
diff --git a/src/lib/schemas/contact.ts b/src/lib/schemas/contact.ts
new file mode 100644
index 00000000..f22121b8
--- /dev/null
+++ b/src/lib/schemas/contact.ts
@@ -0,0 +1,34 @@
+import { CitiesByState } from '@/lib/cities'
+import { z } from 'zod'
+
+export type StateKey = keyof typeof CitiesByState
+
+export const contactSchema = z.object({
+ name: z
+ .string()
+ .trim()
+ .min(2, 'Nome deve ter pelo menos 2 caracteres')
+ .max(120, 'Nome deve ter no máximo 100 caracteres')
+ .regex(
+ /^(?=.* )[\p{L}\p{M}]+(?:[- ][\p{L}\p{M}]+)*$/u,
+ 'Informe nome e sobrenome. Use apenas letras, no máximo um espaço ou hífen entre termos, e sem espaço no início ou no fim.',
+ ),
+ email: z.email('Email inválido'),
+ phone: z
+ .string()
+ .trim()
+ .length(11, 'Telefone celular inválido')
+ .regex(/^\d{11}$/, 'Telefone celular inválido'),
+ state: z.custom(
+ (value) => typeof value === 'string' && value in CitiesByState,
+ 'Estado inválido',
+ ),
+ city: z.string().trim().min(3, 'Cidade inválida').max(100, 'Cidade muito longa'),
+ postalCode: z
+ .string()
+ .trim()
+ .regex(/^(?:\d{8})?$/, 'CEP inválido')
+ .optional(),
+})
+
+export type Contact = z.infer
diff --git a/src/lib/schemas/petition-form.ts b/src/lib/schemas/petition-form.ts
index 3aa99230..a2c03721 100644
--- a/src/lib/schemas/petition-form.ts
+++ b/src/lib/schemas/petition-form.ts
@@ -1,40 +1,7 @@
-import { CitiesByState } from '@/lib/cities'
import { z } from 'zod'
+import { contactSchema } from './contact'
+import { commentSchema } from './comment'
-type StateKey = keyof typeof CitiesByState
-
-export const petitionFormSchema = z.object({
- name: z
- .string()
- .trim()
- .min(2, 'Nome deve ter pelo menos 2 caracteres')
- .max(120, 'Nome deve ter no máximo 100 caracteres')
- .regex(
- /^(?=.* )[\p{L}\p{M}]+(?:[- ][\p{L}\p{M}]+)*$/u,
- 'Informe nome e sobrenome. Use apenas letras, no máximo um espaço ou hífen entre termos, e sem espaço no início ou no fim.',
- ),
- email: z.email('Email inválido'),
- phone: z
- .string()
- .trim()
- .length(11, 'Telefone celular inválido')
- .regex(/^\d{11}$/, 'Telefone celular inválido'),
- state: z.custom(
- (value) => typeof value === 'string' && value in CitiesByState,
- 'Estado inválido',
- ),
- city: z.string().trim().min(3, 'Cidade inválida').max(100, 'Cidade muito longa'),
- postalCode: z
- .string()
- .trim()
- .regex(/^(?:\d{8})?$/, 'CEP inválido')
- .optional(),
- comment: z
- .string()
- .trim()
- .max(1000, 'Comentário muito longo')
- .refine((v) => !/<\/?[a-z][\s\S]*>/i.test(v), 'O comentário não pode conter HTML')
- .optional(),
-})
+export const petitionFormSchema = contactSchema.extend(commentSchema.shape)
export type PetitionFormInput = z.infer
diff --git a/src/lib/schemas/whatsapp-form.ts b/src/lib/schemas/whatsapp-form.ts
new file mode 100644
index 00000000..d082c35b
--- /dev/null
+++ b/src/lib/schemas/whatsapp-form.ts
@@ -0,0 +1,9 @@
+import { z } from 'zod'
+import { contactSchema } from './contact'
+import { commentSchema } from './comment'
+
+export const whatsAppFormSchema = contactSchema
+ .omit({ postalCode: true })
+ .extend(commentSchema.shape)
+
+export type WhatsAppFormInput = z.infer
diff --git a/src/payload-types.ts b/src/payload-types.ts
index 084fa157..072ed988 100644
--- a/src/payload-types.ts
+++ b/src/payload-types.ts
@@ -73,6 +73,7 @@ export interface Config {
contact: Contact;
consent: Consent;
signature: Signature;
+ subscription: Subscription;
'payload-kv': PayloadKv;
'payload-jobs': PayloadJob;
'payload-locked-documents': PayloadLockedDocument;
@@ -87,6 +88,7 @@ export interface Config {
contact: ContactSelect | ContactSelect;
consent: ConsentSelect | ConsentSelect;
signature: SignatureSelect | SignatureSelect;
+ subscription: SubscriptionSelect | SubscriptionSelect;
'payload-kv': PayloadKvSelect | PayloadKvSelect;
'payload-jobs': PayloadJobsSelect | PayloadJobsSelect;
'payload-locked-documents': PayloadLockedDocumentsSelect | PayloadLockedDocumentsSelect;
@@ -270,7 +272,6 @@ export interface Contact {
| 'TO';
city: string;
postalCode?: string | null;
- comment?: string | null;
updatedAt: string;
createdAt: string;
}
@@ -283,6 +284,19 @@ export interface Signature {
contact: number | Contact;
petition: string | Petition;
consent: number | Consent;
+ comment?: string | null;
+ updatedAt: string;
+ createdAt: string;
+}
+/**
+ * This interface was referenced by `Config`'s JSON-Schema
+ * via the `definition` "subscription".
+ */
+export interface Subscription {
+ id: number;
+ contact: number | Contact;
+ consent: number | Consent;
+ comment?: string | null;
updatedAt: string;
createdAt: string;
}
@@ -425,6 +439,10 @@ export interface PayloadLockedDocument {
| ({
relationTo: 'signature';
value: number | Signature;
+ } | null)
+ | ({
+ relationTo: 'subscription';
+ value: number | Subscription;
} | null);
globalSlug?: string | null;
user: {
@@ -540,7 +558,6 @@ export interface ContactSelect {
state?: T;
city?: T;
postalCode?: T;
- comment?: T;
updatedAt?: T;
createdAt?: T;
}
@@ -561,6 +578,18 @@ export interface SignatureSelect {
contact?: T;
petition?: T;
consent?: T;
+ comment?: T;
+ updatedAt?: T;
+ createdAt?: T;
+}
+/**
+ * This interface was referenced by `Config`'s JSON-Schema
+ * via the `definition` "subscription_select".
+ */
+export interface SubscriptionSelect {
+ contact?: T;
+ consent?: T;
+ comment?: T;
updatedAt?: T;
createdAt?: T;
}
@@ -644,7 +673,7 @@ export interface SiteSetting {
headerTitle?: string | null;
socialLinks?:
| {
- platform: 'instagram' | 'facebook' | 'youtube';
+ platform: 'instagram' | 'facebook' | 'youtube' | 'whatsapp';
url: string;
/**
* Rótulo opcional para acessibilidade. Se estiver vazio, usamos um rótulo padrão da plataforma.
diff --git a/src/payload.config.ts b/src/payload.config.ts
index ee7f2c6c..598aeff3 100644
--- a/src/payload.config.ts
+++ b/src/payload.config.ts
@@ -16,6 +16,7 @@ import { Contact } from './collections/Contact'
import { Consent } from './collections/Consent'
import { Signature } from './collections/Signature'
import { vercelBlobStorage } from '@payloadcms/storage-vercel-blob'
+import { Subscription } from './collections/Subscription'
const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)
@@ -31,7 +32,7 @@ export default buildConfig({
globals: [SiteSettings.slug, HomePage.slug],
},
},
- collections: [Users, Media, Petition, Contact, Consent, Signature],
+ collections: [Users, Media, Petition, Contact, Consent, Signature, Subscription],
globals: [SiteSettings, HomePage, Metadata],
editor: lexicalEditor(),
secret: process.env.PAYLOAD_SECRET || '',