Including page mandato-no-whatsapp

This commit is contained in:
Francisco Ignácio Fontoura Solla 2026-03-14 18:07:40 -03:00
parent 81da3dc0d7
commit f804da60eb
46 changed files with 4005 additions and 606 deletions

View file

@ -1,12 +1,12 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "radix-vega",
"style": "radix-nova",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "tailwind.config.ts",
"css": "src/app/(frontend)/styles.css",
"baseColor": "neutral",
"baseColor": "mist",
"cssVariables": true,
"prefix": ""
},

View file

@ -57,9 +57,11 @@
"next": "^15.5.12",
"next-themes": "^0.4.6",
"payload": "3.79.0",
"radix-ui": "^1.4.3",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"react-hook-form": "^7.71.1",
"shadcn": "^4.0.3",
"sharp": "0.34.2",
"tailwind-merge": "^3.4.1",
"tailwindcss-animate": "^1.0.7",

File diff suppressed because it is too large Load diff

BIN
public/BG.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 851 KiB

BIN
public/BG.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

View file

@ -0,0 +1,121 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 26.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 430.9 300.5" style="enable-background:new 0 0 430.9 300.5;" xml:space="preserve">
<style type="text/css">
.st0{clip-path:url(#SVGID_00000150086319665657123860000010957758542566882977_);fill:#FFFFFF;}
.st1{clip-path:url(#SVGID_00000150086319665657123860000010957758542566882977_);fill-rule:evenodd;clip-rule:evenodd;fill:#FFFFFF;}
</style>
<g>
<defs>
<rect id="SVGID_1_" x="12.1" y="49.4" width="430.9" height="300.5"/>
</defs>
<clipPath id="SVGID_00000109724154965777870510000010313207867915096511_">
<use xlink:href="#SVGID_1_" style="overflow:visible;"/>
</clipPath>
<path style="clip-path:url(#SVGID_00000109724154965777870510000010313207867915096511_);fill:#FFFFFF;" d="M171.2,181.5
c0-0.2,0.1-0.3,0.3-0.3h1.6c2.8,0,3.7,1.6,3.7,3.3c0,0.8-0.3,1.6-0.8,2.2c-0.5,0.6-1.5,1-2.8,1h-1.6c-0.2,0-0.3-0.1-0.3-0.3V181.5z
M172.5,182v4.7h0.7c1.7,0,2.3-1.1,2.3-2.3c0-1.2-0.6-2.4-2.3-2.4H172.5z"/>
<path style="clip-path:url(#SVGID_00000109724154965777870510000010313207867915096511_);fill:#FFFFFF;" d="M178.5,181.5
c0-0.2,0.1-0.3,0.3-0.3h3.5c0.2,0,0.3,0.1,0.3,0.2v0.5c0,0.2-0.1,0.2-0.3,0.2h-2.5v1.7h2c0.2,0,0.3,0.1,0.3,0.2v0.5
c0,0.2-0.1,0.2-0.3,0.2h-2v1.8h2.5c0.2,0,0.3,0.1,0.3,0.2v0.5c0,0.2-0.1,0.2-0.3,0.2h-3.5c-0.2,0-0.3-0.1-0.3-0.3V181.5z"/>
<path style="clip-path:url(#SVGID_00000109724154965777870510000010313207867915096511_);fill:#FFFFFF;" d="M184.3,181.5
c0-0.2,0.1-0.3,0.3-0.3h2c0.8,0,1.4,0.2,1.8,0.6c0.4,0.4,0.6,0.9,0.6,1.6c0,1.3-0.9,2.1-2.5,2.1h-1v1.9c0,0.2-0.1,0.3-0.3,0.3h-0.6
c-0.2,0-0.3-0.1-0.3-0.3V181.5z M185.6,182.1v2.3h0.8c1,0,1.5-0.4,1.5-1.1c0-0.7-0.5-1.2-1.5-1.2H185.6z"/>
<path style="clip-path:url(#SVGID_00000109724154965777870510000010313207867915096511_);fill:#FFFFFF;" d="M194.6,181.5
c0-0.2,0.1-0.3,0.3-0.3h0.6c0.2,0,0.3,0.1,0.3,0.3v3.8c0,1.3-0.6,2.5-2.7,2.5c-1,0-1.7-0.3-2.1-0.8c-0.4-0.5-0.5-1.1-0.5-1.8v-3.8
c0-0.2,0.1-0.3,0.3-0.3h0.6c0.2,0,0.3,0.1,0.3,0.3v3.8c0,1,0.5,1.5,1.3,1.5c0.9,0,1.4-0.5,1.4-1.5V181.5z"/>
<path style="clip-path:url(#SVGID_00000109724154965777870510000010313207867915096511_);fill:#FFFFFF;" d="M200.5,182.1v5.2
c0,0.2-0.1,0.3-0.3,0.3h-0.6c-0.2,0-0.3-0.1-0.3-0.3v-5.2h-1.6c-0.2,0-0.3-0.1-0.3-0.2v-0.5c0-0.2,0.1-0.2,0.3-0.2h4.5
c0.2,0,0.3,0.1,0.3,0.2v0.5c0,0.2-0.1,0.2-0.3,0.2H200.5z"/>
<path style="clip-path:url(#SVGID_00000109724154965777870510000010313207867915096511_);fill:#FFFFFF;" d="M208.4,187.6
c-0.3,0-0.3-0.1-0.4-0.4l-0.4-1.3H205l-0.4,1.3c-0.1,0.3-0.2,0.4-0.5,0.4h-0.6c-0.2,0-0.3-0.1-0.2-0.4l2.2-5.7
c0.1-0.3,0.2-0.4,0.6-0.4h0.6c0.3,0,0.4,0.1,0.5,0.4l2.1,5.7c0.1,0.2,0,0.4-0.2,0.4H208.4z M206.2,182.2l-0.9,2.8h1.9L206.2,182.2z
"/>
<path style="clip-path:url(#SVGID_00000109724154965777870510000010313207867915096511_);fill:#FFFFFF;" d="M210.7,181.5
c0-0.2,0.1-0.3,0.3-0.3h1.6c2.8,0,3.7,1.6,3.7,3.3c0,0.8-0.3,1.6-0.8,2.2c-0.5,0.6-1.5,1-2.8,1H211c-0.2,0-0.3-0.1-0.3-0.3V181.5z
M212,182v4.7h0.7c1.7,0,2.3-1.1,2.3-2.3c0-1.2-0.6-2.4-2.3-2.4H212z"/>
<path style="clip-path:url(#SVGID_00000109724154965777870510000010313207867915096511_);fill:#FFFFFF;" d="M221,187.7
c-2.6,0-3.3-1.8-3.3-3.3c0-0.7,0.1-1.2,0.4-1.9c0.2-0.3,0.3-0.6,0.6-0.8c0.5-0.4,1.3-0.7,2.3-0.7c1.6,0,2.4,0.7,2.9,1.5
c0.3,0.6,0.4,1.2,0.4,1.9C224.3,186,223.6,187.7,221,187.7 M221,186.7c1.7,0,2-1.2,2-2.3c0-1.1-0.4-2.4-2-2.4
c-1.7,0-2.1,1.3-2.1,2.4C218.9,185.5,219.3,186.7,221,186.7"/>
<path style="clip-path:url(#SVGID_00000109724154965777870510000010313207867915096511_);fill:#FFFFFF;" d="M228.7,181.5
c0-0.2,0.1-0.3,0.3-0.3h3.3c0.2,0,0.3,0.1,0.3,0.2v0.5c0,0.2-0.1,0.2-0.3,0.2H230v1.8h1.9c0.2,0,0.3,0.1,0.3,0.2v0.5
c0,0.2-0.1,0.2-0.3,0.2H230v2.4c0,0.2-0.1,0.3-0.3,0.3H229c-0.2,0-0.3-0.1-0.3-0.3V181.5z"/>
<path style="clip-path:url(#SVGID_00000109724154965777870510000010313207867915096511_);fill:#FFFFFF;" d="M234.3,181.5
c0-0.2,0.1-0.3,0.3-0.3h3.5c0.2,0,0.3,0.1,0.3,0.2v0.5c0,0.2-0.1,0.2-0.3,0.2h-2.5v1.7h2c0.2,0,0.3,0.1,0.3,0.2v0.5
c0,0.2-0.1,0.2-0.3,0.2h-2v1.8h2.5c0.2,0,0.3,0.1,0.3,0.2v0.5c0,0.2-0.1,0.2-0.3,0.2h-3.5c-0.2,0-0.3-0.1-0.3-0.3V181.5z"/>
<path style="clip-path:url(#SVGID_00000109724154965777870510000010313207867915096511_);fill:#FFFFFF;" d="M240.1,181.5
c0-0.2,0.1-0.3,0.3-0.3h1.6c2.8,0,3.7,1.6,3.7,3.3c0,0.8-0.3,1.6-0.8,2.2c-0.6,0.6-1.5,1-2.8,1h-1.6c-0.2,0-0.3-0.1-0.3-0.3V181.5z
M241.4,182v4.7h0.7c1.7,0,2.3-1.1,2.3-2.3c0-1.2-0.6-2.4-2.3-2.4H241.4z"/>
<path style="clip-path:url(#SVGID_00000109724154965777870510000010313207867915096511_);fill:#FFFFFF;" d="M247.4,181.5
c0-0.2,0.1-0.3,0.3-0.3h3.5c0.2,0,0.3,0.1,0.3,0.2v0.5c0,0.2-0.1,0.2-0.3,0.2h-2.5v1.7h2c0.2,0,0.3,0.1,0.3,0.2v0.5
c0,0.2-0.1,0.2-0.3,0.2h-2v1.8h2.5c0.2,0,0.3,0.1,0.3,0.2v0.5c0,0.2-0.1,0.2-0.3,0.2h-3.5c-0.2,0-0.3-0.1-0.3-0.3V181.5z"/>
<path style="clip-path:url(#SVGID_00000109724154965777870510000010313207867915096511_);fill:#FFFFFF;" d="M257.4,187.6
c-0.3,0-0.4-0.1-0.5-0.4l-1.3-2.1h-1v2.1c0,0.2-0.1,0.3-0.3,0.3h-0.6c-0.2,0-0.3-0.1-0.3-0.3v-5.9c0-0.2,0.1-0.3,0.3-0.3h2
c1.6,0,2.4,0.8,2.4,2.1c0,0.8-0.5,1.5-1.3,1.8l1.5,2.3c0.2,0.2,0.1,0.4-0.1,0.4H257.4z M254.5,182.1v2.2h0.8c0.9,0,1.4-0.4,1.4-1.1
c0-0.7-0.5-1.1-1.4-1.1H254.5z"/>
<path style="clip-path:url(#SVGID_00000109724154965777870510000010313207867915096511_);fill-rule:evenodd;clip-rule:evenodd;fill:#FFFFFF;" d="
M235.6,148.3c-3,2.1-6,4.3-9,6.5c0.6,1.3,1.7,2.7,0.1,4.2c0.6,0,1,0.1,1.2,0.1c0.5,1.6,1,3.1,1.5,4.8c-0.4,0.2-1,0.5-1.7,0.9
c-0.1-0.8-0.2-1.4-0.3-2.1c-0.6,0.2-1.1,0.6-1.7,0.6c-0.2,0-0.4-0.8-0.7-1.4h-1.2c0.4-0.9,0.7-1.6,1.1-2.5c-0.6,0.3-1,0.6-1.4,0.7
c-0.7,0.2-1.7,1.4-1.7-0.6c0-0.1-1.2-0.3-1.3-0.2c-0.7,1.9-2.7,2.1-4.1,3.2c-0.6,0.4-1.2,0.8-1.9,1.1c-0.5,0.2-1.1,0.2-1.6,0.2
c0.3-0.5,0.5-1,1-1.3c1.7-1.3,3.6-2.6,5.3-3.8c0.3-0.2,0.6-0.5,0.7-1c-1.8,1-3.7,2.1-5.5,3.1c-0.4,0.2-1,0.2-1.5,0.3
c0.3-0.5,0.4-1,0.8-1.4c0.8-0.7,1.8-1.3,2.6-2.2c-0.4,0.2-0.8,0.4-1.3,0.6c-0.3,0.1-0.6,0-1,0c0.1-0.4,0.1-0.8,0.3-1
c0.3-0.4,0.7-0.6,1.1-0.9c-1.1-0.9-1.1-0.9,0.6-2.8c-0.5,0.2-0.9,0.5-1.4,0.7c-0.4,0.1-0.9,0.1-1.4,0.1c0.2-0.4,0.4-0.8,0.6-1.2
c0.1-0.2,0.3-0.3,0.5-0.4c-0.1-0.5-0.2-1-0.2-1.5c-1.1,0.6-2.2,1.2-3.3,1.8c-0.1-0.1-0.3-0.1-0.4-0.2c0.2-0.7,0.3-1.4,0.5-2.3
c-1.3,1-2.4,0.9-3.4-0.5c-0.2-0.3-0.9-0.3-1.4-0.4v-0.9c1.6-1,2.3,0.4,3.2,1.2c0.8-0.4,1.4-0.9,2.1-1.2c0.6-0.3,1.3-0.4,1.9-0.6
c-0.1,0.6-0.3,1.2-0.4,1.8c0,0.1,0.1,0.2,0.1,0.4c1.4-0.8,2.8-1.6,4.2-2.4c0.2-0.1,0.4-0.3,0.4-0.6c0.5-1.4,1-2.9,1.5-4.5
c-0.5-0.7,0.3-3.7,1.6-4.9h0.6v2.1c2.4,0.4,0,1.6,0.2,2.4c0.8,0.1,1.5,0.2,0.7,1.3c-0.1,0.1,0.3,0.6,0.6,0.9
c0.2,0.2,0.6,0.2,0.9,0.3c-0.2,0.3-0.3,0.7-0.5,1c-0.4,0.4-0.9,0.8-1.4,1.1c0.2,0.5,0.4,0.8,0.5,1c0.8-0.1,1.4-0.2,2-0.3
c-0.2,0.6-0.5,1.2-0.8,2c0.7-0.4,1.4-0.8,2.1-1.1c0.6-0.2,1.2-0.3,1.9-0.5c-0.2,0.6-0.5,1.1-0.8,1.9c0.8-0.4,1.4-0.9,2.1-1.2
c0.6-0.2,1.3-0.3,1.9-0.5c-0.4,0.6-0.7,1.2-1.2,1.6c-0.6,0.6-1.3,1-2,1.5c0.1,0.1,0.1,0.2,0.2,0.3c2-1.2,4-2.4,6.1-3.4
c0.5-0.2,1.1,0,1.7,0V148.3z"/>
<path style="clip-path:url(#SVGID_00000109724154965777870510000010313207867915096511_);fill:#FFFFFF;" d="M73.4,134.9
c1.1,0,1.7,0.5,1.6,1.6v19.7c0,7.8-4.6,12.2-12.2,12.2c-3.6,0-6.5-0.7-8.5-2c-1.3-0.9-1.9-2-1.9-3.3c0-0.9,0.5-1.8,1.3-1.8
c0.1,0,1,0.3,2.7,0.9c1.7,0.6,3.3,0.9,4.9,0.9c4.4,0,7.3-2.2,7.3-6.9v-16.2h-7.7c-0.9,0-1.3-0.4-1.3-1.1v-2.7
c0-0.8,0.5-1.2,1.3-1.2H73.4z"/>
<path style="clip-path:url(#SVGID_00000109724154965777870510000010313207867915096511_);fill:#FFFFFF;" d="M97.5,168.2
c-13.3,0-16.6-8.9-16.6-16.7c0-3.4,0.5-6.3,2-9.4c0.8-1.5,1.7-2.9,3-4c2.4-2.2,6.5-3.7,11.6-3.7c8,0,12.3,3.3,14.6,7.7
c1.5,3.1,2,6,2,9.4C114,159.3,110.7,168.1,97.5,168.2 M97.5,163.1c8.4,0,10.3-6.1,10.3-11.7c0-5.5-1.9-11.9-10.3-11.9
c-8.4,0-10.4,6.4-10.4,11.9C87.1,157.1,89.1,163.1,97.5,163.1"/>
<path style="clip-path:url(#SVGID_00000109724154965777870510000010313207867915096511_);fill:#FFFFFF;" d="M141,167.7
c-1.5,0-2-0.3-2.7-1.8l-6.5-10.5h-5.1v10.7c0,1.1-0.4,1.6-1.6,1.6h-3.2c-1.2,0-1.6-0.5-1.6-1.6v-29.6c0-1.2,0.4-1.6,1.6-1.6H132
c7.8,0,12.1,4.2,12.1,10.4c0,4.2-2.5,7.5-6.6,9.1l7.6,11.5c0.8,1.2,0.5,1.8-0.7,1.8H141z M126.6,139.6v11h4.2c4.6,0,7.1-2,7.1-5.3
c0-3.6-2.4-5.7-7.1-5.7H126.6z"/>
<path style="clip-path:url(#SVGID_00000109724154965777870510000010313207867915096511_);fill:#FFFFFF;" d="M179.6,164.7
c0,1.1-1.2,2-3.7,2.7c-2.5,0.7-5.5,1-9,1c-5.4,0-9.6-1.5-12.7-4.4c-3.1-2.9-4.7-7.1-4.7-12.5c0-5.3,1.5-9.5,4.6-12.4
c3.1-3,7.4-4.4,12.9-4.4c3.9,0,6.9,0.5,9.2,1.7c1.8,0.8,2.6,1.9,2.6,3.2c0,1-0.5,2.1-1.3,2.1c0,0-1.2-0.3-3.3-0.9
c-2.1-0.6-4.3-0.9-6.3-0.9c-7.6,0-12.1,3.7-12.1,11.6c0,8.1,4.6,11.6,11.9,11.6c1.7,0,3.5-0.2,5.5-0.7v-7.1h-6.3
c-1,0-1.4-0.4-1.4-1.1v-2.7c0-0.8,0.5-1.1,1.5-1.1H178c1,0,1.5,0.5,1.5,1.6V164.7z"/>
<path style="clip-path:url(#SVGID_00000109724154965777870510000010313207867915096511_);fill:#FFFFFF;" d="M186.1,136.5
c0-1.2,0.4-1.6,1.7-1.6h17.4c0.9,0,1.4,0.4,1.4,1.1v2.7c0,0.8-0.5,1.2-1.4,1.2h-12.6v8.7h10c0.9,0,1.4,0.4,1.4,1.1v2.7
c0,0.8-0.5,1.2-1.4,1.2h-10v9.1h12.6c0.9,0,1.4,0.4,1.4,1.1v2.7c0,0.8-0.5,1.2-1.4,1.2h-17.4c-1.2,0-1.7-0.5-1.7-1.6V136.5z"/>
<path style="clip-path:url(#SVGID_00000109724154965777870510000010313207867915096511_);fill:#FFFFFF;" d="M236.6,161.4
c0.5,0,4.5,2.3,9.1,2.3c4.9,0,7.1-2,7.1-4.9c0-4.3-5.7-5.3-10.3-7.1c-3.1-1.2-5.6-3.9-5.6-8c0-6.8,5.7-9.1,11.2-9.1
c3.1,0,5.6,0.5,7.4,1.5c1.7,0.9,2.5,2,2.5,3.4c0,1.2-0.4,1.8-1.1,1.8c-0.6,0-4-1.9-7.9-1.9c-4,0-6,1.3-6,3.9c0,7,15.9,3,15.9,15.2
c0,6.6-4.5,9.9-12.3,9.9c-4,0-7-0.6-9-1.9c-1.3-0.8-1.9-1.9-1.9-3.1C235.6,162.1,235.9,161.4,236.6,161.4"/>
<path style="clip-path:url(#SVGID_00000109724154965777870510000010313207867915096511_);fill:#FFFFFF;" d="M279.7,168.2
c-13.3,0-16.6-8.9-16.6-16.7c0-3.4,0.5-6.3,2-9.4c0.8-1.5,1.7-2.9,3-4c2.4-2.2,6.5-3.7,11.6-3.7c8,0,12.3,3.3,14.6,7.7
c1.5,3.1,2,6,2,9.4C296.2,159.3,292.9,168.1,279.7,168.2 M279.7,163.1c8.4,0,10.3-6.1,10.3-11.7c0-5.5-1.9-11.9-10.3-11.9
c-8.4,0-10.4,6.4-10.4,11.9C269.3,157.1,271.3,163.1,279.7,163.1"/>
<path style="clip-path:url(#SVGID_00000109724154965777870510000010313207867915096511_);fill:#FFFFFF;" d="M302.4,136.5
c0-1.2,0.4-1.6,1.6-1.6h3.2c1.2,0,1.6,0.5,1.6,1.6v25.8h10.7c0.9,0,1.4,0.4,1.4,1.1v3c0,0.8-0.5,1.2-1.4,1.2h-15.6
c-1.2,0-1.6-0.4-1.6-1.6V136.5z"/>
<path style="clip-path:url(#SVGID_00000109724154965777870510000010313207867915096511_);fill:#FFFFFF;" d="M326.3,136.5
c0-1.2,0.4-1.6,1.6-1.6h3.2c1.2,0,1.6,0.5,1.6,1.6v25.8h10.7c0.9,0,1.4,0.4,1.4,1.1v3c0,0.8-0.5,1.2-1.4,1.2h-15.6
c-1.2,0-1.6-0.4-1.6-1.6V136.5z"/>
<path style="clip-path:url(#SVGID_00000109724154965777870510000010313207867915096511_);fill:#FFFFFF;" d="M372.8,167.7
c-1.4,0-1.7-0.3-2.2-1.8l-2.2-6.5h-12.8l-2.1,6.5c-0.5,1.5-0.9,1.8-2.3,1.8H348c-1.2,0-1.6-0.6-1.1-1.8l10.9-28.9
c0.7-1.6,1.2-2.1,2.9-2.1h3c1.6,0,2.2,0.5,2.8,2.1l10.8,28.9c0.5,1.2,0.1,1.8-1.1,1.8H372.8z M362,140.2l-4.7,14.2h9.5L362,140.2z"
/>
<path style="clip-path:url(#SVGID_00000109724154965777870510000010313207867915096511_);fill:#FFFFFF;" d="M264.4,187.6
c-0.3,0-0.3-0.1-0.4-0.4l-0.4-1.3H261l-0.4,1.3c-0.1,0.3-0.2,0.4-0.5,0.4h-0.6c-0.2,0-0.3-0.1-0.2-0.4l2.2-5.7
c0.1-0.3,0.2-0.4,0.6-0.4h0.6c0.3,0,0.4,0.1,0.5,0.4l2.1,5.7c0.1,0.2,0,0.4-0.2,0.4H264.4z M262.3,182.2l-0.9,2.8h1.9L262.3,182.2z
"/>
<path style="clip-path:url(#SVGID_00000109724154965777870510000010313207867915096511_);fill:#FFFFFF;" d="M266.8,181.5
c0-0.2,0.1-0.3,0.3-0.3h0.6c0.2,0,0.3,0.1,0.3,0.3v5.1h2.1c0.2,0,0.3,0.1,0.3,0.2v0.6c0,0.2-0.1,0.2-0.3,0.2h-3.1
c-0.2,0-0.3-0.1-0.3-0.3V181.5z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 12 KiB

BIN
public/LULA_E_SOLLA.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 859 KiB

BIN
public/SOLLA (1).png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 559 KiB

BIN
public/SOLLA.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 998 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 280 KiB

View file

@ -0,0 +1,16 @@
import { Footer } from '@/components/Footer'
import { Header } from '@/components/Header'
import React from 'react'
import '../styles.css'
export default async function RootLayout(props: { children: React.ReactNode }) {
const { children } = props
return (
<>
<Header />
{children}
<Footer />
</>
)
}

View file

@ -15,10 +15,15 @@ export default async function HomePage() {
}
return (
// Main element occupies the whole screen width and the height of screen minus height of header
<main className="w-screen h-screen overflow-hidden">
<main className="w-screen">
{image?.url ? (
<Image src={image.url} alt={image.alt} width={image.width!} height={image.height!} />
<Image
className="w-screen"
src={image.url}
alt={image.alt}
width={image.width!}
height={image.height!}
/>
) : null}
</main>
)

View file

@ -9,24 +9,53 @@ type SubmitPetitionSignatureInput = PetitionFormInput & {
consentId: number
}
export const submitPetitionSignature = async (input: SubmitPetitionSignatureInput) => {
const { petitionId, consentId, ...contactInput } = input
export const submitPetitionSignature = async ({
petitionId,
consentId,
comment,
...contactInput
}: SubmitPetitionSignatureInput) => {
const data = petitionFormSchema.parse(contactInput)
const payload = await getPayload({ config })
const transactionID = await payload.db.beginTransaction()
if (!transactionID) {
throw new Error('Failed to begin transaction')
}
try {
const contact = await payload.create({
collection: 'contact',
data,
req: { transactionID },
})
await payload.create({
await Promise.all([
payload.create({
collection: 'signature',
data: {
contact: contact.id,
petition: petitionId,
consent: consentId,
comment,
},
})
req: { transactionID },
}),
payload.create({
collection: 'subscription',
data: {
contact: contact.id,
consent: consentId,
},
req: { transactionID },
}),
])
return { ok: true as const }
await payload.db.commitTransaction(transactionID)
return { ok: true }
} catch (error) {
await payload.db.rollbackTransaction(transactionID)
throw error
}
}

View file

@ -0,0 +1,41 @@
'use server'
import config from '@payload-config'
import { getPayload } from 'payload'
import { WhatsAppFormInput, whatsAppFormSchema } from '@/lib/schemas/whatsapp-form'
export const submitWhatsapp = async (input: WhatsAppFormInput) => {
const { comment, ...contactInput } = whatsAppFormSchema.parse(input)
const payload = await getPayload({ config })
const transactionID = await payload.db.beginTransaction()
if (!transactionID) {
throw new Error('failed to start transaction')
}
try {
const contact = await payload.create({
collection: 'contact',
data: contactInput,
req: { transactionID },
})
await payload.create({
collection: 'subscription',
data: {
contact: contact.id,
comment,
},
req: { transactionID },
})
await payload.db.commitTransaction(transactionID)
return { ok: true }
} catch (error) {
await payload.db.rollbackTransaction(transactionID)
throw error
}
}

View file

@ -1,5 +1,3 @@
import { Footer } from '@/components/Footer'
import { Header } from '@/components/Header'
import { getCachedDocumentById } from '@/utilities/documents'
import { getCachedGlobal } from '@/utilities/globals'
import { RefreshRouteOnSave } from '@/utilities/RefreshRouteOnSave'
@ -68,20 +66,18 @@ export default async function RootLayout(props: { children: React.ReactNode }) {
const { children } = props
return (
<html lang="pt-BR" className={inter.variable} suppressHydrationWarning>
<body className="antialiased">
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
<html
lang="pt-BR"
className={`${inter.variable} w-screen h-screen scrollbar-hide`}
style={{ colorScheme: 'light' }}
suppressHydrationWarning
>
<body className="antialiased w-screen scrollbar-hide">
<ThemeProvider attribute="class">
<SpeedInsights />
<Analytics />
<RefreshRouteOnSave />
<Header />
{children}
<Footer />
</ThemeProvider>
</body>
</html>

View file

@ -0,0 +1,48 @@
import { SocialLinks } from '@/components/socialLinks'
import { WhatsappForm } from '@/components/WhatsappForm'
import Image from 'next/image'
export default function Page() {
return (
<main className="w-screen h-screen bg-[url(/BG.jpg)] bg-center bg-cover flex flex-col items-center justify-center">
<div className="flex flex-col max-w-300 gap-12">
<div className="relative flex flex-col gap-5">
<h1 className="text-5xl text-left">
FIQUE POR DENTRO
<br />
DO <span className="text-[#f9844e]">NOSSO MANDATO!</span>
</h1>
<h2 className="text-2xl font-light">
PARTICIPE DA NOSSA{' '}
<strong className="font-bold">
COMUNIDADE EXCLUSIVA
<br />
NO WHATSAPP
</strong>{' '}
E FIQUE POR DENTRO DE TODAS
<br />
AS VOTAÇÕES, PROJETOS E BASTIDORES.
</h2>
<WhatsappForm classname="flex-1 w-[40%]" />
<Image
className="flex-[1.8] -mx-10 absolute top-1/2 -translate-y-[50%] right-0 w-[67%]"
src="/LULA_E_SOLLA.png"
width={956}
height={948}
alt=""
/>
</div>
<div className="relative flex flex-col items-center gap-0">
<Image src="LOGO_SOLLA_BRANCO.svg" width={430} height={300} alt="" className="-my-30" />
<p className="text-center">
<strong>Seus dados estão seguros.</strong> Utilizamos suas informações apenas para
contato e envio de conteúdos relacionados.
<br />
Não compartilhamos dados com terceiros. Ao preencher o formulário, você concorda com
nossa política de uso de dados conforme a LGPD.
</p>
</div>
</div>
</main>
)
}

View file

@ -2,6 +2,8 @@
@plugin 'tailwindcss-animate';
@import 'tw-animate-css';
@import 'tw-animate-css';
@import 'shadcn/tailwind.css';
@custom-variant dark (&:is(.dark *));
@source "../../**/*.{js,ts,jsx,tsx,mdx}";
@ -12,6 +14,7 @@
html {
-webkit-font-smoothing: antialiased;
color-scheme: light;
}
html,
@ -64,72 +67,72 @@ body,
}
:root {
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--card: oklch(0.25 0.025 171);
--card-foreground: oklch(0.98 0.004 197.1);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.58 0.22 27);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--chart-1: oklch(0.809 0.105 251.813);
--chart-2: oklch(0.623 0.214 259.815);
--chart-3: oklch(0.546 0.245 262.881);
--chart-4: oklch(0.488 0.243 264.376);
--chart-5: oklch(0.424 0.199 265.638);
--radius: 0.625rem;
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
--popover-foreground: oklch(0.18 0.01 223.9);
--primary: oklch(0.9 0.14 143);
--primary-foreground: oklch(0.2 0.015 171);
--secondary: oklch(0.31 0.02 171);
--secondary-foreground: oklch(0.98 0.004 197.1);
--muted: oklch(0.29 0.02 171);
--muted-foreground: oklch(0.9 0.01 197.1);
--accent: oklch(0.34 0.025 171);
--accent-foreground: oklch(0.98 0.004 197.1);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.7 0.015 197.1 / 20%);
--input: oklch(0.7 0.015 197.1 / 20%);
--ring: oklch(0.9 0.14 143);
--chart-1: oklch(0.81 0.117 11.638);
--chart-2: oklch(0.645 0.246 16.439);
--chart-3: oklch(0.586 0.253 17.585);
--chart-4: oklch(0.514 0.222 16.935);
--chart-5: oklch(0.455 0.188 13.697);
--radius: 0.875rem;
--sidebar: oklch(0.25 0.025 171);
--sidebar-foreground: oklch(0.98 0.004 197.1);
--sidebar-primary: oklch(0.9 0.14 143);
--sidebar-primary-foreground: oklch(0.2 0.015 171);
--sidebar-accent: oklch(0.31 0.02 171);
--sidebar-accent-foreground: oklch(0.98 0.004 197.1);
--sidebar-border: oklch(0.7 0.015 197.1 / 20%);
--sidebar-ring: oklch(0.9 0.14 143);
--background: oklch(0.2 0.02 171);
--foreground: oklch(0.98 0.004 197.1);
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.205 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.87 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.371 0 0);
--accent-foreground: oklch(0.985 0 0);
--background: oklch(0.148 0.004 228.8);
--foreground: oklch(0.987 0.002 197.1);
--card: oklch(0.218 0.008 223.9);
--card-foreground: oklch(0.987 0.002 197.1);
--popover: oklch(0.218 0.008 223.9);
--popover-foreground: oklch(0.987 0.002 197.1);
--primary: oklch(0.925 0.005 214.3);
--primary-foreground: oklch(0.218 0.008 223.9);
--secondary: oklch(0.275 0.011 216.9);
--secondary-foreground: oklch(0.987 0.002 197.1);
--muted: oklch(0.275 0.011 216.9);
--muted-foreground: oklch(0.723 0.014 214.4);
--accent: oklch(0.275 0.011 216.9);
--accent-foreground: oklch(0.987 0.002 197.1);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
--chart-1: oklch(0.809 0.105 251.813);
--chart-2: oklch(0.623 0.214 259.815);
--chart-3: oklch(0.546 0.245 262.881);
--chart-4: oklch(0.488 0.243 264.376);
--chart-5: oklch(0.424 0.199 265.638);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--ring: oklch(0.56 0.021 213.5);
--chart-1: oklch(0.81 0.117 11.638);
--chart-2: oklch(0.645 0.246 16.439);
--chart-3: oklch(0.586 0.253 17.585);
--chart-4: oklch(0.514 0.222 16.935);
--chart-5: oklch(0.455 0.188 13.697);
--sidebar: oklch(0.218 0.008 223.9);
--sidebar-foreground: oklch(0.987 0.002 197.1);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-primary-foreground: oklch(0.987 0.002 197.1);
--sidebar-accent: oklch(0.275 0.011 216.9);
--sidebar-accent-foreground: oklch(0.987 0.002 197.1);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.556 0 0);
--sidebar-ring: oklch(0.56 0.021 213.5);
}
@layer base {
@ -181,4 +184,19 @@ body,
select {
cursor: pointer;
}
html {
@apply font-sans;
}
}
@layer utilities {
.scrollbar-hide {
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
}
.scrollbar-hide::-webkit-scrollbar {
display: none; /* Chrome, Safari */
}
}

View file

@ -56,11 +56,5 @@ export const Contact: CollectionConfig = {
minLength: 8,
maxLength: 8,
},
{
name: 'comment',
type: 'textarea',
label: 'Comentario',
maxLength: 1000,
},
],
}

