petition styles update
This commit is contained in:
parent
aff72f26af
commit
12cc55b663
9 changed files with 156 additions and 31 deletions
|
|
@ -19,31 +19,39 @@ export default async function Page({ params }: { params: Promise<{ id: string }>
|
|||
return notFound()
|
||||
}
|
||||
|
||||
const consentHTML =
|
||||
typeof petition.form.consent !== 'number'
|
||||
? convertLexicalToHTML({ data: petition.form.consent.text })
|
||||
: ''
|
||||
|
||||
return (
|
||||
<main className="bg-background text-foreground">
|
||||
<section className="relative overflow-hidden border-b border-border">
|
||||
<div className="pointer-events-none absolute inset-0 bg-[radial-gradient(circle_at_top_right,oklch(var(--primary)/0.2),transparent_40%),radial-gradient(circle_at_bottom_left,oklch(var(--accent)/0.18),transparent_45%)]" />
|
||||
<div className="relative mx-auto flex min-h-[75vh] w-full max-w-4xl items-center justify-center px-4 py-20 sm:px-6 lg:px-8">
|
||||
<main
|
||||
data-theme="petition"
|
||||
className="h-screen w-screen overflow-y-auto bg-background text-foreground"
|
||||
>
|
||||
<section className="relative overflow-hidden border-b border-border bg-[var(--petition-hero)] text-[var(--petition-hero-foreground)]">
|
||||
<div className="pointer-events-none absolute inset-0 bg-[radial-gradient(circle_at_top_right,var(--petition-hero-glow),transparent_40%),radial-gradient(circle_at_bottom_left,var(--petition-hero-depth),transparent_46%)]" />
|
||||
<div className="relative mx-auto flex min-h-[72vh] w-full max-w-5xl items-center justify-center px-4 py-16 sm:px-6 sm:py-20 lg:px-8">
|
||||
<div className="mx-auto flex max-w-3xl flex-col items-center space-y-6 text-center">
|
||||
<span className="inline-flex w-fit rounded-full border border-border bg-card px-3 py-1 text-xs font-medium uppercase tracking-wider text-muted-foreground">
|
||||
<span className="inline-flex w-fit rounded-full border border-white/20 bg-[var(--petition-hero-chip)] px-3 py-1 text-xs font-medium uppercase tracking-wider text-[var(--petition-hero-chip-foreground)]">
|
||||
Abaixo-assinado
|
||||
</span>
|
||||
<h1 className="text-4xl font-bold leading-tight tracking-tight sm:text-5xl lg:text-6xl">
|
||||
<h1 className="text-4xl font-bold leading-tight tracking-tight text-balance sm:text-5xl lg:text-6xl">
|
||||
{petition.title}
|
||||
</h1>
|
||||
<h2 className="max-w-2xl text-lg leading-relaxed text-muted-foreground sm:text-xl">
|
||||
<h2 className="max-w-2xl border-none text-lg leading-relaxed text-[var(--petition-hero-muted)] sm:text-xl">
|
||||
{petition.subtitle}
|
||||
</h2>
|
||||
<div className="flex flex-wrap justify-center gap-3">
|
||||
<div className="flex flex-wrap justify-center gap-3 border-t border-white/15 pt-6">
|
||||
<Link
|
||||
href="#formulario"
|
||||
className="inline-flex items-center justify-center rounded-md bg-primary px-5 py-2.5 text-sm font-semibold text-primary-foreground transition-colors hover:bg-primary/90 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
||||
className="inline-flex items-center justify-center rounded-md bg-[var(--petition-hero-cta)] px-5 py-2.5 text-sm font-semibold text-[var(--petition-hero-cta-foreground)] transition-colors hover:bg-[color-mix(in_oklab,var(--petition-hero-cta)_90%,white)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/70"
|
||||
>
|
||||
Assinar agora
|
||||
</Link>
|
||||
<Link
|
||||
href="#detalhes"
|
||||
className="inline-flex items-center justify-center rounded-md border border-border bg-background px-5 py-2.5 text-sm font-semibold text-foreground transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
||||
className="inline-flex items-center justify-center rounded-md border border-white/30 bg-transparent px-5 py-2.5 text-sm font-semibold text-white transition-colors hover:bg-white/10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/70"
|
||||
>
|
||||
Ler detalhes
|
||||
</Link>
|
||||
|
|
@ -54,18 +62,30 @@ export default async function Page({ params }: { params: Promise<{ id: string }>
|
|||
|
||||
<section
|
||||
id="detalhes"
|
||||
className="mx-auto w-full max-w-4xl px-4 py-14 sm:px-6 lg:px-8 lg:py-16"
|
||||
className="mx-auto w-full max-w-3xl px-4 py-14 sm:px-6 lg:px-8 lg:py-16"
|
||||
>
|
||||
<article
|
||||
className="space-y-5 leading-relaxed text-foreground/90 [&_a]:font-medium [&_a]:text-primary [&_a]:underline-offset-4 [&_a:hover]:underline [&_blockquote]:border-l-2 [&_blockquote]:border-border [&_blockquote]:pl-4 [&_h1]:text-3xl [&_h1]:font-bold [&_h2]:text-2xl [&_h2]:font-semibold [&_h3]:text-xl [&_h3]:font-semibold [&_li]:ml-5 [&_li]:list-disc [&_p]:text-base [&_strong]:text-foreground"
|
||||
className="space-y-6 text-lg leading-8 text-foreground/90 [&_a]:font-medium [&_a]:text-primary [&_a]:underline-offset-4 [&_a:hover]:underline [&_blockquote]:border-l-4 [&_blockquote]:border-primary/40 [&_blockquote]:pl-4 [&_h1]:text-3xl [&_h1]:font-bold [&_h2]:border-none [&_h2]:text-2xl [&_h2]:font-semibold [&_h2]:text-primary [&_h3]:text-xl [&_h3]:font-semibold [&_li]:ml-5 [&_li]:list-disc [&_p]:text-lg [&_strong]:font-semibold [&_strong]:text-foreground"
|
||||
dangerouslySetInnerHTML={{ __html: convertLexicalToHTML({ data: petition.body }) }}
|
||||
/>
|
||||
</section>
|
||||
|
||||
<section className="border-t border-border bg-muted/30">
|
||||
<div className="mx-auto w-full max-w-3xl px-4 py-14 sm:px-6 lg:px-8 lg:py-16">
|
||||
<div className="rounded-xl border border-border bg-card p-5 shadow-sm sm:p-7">
|
||||
<PetitionForm id="formulario" petition={petition} />
|
||||
<section className="border-t border-border bg-[var(--petition-form-section)]">
|
||||
<div className="mx-auto grid w-full max-w-5xl gap-8 px-4 py-14 sm:px-6 lg:grid-cols-[0.82fr_1.18fr] lg:px-8 lg:py-16">
|
||||
<div className="space-y-4 self-start">
|
||||
<span className="text-sm font-semibold uppercase tracking-wider text-primary">
|
||||
Participe da mobilização
|
||||
</span>
|
||||
<h2 className="border-none text-left text-3xl font-bold leading-tight text-foreground sm:text-4xl">
|
||||
Sua assinatura fortalece a pressão pela jornada 5x2.
|
||||
</h2>
|
||||
<p className="text-base leading-7 text-muted-foreground">
|
||||
Preencha o formulário para somar apoio público à proposta. Os dados serão usados
|
||||
apenas para registrar sua participação nesta campanha.
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-xl border border-border bg-card p-5 text-card-foreground shadow-[0_24px_60px_rgb(122_25_18/0.12)] sm:p-7">
|
||||
<PetitionForm id="formulario" petition={petition} consentHTML={consentHTML} />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
|
|
|||
|
|
@ -104,6 +104,51 @@ body,
|
|||
--sidebar-ring: oklch(0.9 0.14 143);
|
||||
--background: #ae1603;
|
||||
--foreground: oklch(0.98 0.004 197.1);
|
||||
--field-background: #ffffff;
|
||||
--field-foreground: #000000;
|
||||
--field-placeholder: rgb(0 0 0 / 80%);
|
||||
--field-border: rgb(255 255 255 / 80%);
|
||||
--field-disabled-background: rgb(255 255 255 / 70%);
|
||||
--field-disabled-foreground: rgb(0 0 0 / 70%);
|
||||
--field-icon: rgb(0 0 0 / 60%);
|
||||
}
|
||||
|
||||
[data-theme="petition"] {
|
||||
--background: #fff8f2;
|
||||
--foreground: #2c2522;
|
||||
--card: #ffffff;
|
||||
--card-foreground: #2c2522;
|
||||
--popover: #ffffff;
|
||||
--popover-foreground: #2c2522;
|
||||
--primary: #c51414;
|
||||
--primary-foreground: #ffffff;
|
||||
--secondary: #ffe8df;
|
||||
--secondary-foreground: #5f1715;
|
||||
--muted: #f4e7de;
|
||||
--muted-foreground: #675c56;
|
||||
--accent: #ffcf2e;
|
||||
--accent-foreground: #332300;
|
||||
--border: rgb(189 119 104 / 24%);
|
||||
--input: rgb(189 119 104 / 32%);
|
||||
--ring: #c51414;
|
||||
--destructive: #b42318;
|
||||
--petition-hero: #b6120f;
|
||||
--petition-hero-foreground: #fffaf5;
|
||||
--petition-hero-muted: #ffe2d6;
|
||||
--petition-hero-chip: rgb(255 255 255 / 12%);
|
||||
--petition-hero-chip-foreground: #fff7ed;
|
||||
--petition-hero-glow: rgb(255 207 46 / 24%);
|
||||
--petition-hero-depth: rgb(122 0 0 / 35%);
|
||||
--petition-hero-cta: #ffcf2e;
|
||||
--petition-hero-cta-foreground: #332300;
|
||||
--petition-form-section: #f6ebe4;
|
||||
--field-background: #ffffff;
|
||||
--field-foreground: #2c2522;
|
||||
--field-placeholder: rgb(44 37 34 / 58%);
|
||||
--field-border: #ddc7bc;
|
||||
--field-disabled-background: #ece0d9;
|
||||
--field-disabled-foreground: rgb(44 37 34 / 55%);
|
||||
--field-icon: rgb(44 37 34 / 58%);
|
||||
}
|
||||
|
||||
.dark {
|
||||
|
|
|
|||
|
|
@ -22,14 +22,14 @@ import { PhoneInput } from './PhoneInput'
|
|||
import { StateSelect } from './StateSelect'
|
||||
import { CitySelect } from './CitySelect'
|
||||
import { PostalCodeInput } from './PostalCodeInput'
|
||||
import { convertLexicalToHTML } from '@payloadcms/richtext-lexical/html'
|
||||
|
||||
interface PetitionFormProps {
|
||||
id: string
|
||||
petition: Petition
|
||||
consentHTML: string
|
||||
}
|
||||
|
||||
export const PetitionForm = ({ id, petition }: PetitionFormProps) => {
|
||||
export const PetitionForm = ({ id, petition, consentHTML }: PetitionFormProps) => {
|
||||
const methods = useForm<PetitionFormInput>({
|
||||
resolver: zodResolver(petitionFormSchema),
|
||||
defaultValues: {
|
||||
|
|
@ -71,9 +71,7 @@ export const PetitionForm = ({ id, petition }: PetitionFormProps) => {
|
|||
<FieldSet>
|
||||
{petition.form.title ? <FieldLegend>{petition.form.title}</FieldLegend> : null}
|
||||
{petition.form.subtitle ? (
|
||||
<FieldDescription>
|
||||
Assine esta petição e ajude a manter a orla de Salvador para todos nós!
|
||||
</FieldDescription>
|
||||
<FieldDescription>{petition.form.subtitle}</FieldDescription>
|
||||
) : null}
|
||||
<FieldGroup>
|
||||
<Field>
|
||||
|
|
@ -105,10 +103,11 @@ export const PetitionForm = ({ id, petition }: PetitionFormProps) => {
|
|||
<Textarea id="comment" {...methods.register('comment')} />
|
||||
</Field>
|
||||
<Field>
|
||||
{typeof petition.form.consent !== 'number' ? (
|
||||
<FieldDescription
|
||||
{consentHTML ? (
|
||||
<div
|
||||
className="petition-consent text-left text-sm leading-normal font-normal text-muted-foreground [&_a]:underline [&_a]:underline-offset-4 [&_a:hover]:text-primary [&_p]:m-0 [&_strong]:font-semibold [&_strong]:text-foreground"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: convertLexicalToHTML({ data: petition.form.consent.text }),
|
||||
__html: consentHTML,
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
|
|
@ -117,7 +116,11 @@ export const PetitionForm = ({ id, petition }: PetitionFormProps) => {
|
|||
{methods.formState.errors.root.message}
|
||||
</FieldDescription>
|
||||
) : null}
|
||||
<Button type="submit" disabled={isSubmitting}>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
className="text-primary-foreground hover:text-primary-foreground"
|
||||
>
|
||||
{isSubmitting ? 'Enviando...' : 'Assinar'}
|
||||
</Button>
|
||||
</Field>
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ function Input({ className, type, ...props }: ComponentProps<'input'>) {
|
|||
type={type}
|
||||
data-slot="input"
|
||||
className={cn(
|
||||
'h-8 w-full min-w-0 rounded-full border border-white/80 bg-white p-4 py-5 text-base text-black transition-colors outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-black placeholder:text-black/80 focus-visible:border-primary focus-visible:ring-3 focus-visible:ring-primary/30 disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-white/70 disabled:text-black/70 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm',
|
||||
'h-8 w-full min-w-0 rounded-full border border-[var(--field-border)] bg-[var(--field-background)] p-4 py-5 text-base text-[var(--field-foreground)] transition-colors outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-[var(--field-foreground)] placeholder:text-[var(--field-placeholder)] focus-visible:border-primary focus-visible:ring-3 focus-visible:ring-primary/30 disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-[var(--field-disabled-background)] disabled:text-[var(--field-disabled-foreground)] disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
|
|
|||
|
|
@ -20,11 +20,11 @@ function NativeSelect({ className, size = 'default', ...props }: NativeSelectPro
|
|||
<select
|
||||
data-slot="native-select"
|
||||
data-size={size}
|
||||
className="w-full min-w-0 appearance-none rounded-full border border-white/80 bg-white px-4 py-2 text-sm text-black transition-colors outline-none select-none selection:bg-primary selection:text-primary-foreground placeholder:text-black/80 focus-visible:border-primary focus-visible:ring-3 focus-visible:ring-primary/30 disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-white/70 disabled:text-black/70 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 data-[size=sm]:h-7 data-[size=sm]:rounded-[min(var(--radius-md),10px)] data-[size=sm]:py-0.5"
|
||||
className="w-full min-w-0 appearance-none rounded-full border border-[var(--field-border)] bg-[var(--field-background)] px-4 py-2 text-sm text-[var(--field-foreground)] transition-colors outline-none select-none selection:bg-primary selection:text-primary-foreground placeholder:text-[var(--field-placeholder)] focus-visible:border-primary focus-visible:ring-3 focus-visible:ring-primary/30 disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-[var(--field-disabled-background)] disabled:text-[var(--field-disabled-foreground)] aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 data-[size=sm]:h-7 data-[size=sm]:rounded-[min(var(--radius-md),10px)] data-[size=sm]:py-0.5"
|
||||
{...props}
|
||||
/>
|
||||
<ChevronDownIcon
|
||||
className="pointer-events-none absolute top-1/2 right-2.5 size-4 -translate-y-1/2 text-black/60 select-none"
|
||||
className="pointer-events-none absolute top-1/2 right-2.5 size-4 -translate-y-1/2 text-[var(--field-icon)] select-none"
|
||||
aria-hidden="true"
|
||||
data-slot="native-select-icon"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -37,14 +37,14 @@ function SelectTrigger({
|
|||
data-slot="select-trigger"
|
||||
data-size={size}
|
||||
className={cn(
|
||||
"flex w-fit items-center justify-between gap-1.5 rounded-lg border border-white/80 bg-white py-2 pr-2 pl-2.5 text-sm whitespace-nowrap text-black transition-colors outline-none select-none focus-visible:border-primary focus-visible:ring-3 focus-visible:ring-primary/30 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 data-placeholder:text-black/50 data-[size=default]:h-8 data-[size=sm]:h-7 data-[size=sm]:rounded-[min(var(--radius-md),10px)] *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-1.5 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
"flex w-fit items-center justify-between gap-1.5 rounded-lg border border-[var(--field-border)] bg-[var(--field-background)] py-2 pr-2 pl-2.5 text-sm whitespace-nowrap text-[var(--field-foreground)] transition-colors outline-none select-none focus-visible:border-primary focus-visible:ring-3 focus-visible:ring-primary/30 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 data-placeholder:text-[var(--field-placeholder)] data-[size=default]:h-8 data-[size=sm]:h-7 data-[size=sm]:rounded-[min(var(--radius-md),10px)] *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-1.5 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<SelectPrimitive.Icon asChild>
|
||||
<ChevronDownIcon className="pointer-events-none size-4 text-black/60" />
|
||||
<ChevronDownIcon className="pointer-events-none size-4 text-[var(--field-icon)]" />
|
||||
</SelectPrimitive.Icon>
|
||||
</SelectPrimitive.Trigger>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ function Textarea({ className, ...props }: React.ComponentProps<'textarea'>) {
|
|||
<textarea
|
||||
data-slot="textarea"
|
||||
className={cn(
|
||||
'flex field-sizing-content min-h-16 w-full rounded-xl border border-white/80 bg-white px-4 py-2 text-base text-black transition-colors outline-none placeholder:text-black/80 focus-visible:border-primary focus-visible:ring-3 focus-visible:ring-primary/30 disabled:cursor-not-allowed disabled:bg-white/70 disabled:text-black/70 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm',
|
||||
'flex field-sizing-content min-h-16 w-full rounded-xl border border-[var(--field-border)] bg-[var(--field-background)] px-4 py-2 text-base text-[var(--field-foreground)] transition-colors outline-none placeholder:text-[var(--field-placeholder)] focus-visible:border-primary focus-visible:ring-3 focus-visible:ring-primary/30 disabled:cursor-not-allowed disabled:bg-[var(--field-disabled-background)] disabled:text-[var(--field-disabled-foreground)] disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
|
|
|||
|
|
@ -43,6 +43,8 @@ export default buildConfig({
|
|||
pool: {
|
||||
connectionString: process.env.DATABASE_URL || '',
|
||||
},
|
||||
// Local dev can point at the shared production DB; never auto-push schema changes.
|
||||
push: false,
|
||||
}),
|
||||
sharp,
|
||||
plugins: [
|
||||
|
|
|
|||
55
tests/int/petition-page-layout.int.spec.ts
Normal file
55
tests/int/petition-page-layout.int.spec.ts
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
import { readFileSync } from 'node:fs'
|
||||
import { resolve } from 'node:path'
|
||||
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
describe('petition page layout', () => {
|
||||
const pageSource = readFileSync(
|
||||
resolve(process.cwd(), 'src/app/(frontend)/abaixo-assinado/[id]/page.tsx'),
|
||||
'utf8',
|
||||
)
|
||||
const formSource = readFileSync(resolve(process.cwd(), 'src/components/PetitionForm.tsx'), 'utf8')
|
||||
const stylesSource = readFileSync(
|
||||
resolve(process.cwd(), 'src/app/(frontend)/styles.css'),
|
||||
'utf8',
|
||||
)
|
||||
const inputSource = readFileSync(resolve(process.cwd(), 'src/components/ui/input.tsx'), 'utf8')
|
||||
const textareaSource = readFileSync(
|
||||
resolve(process.cwd(), 'src/components/ui/textarea.tsx'),
|
||||
'utf8',
|
||||
)
|
||||
const nativeSelectSource = readFileSync(
|
||||
resolve(process.cwd(), 'src/components/ui/native-select.tsx'),
|
||||
'utf8',
|
||||
)
|
||||
|
||||
it('uses the main element as the vertical scroll container', () => {
|
||||
const mainClass = pageSource.match(/<main[\s\S]*?className="([^"]+)"/)?.[1]
|
||||
|
||||
expect(mainClass?.split(/\s+/)).toEqual(
|
||||
expect.arrayContaining(['h-screen', 'w-screen', 'overflow-y-auto']),
|
||||
)
|
||||
})
|
||||
|
||||
it('scopes the petition color system to the petition page', () => {
|
||||
expect(pageSource).toContain('data-theme="petition"')
|
||||
expect(stylesSource).toContain('[data-theme="petition"]')
|
||||
expect(stylesSource).toContain('--petition-hero')
|
||||
expect(stylesSource).toContain('--petition-form-section')
|
||||
})
|
||||
|
||||
it('keeps petition form copy and consent HTML data-driven', () => {
|
||||
expect(formSource).toContain('petition.form.subtitle')
|
||||
expect(formSource).not.toContain('manter a orla de Salvador')
|
||||
expect(pageSource).toContain('consentHTML')
|
||||
expect(formSource).not.toContain("convertLexicalToHTML")
|
||||
})
|
||||
|
||||
it('uses semantic field color variables for shared form controls', () => {
|
||||
for (const source of [inputSource, textareaSource, nativeSelectSource]) {
|
||||
expect(source).toContain('--field-background')
|
||||
expect(source).toContain('--field-foreground')
|
||||
expect(source).toContain('--field-border')
|
||||
}
|
||||
})
|
||||
})
|
||||
Loading…
Reference in a new issue