View file

@ -31,5 +31,11 @@ export const Signature: CollectionConfig = {
required: true,
relationTo: 'consent',
},
{
name: 'comment',
type: 'textarea',
label: 'Comentario',
maxLength: 1000,
},
],
}

View file

@ -0,0 +1,34 @@
import { CollectionConfig } from 'payload'
export const Subscription: CollectionConfig = {
slug: 'subscription',
labels: {
singular: 'Assinatura',
plural: 'Assinaturas',
},
admin: {
group: 'Contatos',
},
fields: [
{
type: 'relationship',
name: 'contact',
label: 'Contato',
required: true,
relationTo: 'contact',
},
{
type: 'relationship',
name: 'consent',
label: 'Consentimento',
required: true,
relationTo: 'consent',
},
{
name: 'comment',
type: 'textarea',
label: 'Comentario',
maxLength: 1000,
},
],
}

View file

@ -0,0 +1,33 @@
import { useFormContext, useWatch } from 'react-hook-form'
import { NativeSelect, NativeSelectOption } from './ui/native-select'
import { CitiesByState } from '@/lib/cities'
import { useEffect } from 'react'
interface CitySelectProps {
placeholder?: string
}
export const CitySelect = ({ placeholder = 'Selecione uma cidade' }: CitySelectProps) => {
const { register, control, setValue } = useFormContext()
const state: keyof typeof CitiesByState | undefined = useWatch({
control,
name: 'state',
})
const cityOptions = state?.length ? CitiesByState[state] : []
useEffect(() => {
setValue('city', undefined)
}, [state])
return (
<NativeSelect id="city" {...register('city')} disabled={!cityOptions.length} required>
<NativeSelectOption value="">{placeholder}</NativeSelectOption>
{cityOptions.map((city) => (
<NativeSelectOption key={city} value={city}>
{city}
</NativeSelectOption>
))}
</NativeSelect>
)
}

View file

@ -0,0 +1,16 @@
import type { ComponentProps } from 'react'
import { FormInput } from './FormInput'
export const EmailInput = (props: ComponentProps<'input'>) => (
<FormInput
form="email"
id="email"
placeholder="Digite seu e-mail"
autoComplete="email"
inputMode="email"
type="text"
maxLength={254}
required
{...props}
/>
)

View file

@ -0,0 +1,13 @@
import type { ComponentProps } from 'react'
import { Input } from './ui/input'
import { useFormContext } from 'react-hook-form'
interface FormInpuProps extends ComponentProps<'input'> {
form: string
}
export const FormInput = ({ form, ...props }: FormInpuProps) => {
const { register } = useFormContext()
return <Input {...props} {...register(form)} />
}

View file

@ -0,0 +1,15 @@
import type { ComponentProps } from 'react'
import { FormInput } from './FormInput'
export const NameInput = (props: ComponentProps<'input'>) => (
<FormInput
form="name"
id="name"
type="text"
placeholder="Digite seu nome completo"
autoComplete="name"
maxLength={120}
required
{...props}
/>
)

View file

@ -8,17 +8,20 @@ import {
FieldLegend,
FieldSet,
} from '@/components/ui/field'
import { Input } from '@/components/ui/input'
import { Petition } from '@/payload-types'
import { zodResolver } from '@hookform/resolvers/zod'
import { type SubmitHandler, useForm } from 'react-hook-form'
import { NativeSelect, NativeSelectOption } from './ui/native-select'
import { useState, useTransition } from 'react'
import { CitiesByState } from '@/lib/cities'
import { FormProvider, type SubmitHandler, useForm } from 'react-hook-form'
import { useTransition } from 'react'
import { Textarea } from './ui/textarea'
import { Button } from './ui/button'
import { petitionFormSchema, type PetitionFormInput } from '@/lib/schemas/petition-form'
import { submitPetitionSignature } from '@/app/(frontend)/actions/submitPetitionSignature'
import { NameInput } from './NameInput'
import { EmailInput } from './EmailInput'
import { PhoneInput } from './PhoneInput'
import { StateSelect } from './StateSelect'
import { CitySelect } from './CitySelect'
import { PostalCodeInput } from './PostalCodeInput'
interface PetitionFormProps {
id: string
@ -41,19 +44,6 @@ export const PetitionForm = ({ id, petition }: PetitionFormProps) => {
const [isSubmitting, startTransition] = useTransition()
const [cityOptions, setCityOptions] = useState<readonly string[]>([])
const handleStateUpdate = (stateValue?: keyof typeof CitiesByState) => {
if (!stateValue) {
setCityOptions([])
methods.setValue('city', '')
return
}
setCityOptions(CitiesByState[stateValue])
methods.setValue('city', '')
}
const onSubmit: SubmitHandler<PetitionFormInput> = (input) => {
const consentId =
typeof petition.form?.consent === 'number' ? petition.form.consent : petition.form.consent.id
@ -66,7 +56,6 @@ export const PetitionForm = ({ id, petition }: PetitionFormProps) => {
consentId,
})
methods.reset()
setCityOptions([])
} catch {
methods.setError('root', {
message: 'Falha ao enviar assinatura. Tente novamente.',
@ -76,6 +65,7 @@ export const PetitionForm = ({ id, petition }: PetitionFormProps) => {
}
return (
<FormProvider {...methods}>
<form id={id} onSubmit={methods.handleSubmit(onSubmit)}>
<FieldSet>
{petition.form.title ? <FieldLegend>{petition.form.title}</FieldLegend> : null}
@ -87,80 +77,27 @@ export const PetitionForm = ({ id, petition }: PetitionFormProps) => {
<FieldGroup>
<Field>
<FieldLabel htmlFor="name">Nome Completo</FieldLabel>
<Input
id="name"
type="text"
placeholder="Digite seu nome completo"
autoComplete="name"
maxLength={120}
{...methods.register('name')}
required
/>
<NameInput />
</Field>
<Field>
<FieldLabel htmlFor="email">E-mail</FieldLabel>
<Input
id="email"
placeholder="Digite seu e-mail"
autoComplete="email"
inputMode="email"
type="text"
maxLength={254}
{...methods.register('email')}
required
/>
<EmailInput />
</Field>
<Field>
<FieldLabel htmlFor="phone">Celular</FieldLabel>
<Input
id="phone"
placeholder="(71) 99999-9999"
autoComplete="tel"
inputMode="tel"
type="tel"
maxLength={15}
{...methods.register('phone')}
required
/>
<PhoneInput />
</Field>
<Field>
<FieldLabel htmlFor="state">Estado</FieldLabel>
<NativeSelect
required
{...methods.register('state', { onChange: (e) => handleStateUpdate(e.target.value) })}
>
<NativeSelectOption value="">Selecione um estado</NativeSelectOption>
{Object.keys(CitiesByState).map((state) => (
<NativeSelectOption key={state} value={state}>
{state}
</NativeSelectOption>
))}
</NativeSelect>
<StateSelect />
</Field>
<Field>
<FieldLabel htmlFor="city">Cidade</FieldLabel>
<NativeSelect
id="city"
{...methods.register('city')}
disabled={!cityOptions.length}
required
>
<NativeSelectOption value="">Selecione uma cidade</NativeSelectOption>
{cityOptions.map((city) => (
<NativeSelectOption key={city} value={city}>
{city}
</NativeSelectOption>
))}
</NativeSelect>
<CitySelect />
</Field>
<Field>
<FieldLabel htmlFor="postalCode">CEP</FieldLabel>
<Input
id="postalCode"
type="text"
placeholder="00000-000"
{...methods.register('postalCode')}
/>
<PostalCodeInput />
</Field>
<Field>
<FieldLabel htmlFor="comment">Comentário</FieldLabel>
@ -182,5 +119,6 @@ export const PetitionForm = ({ id, petition }: PetitionFormProps) => {
</FieldGroup>
</FieldSet>
</form>
</FormProvider>
)
}

View file

@ -0,0 +1,16 @@
import type { ComponentProps } from 'react'
import { FormInput } from './FormInput'
export const PhoneInput = (props: ComponentProps<'input'>) => (
<FormInput
form="phone"
id="phone"
placeholder="(71) 99999-9999"
autoComplete="tel"
inputMode="tel"
type="tel"
maxLength={15}
required
{...props}
/>
)

View file

@ -0,0 +1,6 @@
import type { ComponentProps } from 'react'
import { FormInput } from './FormInput'
export const PostalCodeInput = (props: ComponentProps<'input'>) => (
<FormInput form="postalCode" id="postalCode" type="text" placeholder="00000-000" {...props} />
)

View file

@ -0,0 +1,22 @@
import { useFormContext } from 'react-hook-form'
import { NativeSelect, NativeSelectOption } from './ui/native-select'
import { CitiesByState } from '@/lib/cities'
interface StateSelectProps {
placeholder?: string
}
export const StateSelect = ({ placeholder = 'Selecione um estado' }) => {
const { register } = useFormContext()
return (
<NativeSelect required {...register('state')}>
<NativeSelectOption value="">{placeholder}</NativeSelectOption>
{Object.keys(CitiesByState).map((state) => (
<NativeSelectOption key={state} value={state}>
{state}
</NativeSelectOption>
))}
</NativeSelect>
)
}

View file

@ -7,5 +7,15 @@ export function ThemeProvider({
children,
...props
}: React.ComponentProps<typeof NextThemesProvider>) {
return <NextThemesProvider {...props}>{children}</NextThemesProvider>
return (
<NextThemesProvider
{...props}
forcedTheme="light"
defaultTheme="light"
enableSystem={false}
disableTransitionOnChange
>
{children}
</NextThemesProvider>
)
}

View file

@ -0,0 +1,96 @@
'use client'
import { CitySelect } from '@/components/CitySelect'
import { EmailInput } from '@/components/EmailInput'
import { NameInput } from '@/components/NameInput'
import { PhoneInput } from '@/components/PhoneInput'
import { StateSelect } from '@/components/StateSelect'
import { Button } from '@/components/ui/button'
import { Field, FieldDescription, FieldGroup, FieldLabel, FieldLegend } from '@/components/ui/field'
import { Textarea } from '@/components/ui/textarea'
import { WhatsAppFormInput, whatsAppFormSchema } from '@/lib/schemas/whatsapp-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { FormProvider, SubmitHandler, useForm } from 'react-hook-form'
import { useTransition } from 'react'
import { submitWhatsapp } from '@/app/(frontend)/actions/submitWhatsapp'
interface WhatsappFormProps {
classname?: string
}
export const WhatsappForm = ({ classname }: WhatsappFormProps) => {
const methods = useForm<WhatsAppFormInput>({
resolver: zodResolver(whatsAppFormSchema),
defaultValues: {
name: '',
email: '',
phone: '',
state: undefined,
city: '',
comment: '',
},
})
const [isSubmitting, startTransition] = useTransition()
const onSubmit: SubmitHandler<WhatsAppFormInput> = (input) => {
startTransition(async () => {
try {
await submitWhatsapp(input)
methods.reset()
} catch {
methods.setError('root', {
message: 'Falha ao enviar assinatura. Tente novamente.',
})
}
})
}
return (
<FormProvider {...methods}>
<form
id="mandato-no-whatsapp"
onSubmit={methods.handleSubmit(onSubmit)}
className={classname}
>
<FieldGroup>
<Field>
<NameInput placeholder="Nome completo*" />
</Field>
<Field>
<PhoneInput placeholder="WhatsApp (com DDD)*" />
</Field>
<Field>
<EmailInput placeholder="Digite seu melhor email*" />
</Field>
<div className="flex gap-2">
<Field>
<StateSelect placeholder="Estado" />
</Field>
<Field>
<CitySelect placeholder="Cidade" />
</Field>
</div>
<Field>
<Textarea
id="comment"
{...methods.register('comment')}
placeholder='Campo de Interesse: "O que você mais quer ver no canal?" (Votações, Bastidores, Ações Sociais, Agenda).'
required
className="min-h-22"
/>
</Field>
<Field>
<Button
type="submit"
disabled={isSubmitting}
className="bg-[#99c343] font-bold text-xl py-6 rounded-full"
>
{isSubmitting ? 'Enviando...' : 'FAZER MEU CADASTRO'}
</Button>
</Field>
</FieldGroup>
</form>
</FormProvider>
)
}

View file

@ -1,3 +1,4 @@
import { cn } from '@/lib/utils'
import { getCachedGlobal } from '@/utilities/globals'
import Link from 'next/link'
@ -7,6 +8,27 @@ type IconProps = {
const baseIconClass = 'h-5 w-5'
const WhatsAppIcon = ({ className }: IconProps) => (
<svg
xmlns="http://www.w3.org/2000/svg"
shape-rendering="geometricPrecision"
text-rendering="geometricPrecision"
image-rendering="optimizeQuality"
fill="currenColor"
fill-rule="evenodd"
clip-rule="evenodd"
viewBox="0 0 510 512.459"
className={className || baseIconClass}
aria-hidden
>
<title>Whatsapp</title>
<path
fill="#111B21"
d="M435.689 74.468C387.754 26.471 324 .025 256.071 0 116.098 0 2.18 113.906 2.131 253.916c-.024 44.758 11.677 88.445 33.898 126.946L0 512.459l134.617-35.311c37.087 20.238 78.85 30.891 121.345 30.903h.109c139.949 0 253.88-113.917 253.928-253.928.024-67.855-26.361-131.645-74.31-179.643v-.012zm-179.618 390.7h-.085c-37.868-.011-75.016-10.192-107.428-29.417l-7.707-4.577-79.886 20.953 21.32-77.889-5.017-7.987c-21.125-33.605-32.29-72.447-32.266-112.322.049-116.366 94.729-211.046 211.155-211.046 56.373.025 109.364 22.003 149.214 61.903 39.853 39.888 61.781 92.927 61.757 149.313-.05 116.377-94.728 211.058-211.057 211.058v.011zm115.768-158.067c-6.344-3.178-37.537-18.52-43.358-20.639-5.82-2.119-10.044-3.177-14.27 3.178-4.225 6.357-16.388 20.651-20.09 24.875-3.702 4.238-7.403 4.762-13.747 1.583-6.343-3.178-26.787-9.874-51.029-31.487-18.86-16.827-31.597-37.598-35.297-43.955-3.702-6.355-.39-9.789 2.775-12.943 2.849-2.848 6.344-7.414 9.522-11.116s4.225-6.355 6.343-10.581c2.12-4.238 1.06-7.937-.522-11.117-1.584-3.177-14.271-34.409-19.568-47.108-5.151-12.37-10.385-10.69-14.269-10.897-3.703-.183-7.927-.219-12.164-.219s-11.105 1.582-16.925 7.939c-5.82 6.354-22.209 21.709-22.209 52.927 0 31.22 22.733 61.405 25.911 65.642 3.177 4.237 44.745 68.318 108.389 95.812 15.135 6.538 26.957 10.446 36.175 13.368 15.196 4.834 29.027 4.153 39.96 2.52 12.19-1.825 37.54-15.353 42.824-30.172 5.283-14.818 5.283-27.529 3.701-30.172-1.582-2.641-5.819-4.237-12.163-7.414l.011-.024z"
/>
</svg>
)
const InstagramIcon = ({ className }: IconProps) => (
<svg
viewBox="0 0 24 24"
@ -64,6 +86,7 @@ const icons = {
instagram: InstagramIcon,
facebook: FacebookIcon,
youtube: YoutubeIcon,
whatsapp: WhatsAppIcon,
custom: GlobeIcon,
} as const
@ -71,20 +94,22 @@ const defaultA11yLabels: Record<string, string> = {
instagram: 'Instagram',
facebook: 'Facebook',
youtube: 'YouTube',
whatsapp: 'WhatsApp',
custom: 'Link externo',
}
type SocialLinksProps = {
ariaLabel: string
className?: string
}
export const SocialLinks = async ({ ariaLabel }: SocialLinksProps) => {
export const SocialLinks = async ({ ariaLabel, className }: SocialLinksProps) => {
const { socialLinks: links } = await getCachedGlobal('site-settings', 2)()
if (!links?.length) return null
return (
<nav aria-label={ariaLabel} className="flex items-center gap-3">
<nav aria-label={ariaLabel} className={cn('flex items-center gap-3', className)}>
{links.map((link) => {
if (!link.enabled || !link.url) return null

View file

@ -1,37 +1,37 @@
import * as React from 'react'
import { cva, type VariantProps } from 'class-variance-authority'
import { Root } from '@radix-ui/react-slot'
import { Slot } from 'radix-ui'
import { cn } from '@/lib/utils'
const buttonVariants = cva(
"focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 rounded-md border border-transparent bg-clip-padding text-sm font-medium focus-visible:ring-3 aria-invalid:ring-3 [&_svg:not([class*='size-'])]:size-4 inline-flex items-center justify-center whitespace-nowrap transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none shrink-0 [&_svg]:shrink-0 outline-none group/button select-none",
"group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
{
variants: {
variant: {
default: 'bg-primary text-primary-foreground hover:bg-primary/80',
default: 'bg-primary text-primary-foreground [a]:hover:bg-primary/80',
outline:
'border-border bg-background hover:bg-muted hover:text-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50 aria-expanded:bg-muted aria-expanded:text-foreground shadow-xs',
'border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50',
secondary:
'bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground',
ghost:
'hover:bg-muted hover:text-foreground dark:hover:bg-muted/50 aria-expanded:bg-muted aria-expanded:text-foreground',
'hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50',
destructive:
'bg-destructive/10 hover:bg-destructive/20 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/20 text-destructive focus-visible:border-destructive/40 dark:hover:bg-destructive/30',
'bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40',
link: 'text-primary underline-offset-4 hover:underline',
},
size: {
default:
'h-9 gap-1.5 px-2.5 in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2',
xs: "h-6 gap-1 rounded-[min(var(--radius-md),8px)] px-2 text-xs in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
sm: 'h-8 gap-1 rounded-[min(var(--radius-md),10px)] px-2.5 in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5',
lg: 'h-10 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-3 has-data-[icon=inline-start]:pl-3',
icon: 'size-9',
'h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2',
xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
lg: 'h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-3 has-data-[icon=inline-start]:pl-3',
icon: 'size-8',
'icon-xs':
"size-6 rounded-[min(var(--radius-md),8px)] in-data-[slot=button-group]:rounded-md [&_svg:not([class*='size-'])]:size-3",
"size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3",
'icon-sm':
'size-8 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-md',
'icon-lg': 'size-10',
'size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg',
'icon-lg': 'size-9',
},
},
defaultVariants: {
@ -51,14 +51,19 @@ function Button({
VariantProps<typeof buttonVariants> & {
asChild?: boolean
}) {
const Comp = asChild ? Root : 'button'
const Comp = asChild ? Slot.Root : 'button'
const isSubmitButton = props.type === 'submit'
return (
<Comp
data-slot="button"
data-variant={variant}
data-size={size}
className={cn(buttonVariants({ variant, size, className }))}
className={cn(
buttonVariants({ variant, size }),
isSubmitButton && 'font-bold text-white hover:text-white',
className,
)}
{...props}
/>
)

View file

@ -1,20 +1,19 @@
"use client"
'use client'
import { useMemo } from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { useMemo } from 'react'
import { cva, type VariantProps } from 'class-variance-authority'
import { cn } from "@/lib/utils"
import { Label } from "@/components/ui/label"
import { Separator } from "@/components/ui/separator"
import { cn } from '@/lib/utils'
import { Label } from '@/components/ui/label'
import { Separator } from '@/components/ui/separator'
function FieldSet({ className, ...props }: React.ComponentProps<"fieldset">) {
function FieldSet({ className, ...props }: React.ComponentProps<'fieldset'>) {
return (
<fieldset
data-slot="field-set"
className={cn(
"flex flex-col gap-6",
"has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3",
className
'flex flex-col gap-4 has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3',
className,
)}
{...props}
/>
@ -23,66 +22,55 @@ function FieldSet({ className, ...props }: React.ComponentProps<"fieldset">) {
function FieldLegend({
className,
variant = "legend",
variant = 'legend',
...props
}: React.ComponentProps<"legend"> & { variant?: "legend" | "label" }) {
}: React.ComponentProps<'legend'> & { variant?: 'legend' | 'label' }) {
return (
<legend
data-slot="field-legend"
data-variant={variant}
className={cn(
"mb-3 font-medium",
"data-[variant=legend]:text-base",
"data-[variant=label]:text-sm",
className
'mb-1.5 font-medium data-[variant=label]:text-sm data-[variant=legend]:text-base',
className,
)}
{...props}
/>
)
}
function FieldGroup({ className, ...props }: React.ComponentProps<"div">) {
function FieldGroup({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="field-group"
className={cn(
"group/field-group @container/field-group flex w-full flex-col gap-7 data-[slot=checkbox-group]:gap-3 [&>[data-slot=field-group]]:gap-4",
className
'group/field-group @container/field-group flex w-full flex-col gap-4 data-[slot=checkbox-group]:gap-3 *:data-[slot=field-group]:gap-4',
className,
)}
{...props}
/>
)
}
const fieldVariants = cva(
"group/field data-[invalid=true]:text-destructive flex w-full gap-3",
{
const fieldVariants = cva('group/field flex w-full gap-2 data-[invalid=true]:text-destructive', {
variants: {
orientation: {
vertical: ["flex-col [&>*]:w-full [&>.sr-only]:w-auto"],
horizontal: [
"flex-row items-center",
"[&>[data-slot=field-label]]:flex-auto",
"has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px has-[>[data-slot=field-content]]:items-start",
],
responsive: [
"@md/field-group:flex-row @md/field-group:items-center @md/field-group:[&>*]:w-auto flex-col [&>*]:w-full [&>.sr-only]:w-auto",
"@md/field-group:[&>[data-slot=field-label]]:flex-auto",
"@md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px",
],
vertical: 'flex-col *:w-full [&>.sr-only]:w-auto',
horizontal:
'flex-row items-center has-[>[data-slot=field-content]]:items-start *:data-[slot=field-label]:flex-auto has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px',
responsive:
'flex-col *:w-full @md/field-group:flex-row @md/field-group:items-center @md/field-group:*:w-auto @md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:*:data-[slot=field-label]:flex-auto [&>.sr-only]:w-auto @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px',
},
},
defaultVariants: {
orientation: "vertical",
orientation: 'vertical',
},
}
)
})
function Field({
className,
orientation = "vertical",
orientation = 'vertical',
...props
}: React.ComponentProps<"div"> & VariantProps<typeof fieldVariants>) {
}: React.ComponentProps<'div'> & VariantProps<typeof fieldVariants>) {
return (
<div
role="group"
@ -94,59 +82,52 @@ function Field({
)
}
function FieldContent({ className, ...props }: React.ComponentProps<"div">) {
function FieldContent({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="field-content"
className={cn(
"group/field-content flex flex-1 flex-col gap-1.5 leading-snug",
className
)}
className={cn('group/field-content flex flex-1 flex-col gap-0.5 leading-snug', className)}
{...props}
/>
)
}
function FieldLabel({
className,
...props
}: React.ComponentProps<typeof Label>) {
function FieldLabel({ className, ...props }: React.ComponentProps<typeof Label>) {
return (
<Label
data-slot="field-label"
className={cn(
"group/field-label peer/field-label flex w-fit gap-2 leading-snug group-data-[disabled=true]/field:opacity-50",
"has-[>[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 (
<div
data-slot="field-label"
className={cn(
"flex w-fit items-center gap-2 text-sm font-medium leading-snug group-data-[disabled=true]/field:opacity-50",
className
'flex w-fit items-center gap-2 text-sm leading-snug font-medium group-data-[disabled=true]/field:opacity-50',
className,
)}
{...props}
/>
)
}
function FieldDescription({ className, ...props }: React.ComponentProps<"p">) {
function FieldDescription({ className, ...props }: React.ComponentProps<'p'>) {
return (
<p
data-slot="field-description"
className={cn(
"text-muted-foreground text-sm font-normal leading-normal group-has-[[data-orientation=horizontal]]/field:text-balance",
"nth-last-2:-mt-1 last:mt-0 [[data-variant=legend]+&]:-mt-1.5",
"[&>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}
>
<Separator className="absolute inset-0 top-1/2" />
{children && (
<span
className="bg-background text-muted-foreground relative mx-auto block w-fit px-2"
className="relative mx-auto block w-fit bg-background px-2 text-muted-foreground"
data-slot="field-separator-content"
>
{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 (
<ul className="ml-4 flex list-disc flex-col gap-1">
{errors.map(
(error, index) =>
error?.message && <li key={index}>{error.message}</li>
)}
{uniqueErrors.map((error, index) => error?.message && <li key={index}>{error.message}</li>)}
</ul>
)
}, [children, errors])
@ -222,7 +202,7 @@ function FieldError({
<div
role="alert"
data-slot="field-error"
className={cn("text-destructive text-sm font-normal", className)}
className={cn('text-sm font-normal text-destructive', className)}
{...props}
>
{content}

View file

@ -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<HTMLInputElement, React.ComponentProps<"input">>(
({ className, type, ...props }, ref) => {
function Input({ className, type, ...props }: React.ComponentProps<'input'>) {
return (
<input
type={type}
data-slot="input"
className={cn(
"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className
'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',
className,
)}
ref={ref}
{...props}
/>
)
}
)
Input.displayName = "Input"
export { Input }

View file

@ -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<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
VariantProps<typeof labelVariants>
>(({ className, ...props }, ref) => (
function Label({
className,
...props
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
return (
<LabelPrimitive.Root
ref={ref}
className={cn(labelVariants(), className)}
data-slot="label"
className={cn(
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
className
)}
{...props}
/>
))
Label.displayName = LabelPrimitive.Root.displayName
)
}
export { Label }

View file

@ -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<React.ComponentProps<"select">, "size"> & {
size?: "sm" | "default"
type NativeSelectProps = Omit<React.ComponentProps<'select'>, 'size'> & {
size?: 'sm' | 'default'
}
function NativeSelect({
className,
size = "default",
...props
}: NativeSelectProps) {
function NativeSelect({ className, size = 'default', ...props }: NativeSelectProps) {
return (
<div
className={cn(
"group/native-select relative w-fit has-[select:disabled]:opacity-50",
className
'group/native-select relative w-fit has-[select:disabled]:opacity-50',
className,
)}
data-slot="native-select-wrapper"
data-size={size}
@ -24,29 +20,24 @@ function NativeSelect({
<select
data-slot="native-select"
data-size={size}
className="border-input placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 dark:hover:bg-input/50 focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 h-9 w-full min-w-0 appearance-none rounded-md border bg-transparent py-1 pr-8 pl-2.5 text-sm shadow-xs transition-[color,box-shadow] select-none focus-visible:ring-3 aria-invalid:ring-3 data-[size=sm]:h-8 outline-none disabled:pointer-events-none disabled:cursor-not-allowed"
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"
{...props}
/>
<ChevronDownIcon className="text-muted-foreground top-1/2 right-2.5 size-4 -translate-y-1/2 pointer-events-none absolute select-none" aria-hidden="true" data-slot="native-select-icon" />
<ChevronDownIcon
className="pointer-events-none absolute top-1/2 right-2.5 size-4 -translate-y-1/2 text-black/60 select-none"
aria-hidden="true"
data-slot="native-select-icon"
/>
</div>
)
}
function NativeSelectOption({ ...props }: React.ComponentProps<"option">) {
function NativeSelectOption({ ...props }: React.ComponentProps<'option'>) {
return <option data-slot="native-select-option" {...props} />
}
function NativeSelectOptGroup({
className,
...props
}: React.ComponentProps<"optgroup">) {
return (
<optgroup
data-slot="native-select-optgroup"
className={cn(className)}
{...props}
/>
)
function NativeSelectOptGroup({ className, ...props }: React.ComponentProps<'optgroup'>) {
return <optgroup data-slot="native-select-optgroup" className={cn(className)} {...props} />
}
export { NativeSelect, NativeSelectOptGroup, NativeSelectOption }

View file

@ -1,87 +1,83 @@
'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<typeof SelectPrimitive.Root>) {
return <SelectPrimitive.Root data-slot="select" {...props} />
}
const SelectGroup = SelectPrimitive.Group
function SelectGroup({ className, ...props }: React.ComponentProps<typeof SelectPrimitive.Group>) {
return (
<SelectPrimitive.Group
data-slot="select-group"
className={cn('scroll-my-1 p-1', className)}
{...props}
/>
)
}
const SelectValue = SelectPrimitive.Value
function SelectValue({ ...props }: React.ComponentProps<typeof SelectPrimitive.Value>) {
return <SelectPrimitive.Value data-slot="select-value" {...props} />
}
const SelectTrigger = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
function SelectTrigger({
className,
size = 'default',
children,
...props
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
size?: 'sm' | 'default'
}) {
return (
<SelectPrimitive.Trigger
ref={ref}
data-slot="select-trigger"
data-size={size}
className={cn(
'flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background data-[placeholder]:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1',
"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",
className,
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDown className="h-4 w-4 opacity-50" />
<ChevronDownIcon className="pointer-events-none size-4 text-black/60" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
))
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
)
}
const SelectScrollUpButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollUpButton
ref={ref}
className={cn('flex cursor-default items-center justify-center py-1', className)}
{...props}
>
<ChevronUp className="h-4 w-4" />
</SelectPrimitive.ScrollUpButton>
))
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
const SelectScrollDownButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollDownButton
ref={ref}
className={cn('flex cursor-default items-center justify-center py-1', className)}
{...props}
>
<ChevronDown className="h-4 w-4" />
</SelectPrimitive.ScrollDownButton>
))
SelectScrollDownButton.displayName = SelectPrimitive.ScrollDownButton.displayName
const SelectContent = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(({ className, children, position = 'popper', ...props }, ref) => (
function SelectContent({
className,
children,
position = 'item-aligned',
align = 'center',
...props
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
return (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
ref={ref}
data-slot="select-content"
data-align-trigger={position === 'item-aligned'}
className={cn(
'relative z-50 max-h-[--radix-select-content-available-height] min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-select-content-transform-origin]',
'relative z-50 max-h-(--radix-select-content-available-height) min-w-36 origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-lg bg-popover text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-[align-trigger=true]:animate-none data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95',
position === 'popper' &&
'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1',
className,
)}
position={position}
align={align}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
data-position={position}
className={cn(
"p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
'data-[position=popper]:h-(--radix-select-trigger-height) data-[position=popper]:w-full data-[position=popper]:min-w-(--radix-select-trigger-width)',
position === 'popper' && '',
)}
>
{children}
@ -89,64 +85,101 @@ const SelectContent = React.forwardRef<
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
))
SelectContent.displayName = SelectPrimitive.Content.displayName
)
}
const SelectLabel = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
>(({ className, ...props }, ref) => (
function SelectLabel({ className, ...props }: React.ComponentProps<typeof SelectPrimitive.Label>) {
return (
<SelectPrimitive.Label
ref={ref}
className={cn('px-2 py-1.5 text-sm font-semibold', className)}
data-slot="select-label"
className={cn('px-1.5 py-1 text-xs text-muted-foreground', className)}
{...props}
/>
))
SelectLabel.displayName = SelectPrimitive.Label.displayName
)
}
const SelectItem = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
>(({ className, children, ...props }, ref) => (
function SelectItem({
className,
children,
...props
}: React.ComponentProps<typeof SelectPrimitive.Item>) {
return (
<SelectPrimitive.Item
ref={ref}
data-slot="select-item"
className={cn(
'relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
"relative flex w-full cursor-default items-center gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
className,
)}
{...props}
>
<span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
<span className="pointer-events-none absolute right-2 flex size-4 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
<CheckIcon className="pointer-events-none" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
))
SelectItem.displayName = SelectPrimitive.Item.displayName
)
}
const SelectSeparator = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
>(({ className, ...props }, ref) => (
function SelectSeparator({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.Separator>) {
return (
<SelectPrimitive.Separator
ref={ref}
className={cn('-mx-1 my-1 h-px bg-muted', className)}
data-slot="select-separator"
className={cn('pointer-events-none -mx-1 my-1 h-px bg-border', className)}
{...props}
/>
))
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
)
}
function SelectScrollUpButton({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
return (
<SelectPrimitive.ScrollUpButton
data-slot="select-scroll-up-button"
className={cn(
"z-10 flex cursor-default items-center justify-center bg-popover py-1 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
>
<ChevronUpIcon />
</SelectPrimitive.ScrollUpButton>
)
}
function SelectScrollDownButton({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
return (
<SelectPrimitive.ScrollDownButton
data-slot="select-scroll-down-button"
className={cn(
"z-10 flex cursor-default items-center justify-center bg-popover py-1 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
>
<ChevronDownIcon />
</SelectPrimitive.ScrollDownButton>
)
}
export {
Select,
SelectGroup,
SelectValue,
SelectTrigger,
SelectContent,
SelectLabel,
SelectGroup,
SelectItem,
SelectSeparator,
SelectScrollUpButton,
SelectLabel,
SelectScrollDownButton,
SelectScrollUpButton,
SelectSeparator,
SelectTrigger,
SelectValue,
}

View file

@ -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<typeof SeparatorPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
>(
(
{ className, orientation = "horizontal", decorative = true, ...props },
ref
) => (
function Separator({
className,
orientation = "horizontal",
decorative = true,
...props
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
return (
<SeparatorPrimitive.Root
ref={ref}
data-slot="separator"
decorative={decorative}
orientation={orientation}
className={cn(
"shrink-0 bg-border",
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
"shrink-0 bg-border data-horizontal:h-px data-horizontal:w-full data-vertical:w-px data-vertical:self-stretch",
className
)}
{...props}
/>
)
)
Separator.displayName = SeparatorPrimitive.Root.displayName
}
export { Separator }

View file

@ -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 (
<textarea
data-slot="textarea"
className={cn(
"border-input dark:bg-input/30 focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 rounded-md border bg-transparent px-2.5 py-2 text-base shadow-xs transition-[color,box-shadow] focus-visible:ring-3 aria-invalid:ring-3 md:text-sm placeholder:text-muted-foreground flex field-sizing-content min-h-16 w-full outline-none disabled:cursor-not-allowed disabled:opacity-50",
className
'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',
className,
)}
{...props}
/>

View file

@ -42,6 +42,7 @@ export const SiteSettings: GlobalConfig = {
{ label: 'Instagram', value: 'instagram' },
{ label: 'Facebook', value: 'facebook' },
{ label: 'YouTube', value: 'youtube' },
{ label: 'WhatsApp', value: 'whatsapp' },
],
},
{

View file

@ -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<typeof commentSchema>

View file

@ -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<StateKey>(
(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<typeof contactSchema>

View file

@ -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<StateKey>(
(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<typeof petitionFormSchema>

View file

@ -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<typeof whatsAppFormSchema>

View file

@ -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<false> | ContactSelect<true>;
consent: ConsentSelect<false> | ConsentSelect<true>;
signature: SignatureSelect<false> | SignatureSelect<true>;
subscription: SubscriptionSelect<false> | SubscriptionSelect<true>;
'payload-kv': PayloadKvSelect<false> | PayloadKvSelect<true>;
'payload-jobs': PayloadJobsSelect<false> | PayloadJobsSelect<true>;
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
@ -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<T extends boolean = true> {
state?: T;
city?: T;
postalCode?: T;
comment?: T;
updatedAt?: T;
createdAt?: T;
}
@ -561,6 +578,18 @@ export interface SignatureSelect<T extends boolean = true> {
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<T extends boolean = true> {
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.

View file

@ -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 || '',