Including page mandato-no-whatsapp
|
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"$schema": "https://ui.shadcn.com/schema.json",
|
"$schema": "https://ui.shadcn.com/schema.json",
|
||||||
"style": "radix-vega",
|
"style": "radix-nova",
|
||||||
"rsc": true,
|
"rsc": true,
|
||||||
"tsx": true,
|
"tsx": true,
|
||||||
"tailwind": {
|
"tailwind": {
|
||||||
"config": "tailwind.config.ts",
|
"config": "tailwind.config.ts",
|
||||||
"css": "src/app/(frontend)/styles.css",
|
"css": "src/app/(frontend)/styles.css",
|
||||||
"baseColor": "neutral",
|
"baseColor": "mist",
|
||||||
"cssVariables": true,
|
"cssVariables": true,
|
||||||
"prefix": ""
|
"prefix": ""
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -57,9 +57,11 @@
|
||||||
"next": "^15.5.12",
|
"next": "^15.5.12",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"payload": "3.79.0",
|
"payload": "3.79.0",
|
||||||
|
"radix-ui": "^1.4.3",
|
||||||
"react": "^19.2.4",
|
"react": "^19.2.4",
|
||||||
"react-dom": "^19.2.4",
|
"react-dom": "^19.2.4",
|
||||||
"react-hook-form": "^7.71.1",
|
"react-hook-form": "^7.71.1",
|
||||||
|
"shadcn": "^4.0.3",
|
||||||
"sharp": "0.34.2",
|
"sharp": "0.34.2",
|
||||||
"tailwind-merge": "^3.4.1",
|
"tailwind-merge": "^3.4.1",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
|
|
|
||||||
2933
pnpm-lock.yaml
BIN
public/BG.jpg
Normal file
|
After Width: | Height: | Size: 851 KiB |
BIN
public/BG.png
Normal file
|
After Width: | Height: | Size: 1.9 MiB |
121
public/LOGO_SOLLA_BRANCO.svg
Normal 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
|
After Width: | Height: | Size: 859 KiB |
BIN
public/SOLLA (1).png
Normal file
|
After Width: | Height: | Size: 559 KiB |
BIN
public/SOLLA.png
Normal file
|
After Width: | Height: | Size: 998 KiB |
BIN
public/WhatsApp Image 2026-03-11 at 17.54.55.jpeg
Normal file
|
After Width: | Height: | Size: 280 KiB |
16
src/app/(frontend)/(home)/layout.tsx
Normal 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 />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -15,10 +15,15 @@ export default async function HomePage() {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
// Main element occupies the whole screen width and the height of screen minus height of header
|
<main className="w-screen">
|
||||||
<main className="w-screen h-screen overflow-hidden">
|
|
||||||
{image?.url ? (
|
{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}
|
) : null}
|
||||||
</main>
|
</main>
|
||||||
)
|
)
|
||||||
|
|
@ -9,24 +9,53 @@ type SubmitPetitionSignatureInput = PetitionFormInput & {
|
||||||
consentId: number
|
consentId: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export const submitPetitionSignature = async (input: SubmitPetitionSignatureInput) => {
|
export const submitPetitionSignature = async ({
|
||||||
const { petitionId, consentId, ...contactInput } = input
|
petitionId,
|
||||||
|
consentId,
|
||||||
|
comment,
|
||||||
|
...contactInput
|
||||||
|
}: SubmitPetitionSignatureInput) => {
|
||||||
const data = petitionFormSchema.parse(contactInput)
|
const data = petitionFormSchema.parse(contactInput)
|
||||||
const payload = await getPayload({ config })
|
const payload = await getPayload({ config })
|
||||||
|
|
||||||
const contact = await payload.create({
|
const transactionID = await payload.db.beginTransaction()
|
||||||
collection: 'contact',
|
|
||||||
data,
|
|
||||||
})
|
|
||||||
|
|
||||||
await payload.create({
|
if (!transactionID) {
|
||||||
collection: 'signature',
|
throw new Error('Failed to begin transaction')
|
||||||
data: {
|
}
|
||||||
contact: contact.id,
|
|
||||||
petition: petitionId,
|
|
||||||
consent: consentId,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
return { ok: true as const }
|
try {
|
||||||
|
const contact = await payload.create({
|
||||||
|
collection: 'contact',
|
||||||
|
data,
|
||||||
|
req: { transactionID },
|
||||||
|
})
|
||||||
|
|
||||||
|
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 },
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
|
||||||
|
await payload.db.commitTransaction(transactionID)
|
||||||
|
return { ok: true }
|
||||||
|
} catch (error) {
|
||||||
|
await payload.db.rollbackTransaction(transactionID)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
41
src/app/(frontend)/actions/submitWhatsapp.ts
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
import { Footer } from '@/components/Footer'
|
|
||||||
import { Header } from '@/components/Header'
|
|
||||||
import { getCachedDocumentById } from '@/utilities/documents'
|
import { getCachedDocumentById } from '@/utilities/documents'
|
||||||
import { getCachedGlobal } from '@/utilities/globals'
|
import { getCachedGlobal } from '@/utilities/globals'
|
||||||
import { RefreshRouteOnSave } from '@/utilities/RefreshRouteOnSave'
|
import { RefreshRouteOnSave } from '@/utilities/RefreshRouteOnSave'
|
||||||
|
|
@ -68,20 +66,18 @@ export default async function RootLayout(props: { children: React.ReactNode }) {
|
||||||
const { children } = props
|
const { children } = props
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<html lang="pt-BR" className={inter.variable} suppressHydrationWarning>
|
<html
|
||||||
<body className="antialiased">
|
lang="pt-BR"
|
||||||
<ThemeProvider
|
className={`${inter.variable} w-screen h-screen scrollbar-hide`}
|
||||||
attribute="class"
|
style={{ colorScheme: 'light' }}
|
||||||
defaultTheme="system"
|
suppressHydrationWarning
|
||||||
enableSystem
|
>
|
||||||
disableTransitionOnChange
|
<body className="antialiased w-screen scrollbar-hide">
|
||||||
>
|
<ThemeProvider attribute="class">
|
||||||
<SpeedInsights />
|
<SpeedInsights />
|
||||||
<Analytics />
|
<Analytics />
|
||||||
<RefreshRouteOnSave />
|
<RefreshRouteOnSave />
|
||||||
<Header />
|
|
||||||
{children}
|
{children}
|
||||||
<Footer />
|
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
48
src/app/(frontend)/mandato-no-whatsapp/page.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -2,6 +2,8 @@
|
||||||
|
|
||||||
@plugin 'tailwindcss-animate';
|
@plugin 'tailwindcss-animate';
|
||||||
@import 'tw-animate-css';
|
@import 'tw-animate-css';
|
||||||
|
@import 'tw-animate-css';
|
||||||
|
@import 'shadcn/tailwind.css';
|
||||||
|
|
||||||
@custom-variant dark (&:is(.dark *));
|
@custom-variant dark (&:is(.dark *));
|
||||||
@source "../../**/*.{js,ts,jsx,tsx,mdx}";
|
@source "../../**/*.{js,ts,jsx,tsx,mdx}";
|
||||||
|
|
@ -12,6 +14,7 @@
|
||||||
|
|
||||||
html {
|
html {
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
|
color-scheme: light;
|
||||||
}
|
}
|
||||||
|
|
||||||
html,
|
html,
|
||||||
|
|
@ -64,72 +67,72 @@ body,
|
||||||
}
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--background: oklch(1 0 0);
|
--card: oklch(0.25 0.025 171);
|
||||||
--foreground: oklch(0.145 0 0);
|
--card-foreground: oklch(0.98 0.004 197.1);
|
||||||
--card: oklch(1 0 0);
|
|
||||||
--card-foreground: oklch(0.145 0 0);
|
|
||||||
--popover: oklch(1 0 0);
|
--popover: oklch(1 0 0);
|
||||||
--popover-foreground: oklch(0.145 0 0);
|
--popover-foreground: oklch(0.18 0.01 223.9);
|
||||||
--primary: oklch(0.205 0 0);
|
--primary: oklch(0.9 0.14 143);
|
||||||
--primary-foreground: oklch(0.985 0 0);
|
--primary-foreground: oklch(0.2 0.015 171);
|
||||||
--secondary: oklch(0.97 0 0);
|
--secondary: oklch(0.31 0.02 171);
|
||||||
--secondary-foreground: oklch(0.205 0 0);
|
--secondary-foreground: oklch(0.98 0.004 197.1);
|
||||||
--muted: oklch(0.97 0 0);
|
--muted: oklch(0.29 0.02 171);
|
||||||
--muted-foreground: oklch(0.556 0 0);
|
--muted-foreground: oklch(0.9 0.01 197.1);
|
||||||
--accent: oklch(0.97 0 0);
|
--accent: oklch(0.34 0.025 171);
|
||||||
--accent-foreground: oklch(0.205 0 0);
|
--accent-foreground: oklch(0.98 0.004 197.1);
|
||||||
--destructive: oklch(0.58 0.22 27);
|
--destructive: oklch(0.577 0.245 27.325);
|
||||||
--border: oklch(0.922 0 0);
|
--border: oklch(0.7 0.015 197.1 / 20%);
|
||||||
--input: oklch(0.922 0 0);
|
--input: oklch(0.7 0.015 197.1 / 20%);
|
||||||
--ring: oklch(0.708 0 0);
|
--ring: oklch(0.9 0.14 143);
|
||||||
--chart-1: oklch(0.809 0.105 251.813);
|
--chart-1: oklch(0.81 0.117 11.638);
|
||||||
--chart-2: oklch(0.623 0.214 259.815);
|
--chart-2: oklch(0.645 0.246 16.439);
|
||||||
--chart-3: oklch(0.546 0.245 262.881);
|
--chart-3: oklch(0.586 0.253 17.585);
|
||||||
--chart-4: oklch(0.488 0.243 264.376);
|
--chart-4: oklch(0.514 0.222 16.935);
|
||||||
--chart-5: oklch(0.424 0.199 265.638);
|
--chart-5: oklch(0.455 0.188 13.697);
|
||||||
--radius: 0.625rem;
|
--radius: 0.875rem;
|
||||||
--sidebar: oklch(0.985 0 0);
|
--sidebar: oklch(0.25 0.025 171);
|
||||||
--sidebar-foreground: oklch(0.145 0 0);
|
--sidebar-foreground: oklch(0.98 0.004 197.1);
|
||||||
--sidebar-primary: oklch(0.205 0 0);
|
--sidebar-primary: oklch(0.9 0.14 143);
|
||||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
--sidebar-primary-foreground: oklch(0.2 0.015 171);
|
||||||
--sidebar-accent: oklch(0.97 0 0);
|
--sidebar-accent: oklch(0.31 0.02 171);
|
||||||
--sidebar-accent-foreground: oklch(0.205 0 0);
|
--sidebar-accent-foreground: oklch(0.98 0.004 197.1);
|
||||||
--sidebar-border: oklch(0.922 0 0);
|
--sidebar-border: oklch(0.7 0.015 197.1 / 20%);
|
||||||
--sidebar-ring: oklch(0.708 0 0);
|
--sidebar-ring: oklch(0.9 0.14 143);
|
||||||
|
--background: oklch(0.2 0.02 171);
|
||||||
|
--foreground: oklch(0.98 0.004 197.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark {
|
.dark {
|
||||||
--background: oklch(0.145 0 0);
|
--background: oklch(0.148 0.004 228.8);
|
||||||
--foreground: oklch(0.985 0 0);
|
--foreground: oklch(0.987 0.002 197.1);
|
||||||
--card: oklch(0.205 0 0);
|
--card: oklch(0.218 0.008 223.9);
|
||||||
--card-foreground: oklch(0.985 0 0);
|
--card-foreground: oklch(0.987 0.002 197.1);
|
||||||
--popover: oklch(0.205 0 0);
|
--popover: oklch(0.218 0.008 223.9);
|
||||||
--popover-foreground: oklch(0.985 0 0);
|
--popover-foreground: oklch(0.987 0.002 197.1);
|
||||||
--primary: oklch(0.87 0 0);
|
--primary: oklch(0.925 0.005 214.3);
|
||||||
--primary-foreground: oklch(0.205 0 0);
|
--primary-foreground: oklch(0.218 0.008 223.9);
|
||||||
--secondary: oklch(0.269 0 0);
|
--secondary: oklch(0.275 0.011 216.9);
|
||||||
--secondary-foreground: oklch(0.985 0 0);
|
--secondary-foreground: oklch(0.987 0.002 197.1);
|
||||||
--muted: oklch(0.269 0 0);
|
--muted: oklch(0.275 0.011 216.9);
|
||||||
--muted-foreground: oklch(0.708 0 0);
|
--muted-foreground: oklch(0.723 0.014 214.4);
|
||||||
--accent: oklch(0.371 0 0);
|
--accent: oklch(0.275 0.011 216.9);
|
||||||
--accent-foreground: oklch(0.985 0 0);
|
--accent-foreground: oklch(0.987 0.002 197.1);
|
||||||
--destructive: oklch(0.704 0.191 22.216);
|
--destructive: oklch(0.704 0.191 22.216);
|
||||||
--border: oklch(1 0 0 / 10%);
|
--border: oklch(1 0 0 / 10%);
|
||||||
--input: oklch(1 0 0 / 15%);
|
--input: oklch(1 0 0 / 15%);
|
||||||
--ring: oklch(0.556 0 0);
|
--ring: oklch(0.56 0.021 213.5);
|
||||||
--chart-1: oklch(0.809 0.105 251.813);
|
--chart-1: oklch(0.81 0.117 11.638);
|
||||||
--chart-2: oklch(0.623 0.214 259.815);
|
--chart-2: oklch(0.645 0.246 16.439);
|
||||||
--chart-3: oklch(0.546 0.245 262.881);
|
--chart-3: oklch(0.586 0.253 17.585);
|
||||||
--chart-4: oklch(0.488 0.243 264.376);
|
--chart-4: oklch(0.514 0.222 16.935);
|
||||||
--chart-5: oklch(0.424 0.199 265.638);
|
--chart-5: oklch(0.455 0.188 13.697);
|
||||||
--sidebar: oklch(0.205 0 0);
|
--sidebar: oklch(0.218 0.008 223.9);
|
||||||
--sidebar-foreground: oklch(0.985 0 0);
|
--sidebar-foreground: oklch(0.987 0.002 197.1);
|
||||||
--sidebar-primary: oklch(0.488 0.243 264.376);
|
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
--sidebar-primary-foreground: oklch(0.987 0.002 197.1);
|
||||||
--sidebar-accent: oklch(0.269 0 0);
|
--sidebar-accent: oklch(0.275 0.011 216.9);
|
||||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
--sidebar-accent-foreground: oklch(0.987 0.002 197.1);
|
||||||
--sidebar-border: oklch(1 0 0 / 10%);
|
--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 {
|
@layer base {
|
||||||
|
|
@ -181,4 +184,19 @@ body,
|
||||||
select {
|
select {
|
||||||
cursor: pointer;
|
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 */
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -56,11 +56,5 @@ export const Contact: CollectionConfig = {
|
||||||
minLength: 8,
|
minLength: 8,
|
||||||
maxLength: 8,
|
maxLength: 8,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: 'comment',
|
|
||||||
type: 'textarea',
|
|
||||||
label: 'Comentario',
|
|
||||||
maxLength: 1000,
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -31,5 +31,11 @@ export const Signature: CollectionConfig = {
|
||||||
required: true,
|
required: true,
|
||||||
relationTo: 'consent',
|
relationTo: 'consent',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'comment',
|
||||||
|
type: 'textarea',
|
||||||
|
label: 'Comentario',
|
||||||
|
maxLength: 1000,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
|
||||||
34
src/collections/Subscription.ts
Normal 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,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
33
src/components/CitySelect.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
16
src/components/EmailInput.tsx
Normal 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}
|
||||||
|
/>
|
||||||
|
)
|
||||||
13
src/components/FormInput.tsx
Normal 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)} />
|
||||||
|
}
|
||||||
15
src/components/NameInput.tsx
Normal 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}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
|
@ -8,17 +8,20 @@ import {
|
||||||
FieldLegend,
|
FieldLegend,
|
||||||
FieldSet,
|
FieldSet,
|
||||||
} from '@/components/ui/field'
|
} from '@/components/ui/field'
|
||||||
import { Input } from '@/components/ui/input'
|
|
||||||
import { Petition } from '@/payload-types'
|
import { Petition } from '@/payload-types'
|
||||||
import { zodResolver } from '@hookform/resolvers/zod'
|
import { zodResolver } from '@hookform/resolvers/zod'
|
||||||
import { type SubmitHandler, useForm } from 'react-hook-form'
|
import { FormProvider, type SubmitHandler, useForm } from 'react-hook-form'
|
||||||
import { NativeSelect, NativeSelectOption } from './ui/native-select'
|
import { useTransition } from 'react'
|
||||||
import { useState, useTransition } from 'react'
|
|
||||||
import { CitiesByState } from '@/lib/cities'
|
|
||||||
import { Textarea } from './ui/textarea'
|
import { Textarea } from './ui/textarea'
|
||||||
import { Button } from './ui/button'
|
import { Button } from './ui/button'
|
||||||
import { petitionFormSchema, type PetitionFormInput } from '@/lib/schemas/petition-form'
|
import { petitionFormSchema, type PetitionFormInput } from '@/lib/schemas/petition-form'
|
||||||
import { submitPetitionSignature } from '@/app/(frontend)/actions/submitPetitionSignature'
|
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 {
|
interface PetitionFormProps {
|
||||||
id: string
|
id: string
|
||||||
|
|
@ -41,19 +44,6 @@ export const PetitionForm = ({ id, petition }: PetitionFormProps) => {
|
||||||
|
|
||||||
const [isSubmitting, startTransition] = useTransition()
|
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 onSubmit: SubmitHandler<PetitionFormInput> = (input) => {
|
||||||
const consentId =
|
const consentId =
|
||||||
typeof petition.form?.consent === 'number' ? petition.form.consent : petition.form.consent.id
|
typeof petition.form?.consent === 'number' ? petition.form.consent : petition.form.consent.id
|
||||||
|
|
@ -66,7 +56,6 @@ export const PetitionForm = ({ id, petition }: PetitionFormProps) => {
|
||||||
consentId,
|
consentId,
|
||||||
})
|
})
|
||||||
methods.reset()
|
methods.reset()
|
||||||
setCityOptions([])
|
|
||||||
} catch {
|
} catch {
|
||||||
methods.setError('root', {
|
methods.setError('root', {
|
||||||
message: 'Falha ao enviar assinatura. Tente novamente.',
|
message: 'Falha ao enviar assinatura. Tente novamente.',
|
||||||
|
|
@ -76,111 +65,60 @@ export const PetitionForm = ({ id, petition }: PetitionFormProps) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form id={id} onSubmit={methods.handleSubmit(onSubmit)}>
|
<FormProvider {...methods}>
|
||||||
<FieldSet>
|
<form id={id} onSubmit={methods.handleSubmit(onSubmit)}>
|
||||||
{petition.form.title ? <FieldLegend>{petition.form.title}</FieldLegend> : null}
|
<FieldSet>
|
||||||
{petition.form.subtitle ? (
|
{petition.form.title ? <FieldLegend>{petition.form.title}</FieldLegend> : null}
|
||||||
<FieldDescription>
|
{petition.form.subtitle ? (
|
||||||
Assine esta petição e ajude a manter a orla de Salvador para todos nós!
|
<FieldDescription>
|
||||||
</FieldDescription>
|
Assine esta petição e ajude a manter a orla de Salvador para todos nós!
|
||||||
) : null}
|
</FieldDescription>
|
||||||
<FieldGroup>
|
) : null}
|
||||||
<Field>
|
<FieldGroup>
|
||||||
<FieldLabel htmlFor="name">Nome Completo</FieldLabel>
|
<Field>
|
||||||
<Input
|
<FieldLabel htmlFor="name">Nome Completo</FieldLabel>
|
||||||
id="name"
|
<NameInput />
|
||||||
type="text"
|
</Field>
|
||||||
placeholder="Digite seu nome completo"
|
<Field>
|
||||||
autoComplete="name"
|
<FieldLabel htmlFor="email">E-mail</FieldLabel>
|
||||||
maxLength={120}
|
<EmailInput />
|
||||||
{...methods.register('name')}
|
</Field>
|
||||||
required
|
<Field>
|
||||||
/>
|
<FieldLabel htmlFor="phone">Celular</FieldLabel>
|
||||||
</Field>
|
<PhoneInput />
|
||||||
<Field>
|
</Field>
|
||||||
<FieldLabel htmlFor="email">E-mail</FieldLabel>
|
<Field>
|
||||||
<Input
|
<FieldLabel htmlFor="state">Estado</FieldLabel>
|
||||||
id="email"
|
<StateSelect />
|
||||||
placeholder="Digite seu e-mail"
|
</Field>
|
||||||
autoComplete="email"
|
<Field>
|
||||||
inputMode="email"
|
<FieldLabel htmlFor="city">Cidade</FieldLabel>
|
||||||
type="text"
|
<CitySelect />
|
||||||
maxLength={254}
|
</Field>
|
||||||
{...methods.register('email')}
|
<Field>
|
||||||
required
|
<FieldLabel htmlFor="postalCode">CEP</FieldLabel>
|
||||||
/>
|
<PostalCodeInput />
|
||||||
</Field>
|
</Field>
|
||||||
<Field>
|
<Field>
|
||||||
<FieldLabel htmlFor="phone">Celular</FieldLabel>
|
<FieldLabel htmlFor="comment">Comentário</FieldLabel>
|
||||||
<Input
|
<Textarea id="comment" {...methods.register('comment')} />
|
||||||
id="phone"
|
</Field>
|
||||||
placeholder="(71) 99999-9999"
|
<Field>
|
||||||
autoComplete="tel"
|
{typeof petition.form.consent !== 'number' ? (
|
||||||
inputMode="tel"
|
<FieldDescription>{petition.form.consent.text}</FieldDescription>
|
||||||
type="tel"
|
) : null}
|
||||||
maxLength={15}
|
{methods.formState.errors.root?.message ? (
|
||||||
{...methods.register('phone')}
|
<FieldDescription className="text-destructive">
|
||||||
required
|
{methods.formState.errors.root.message}
|
||||||
/>
|
</FieldDescription>
|
||||||
</Field>
|
) : null}
|
||||||
<Field>
|
<Button type="submit" disabled={isSubmitting}>
|
||||||
<FieldLabel htmlFor="state">Estado</FieldLabel>
|
{isSubmitting ? 'Enviando...' : 'Assinar'}
|
||||||
<NativeSelect
|
</Button>
|
||||||
required
|
</Field>
|
||||||
{...methods.register('state', { onChange: (e) => handleStateUpdate(e.target.value) })}
|
</FieldGroup>
|
||||||
>
|
</FieldSet>
|
||||||
<NativeSelectOption value="">Selecione um estado</NativeSelectOption>
|
</form>
|
||||||
{Object.keys(CitiesByState).map((state) => (
|
</FormProvider>
|
||||||
<NativeSelectOption key={state} value={state}>
|
|
||||||
{state}
|
|
||||||
</NativeSelectOption>
|
|
||||||
))}
|
|
||||||
</NativeSelect>
|
|
||||||
</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>
|
|
||||||
</Field>
|
|
||||||
<Field>
|
|
||||||
<FieldLabel htmlFor="postalCode">CEP</FieldLabel>
|
|
||||||
<Input
|
|
||||||
id="postalCode"
|
|
||||||
type="text"
|
|
||||||
placeholder="00000-000"
|
|
||||||
{...methods.register('postalCode')}
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
<Field>
|
|
||||||
<FieldLabel htmlFor="comment">Comentário</FieldLabel>
|
|
||||||
<Textarea id="comment" {...methods.register('comment')} />
|
|
||||||
</Field>
|
|
||||||
<Field>
|
|
||||||
{typeof petition.form.consent !== 'number' ? (
|
|
||||||
<FieldDescription>{petition.form.consent.text}</FieldDescription>
|
|
||||||
) : null}
|
|
||||||
{methods.formState.errors.root?.message ? (
|
|
||||||
<FieldDescription className="text-destructive">
|
|
||||||
{methods.formState.errors.root.message}
|
|
||||||
</FieldDescription>
|
|
||||||
) : null}
|
|
||||||
<Button type="submit" disabled={isSubmitting}>
|
|
||||||
{isSubmitting ? 'Enviando...' : 'Assinar'}
|
|
||||||
</Button>
|
|
||||||
</Field>
|
|
||||||
</FieldGroup>
|
|
||||||
</FieldSet>
|
|
||||||
</form>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
16
src/components/PhoneInput.tsx
Normal 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}
|
||||||
|
/>
|
||||||
|
)
|
||||||
6
src/components/PostalCodeInput.tsx
Normal 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} />
|
||||||
|
)
|
||||||
22
src/components/StateSelect.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -7,5 +7,15 @@ export function ThemeProvider({
|
||||||
children,
|
children,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof NextThemesProvider>) {
|
}: React.ComponentProps<typeof NextThemesProvider>) {
|
||||||
return <NextThemesProvider {...props}>{children}</NextThemesProvider>
|
return (
|
||||||
|
<NextThemesProvider
|
||||||
|
{...props}
|
||||||
|
forcedTheme="light"
|
||||||
|
defaultTheme="light"
|
||||||
|
enableSystem={false}
|
||||||
|
disableTransitionOnChange
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</NextThemesProvider>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
96
src/components/WhatsappForm.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
import { getCachedGlobal } from '@/utilities/globals'
|
import { getCachedGlobal } from '@/utilities/globals'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
|
|
||||||
|
|
@ -7,6 +8,27 @@ type IconProps = {
|
||||||
|
|
||||||
const baseIconClass = 'h-5 w-5'
|
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) => (
|
const InstagramIcon = ({ className }: IconProps) => (
|
||||||
<svg
|
<svg
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
|
|
@ -64,6 +86,7 @@ const icons = {
|
||||||
instagram: InstagramIcon,
|
instagram: InstagramIcon,
|
||||||
facebook: FacebookIcon,
|
facebook: FacebookIcon,
|
||||||
youtube: YoutubeIcon,
|
youtube: YoutubeIcon,
|
||||||
|
whatsapp: WhatsAppIcon,
|
||||||
custom: GlobeIcon,
|
custom: GlobeIcon,
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
|
|
@ -71,20 +94,22 @@ const defaultA11yLabels: Record<string, string> = {
|
||||||
instagram: 'Instagram',
|
instagram: 'Instagram',
|
||||||
facebook: 'Facebook',
|
facebook: 'Facebook',
|
||||||
youtube: 'YouTube',
|
youtube: 'YouTube',
|
||||||
|
whatsapp: 'WhatsApp',
|
||||||
custom: 'Link externo',
|
custom: 'Link externo',
|
||||||
}
|
}
|
||||||
|
|
||||||
type SocialLinksProps = {
|
type SocialLinksProps = {
|
||||||
ariaLabel: string
|
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)()
|
const { socialLinks: links } = await getCachedGlobal('site-settings', 2)()
|
||||||
|
|
||||||
if (!links?.length) return null
|
if (!links?.length) return null
|
||||||
|
|
||||||
return (
|
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) => {
|
{links.map((link) => {
|
||||||
if (!link.enabled || !link.url) return null
|
if (!link.enabled || !link.url) return null
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,37 +1,37 @@
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { cva, type VariantProps } from 'class-variance-authority'
|
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'
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
const buttonVariants = cva(
|
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: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
default: 'bg-primary text-primary-foreground hover:bg-primary/80',
|
default: 'bg-primary text-primary-foreground [a]:hover:bg-primary/80',
|
||||||
outline:
|
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:
|
secondary:
|
||||||
'bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground',
|
'bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground',
|
||||||
ghost:
|
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:
|
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',
|
link: 'text-primary underline-offset-4 hover:underline',
|
||||||
},
|
},
|
||||||
size: {
|
size: {
|
||||||
default:
|
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',
|
'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),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",
|
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-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',
|
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-10 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-3 has-data-[icon=inline-start]:pl-3',
|
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-9',
|
icon: 'size-8',
|
||||||
'icon-xs':
|
'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':
|
'icon-sm':
|
||||||
'size-8 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-md',
|
'size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg',
|
||||||
'icon-lg': 'size-10',
|
'icon-lg': 'size-9',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
|
|
@ -51,14 +51,19 @@ function Button({
|
||||||
VariantProps<typeof buttonVariants> & {
|
VariantProps<typeof buttonVariants> & {
|
||||||
asChild?: boolean
|
asChild?: boolean
|
||||||
}) {
|
}) {
|
||||||
const Comp = asChild ? Root : 'button'
|
const Comp = asChild ? Slot.Root : 'button'
|
||||||
|
const isSubmitButton = props.type === 'submit'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Comp
|
<Comp
|
||||||
data-slot="button"
|
data-slot="button"
|
||||||
data-variant={variant}
|
data-variant={variant}
|
||||||
data-size={size}
|
data-size={size}
|
||||||
className={cn(buttonVariants({ variant, size, className }))}
|
className={cn(
|
||||||
|
buttonVariants({ variant, size }),
|
||||||
|
isSubmitButton && 'font-bold text-white hover:text-white',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,19 @@
|
||||||
"use client"
|
'use client'
|
||||||
|
|
||||||
import { useMemo } from "react"
|
import { useMemo } from 'react'
|
||||||
import { cva, type VariantProps } from "class-variance-authority"
|
import { cva, type VariantProps } from 'class-variance-authority'
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from '@/lib/utils'
|
||||||
import { Label } from "@/components/ui/label"
|
import { Label } from '@/components/ui/label'
|
||||||
import { Separator } from "@/components/ui/separator"
|
import { Separator } from '@/components/ui/separator'
|
||||||
|
|
||||||
function FieldSet({ className, ...props }: React.ComponentProps<"fieldset">) {
|
function FieldSet({ className, ...props }: React.ComponentProps<'fieldset'>) {
|
||||||
return (
|
return (
|
||||||
<fieldset
|
<fieldset
|
||||||
data-slot="field-set"
|
data-slot="field-set"
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex flex-col gap-6",
|
'flex flex-col gap-4 has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3',
|
||||||
"has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3",
|
className,
|
||||||
className
|
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
|
|
@ -23,66 +22,55 @@ function FieldSet({ className, ...props }: React.ComponentProps<"fieldset">) {
|
||||||
|
|
||||||
function FieldLegend({
|
function FieldLegend({
|
||||||
className,
|
className,
|
||||||
variant = "legend",
|
variant = 'legend',
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<"legend"> & { variant?: "legend" | "label" }) {
|
}: React.ComponentProps<'legend'> & { variant?: 'legend' | 'label' }) {
|
||||||
return (
|
return (
|
||||||
<legend
|
<legend
|
||||||
data-slot="field-legend"
|
data-slot="field-legend"
|
||||||
data-variant={variant}
|
data-variant={variant}
|
||||||
className={cn(
|
className={cn(
|
||||||
"mb-3 font-medium",
|
'mb-1.5 font-medium data-[variant=label]:text-sm data-[variant=legend]:text-base',
|
||||||
"data-[variant=legend]:text-base",
|
className,
|
||||||
"data-[variant=label]:text-sm",
|
|
||||||
className
|
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function FieldGroup({ className, ...props }: React.ComponentProps<"div">) {
|
function FieldGroup({ className, ...props }: React.ComponentProps<'div'>) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-slot="field-group"
|
data-slot="field-group"
|
||||||
className={cn(
|
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",
|
'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
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const fieldVariants = cva(
|
const fieldVariants = cva('group/field flex w-full gap-2 data-[invalid=true]:text-destructive', {
|
||||||
"group/field data-[invalid=true]:text-destructive flex w-full gap-3",
|
variants: {
|
||||||
{
|
orientation: {
|
||||||
variants: {
|
vertical: 'flex-col *:w-full [&>.sr-only]:w-auto',
|
||||||
orientation: {
|
horizontal:
|
||||||
vertical: ["flex-col [&>*]:w-full [&>.sr-only]:w-auto"],
|
'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',
|
||||||
horizontal: [
|
responsive:
|
||||||
"flex-row items-center",
|
'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',
|
||||||
"[&>[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",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
},
|
||||||
orientation: "vertical",
|
defaultVariants: {
|
||||||
},
|
orientation: 'vertical',
|
||||||
}
|
},
|
||||||
)
|
})
|
||||||
|
|
||||||
function Field({
|
function Field({
|
||||||
className,
|
className,
|
||||||
orientation = "vertical",
|
orientation = 'vertical',
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<"div"> & VariantProps<typeof fieldVariants>) {
|
}: React.ComponentProps<'div'> & VariantProps<typeof fieldVariants>) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
role="group"
|
role="group"
|
||||||
|
|
@ -94,59 +82,52 @@ function Field({
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function FieldContent({ className, ...props }: React.ComponentProps<"div">) {
|
function FieldContent({ className, ...props }: React.ComponentProps<'div'>) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-slot="field-content"
|
data-slot="field-content"
|
||||||
className={cn(
|
className={cn('group/field-content flex flex-1 flex-col gap-0.5 leading-snug', className)}
|
||||||
"group/field-content flex flex-1 flex-col gap-1.5 leading-snug",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function FieldLabel({
|
function FieldLabel({ className, ...props }: React.ComponentProps<typeof Label>) {
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof Label>) {
|
|
||||||
return (
|
return (
|
||||||
<Label
|
<Label
|
||||||
data-slot="field-label"
|
data-slot="field-label"
|
||||||
className={cn(
|
className={cn(
|
||||||
"group/field-label peer/field-label flex w-fit gap-2 leading-snug group-data-[disabled=true]/field:opacity-50",
|
'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 has-[>[data-slot=field]]:rounded-md has-[>[data-slot=field]]:border [&>[data-slot=field]]:p-4",
|
'has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col',
|
||||||
"has-data-[state=checked]:bg-primary/5 has-data-[state=checked]:border-primary dark:has-data-[state=checked]:bg-primary/10",
|
className,
|
||||||
className
|
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function FieldTitle({ className, ...props }: React.ComponentProps<"div">) {
|
function FieldTitle({ className, ...props }: React.ComponentProps<'div'>) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-slot="field-label"
|
data-slot="field-label"
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex w-fit items-center gap-2 text-sm font-medium leading-snug group-data-[disabled=true]/field:opacity-50",
|
'flex w-fit items-center gap-2 text-sm leading-snug font-medium group-data-[disabled=true]/field:opacity-50',
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function FieldDescription({ className, ...props }: React.ComponentProps<"p">) {
|
function FieldDescription({ className, ...props }: React.ComponentProps<'p'>) {
|
||||||
return (
|
return (
|
||||||
<p
|
<p
|
||||||
data-slot="field-description"
|
data-slot="field-description"
|
||||||
className={cn(
|
className={cn(
|
||||||
"text-muted-foreground text-sm font-normal leading-normal group-has-[[data-orientation=horizontal]]/field:text-balance",
|
'text-left text-sm leading-normal font-normal text-muted-foreground group-has-data-horizontal/field:text-balance [[data-variant=legend]+&]:-mt-1.5',
|
||||||
"nth-last-2:-mt-1 last:mt-0 [[data-variant=legend]+&]:-mt-1.5",
|
'last:mt-0 nth-last-2:-mt-1',
|
||||||
"[&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4",
|
'[&>a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary',
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
|
|
@ -157,7 +138,7 @@ function FieldSeparator({
|
||||||
children,
|
children,
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<"div"> & {
|
}: React.ComponentProps<'div'> & {
|
||||||
children?: React.ReactNode
|
children?: React.ReactNode
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
|
|
@ -165,15 +146,15 @@ function FieldSeparator({
|
||||||
data-slot="field-separator"
|
data-slot="field-separator"
|
||||||
data-content={!!children}
|
data-content={!!children}
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative -my-2 h-5 text-sm group-data-[variant=outline]/field-group:-mb-2",
|
'relative -my-2 h-5 text-sm group-data-[variant=outline]/field-group:-mb-2',
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<Separator className="absolute inset-0 top-1/2" />
|
<Separator className="absolute inset-0 top-1/2" />
|
||||||
{children && (
|
{children && (
|
||||||
<span
|
<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"
|
data-slot="field-separator-content"
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|
@ -188,7 +169,7 @@ function FieldError({
|
||||||
children,
|
children,
|
||||||
errors,
|
errors,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<"div"> & {
|
}: React.ComponentProps<'div'> & {
|
||||||
errors?: Array<{ message?: string } | undefined>
|
errors?: Array<{ message?: string } | undefined>
|
||||||
}) {
|
}) {
|
||||||
const content = useMemo(() => {
|
const content = useMemo(() => {
|
||||||
|
|
@ -196,20 +177,19 @@ function FieldError({
|
||||||
return children
|
return children
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!errors) {
|
if (!errors?.length) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
if (errors?.length === 1 && errors[0]?.message) {
|
const uniqueErrors = [...new Map(errors.map((error) => [error?.message, error])).values()]
|
||||||
return errors[0].message
|
|
||||||
|
if (uniqueErrors?.length == 1) {
|
||||||
|
return uniqueErrors[0]?.message
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ul className="ml-4 flex list-disc flex-col gap-1">
|
<ul className="ml-4 flex list-disc flex-col gap-1">
|
||||||
{errors.map(
|
{uniqueErrors.map((error, index) => error?.message && <li key={index}>{error.message}</li>)}
|
||||||
(error, index) =>
|
|
||||||
error?.message && <li key={index}>{error.message}</li>
|
|
||||||
)}
|
|
||||||
</ul>
|
</ul>
|
||||||
)
|
)
|
||||||
}, [children, errors])
|
}, [children, errors])
|
||||||
|
|
@ -222,7 +202,7 @@ function FieldError({
|
||||||
<div
|
<div
|
||||||
role="alert"
|
role="alert"
|
||||||
data-slot="field-error"
|
data-slot="field-error"
|
||||||
className={cn("text-destructive text-sm font-normal", className)}
|
className={cn('text-sm font-normal text-destructive', className)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{content}
|
{content}
|
||||||
|
|
|
||||||
|
|
@ -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">>(
|
function Input({ className, type, ...props }: React.ComponentProps<'input'>) {
|
||||||
({ className, type, ...props }, ref) => {
|
return (
|
||||||
return (
|
<input
|
||||||
<input
|
type={type}
|
||||||
type={type}
|
data-slot="input"
|
||||||
className={cn(
|
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",
|
'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
|
className,
|
||||||
)}
|
)}
|
||||||
ref={ref}
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
)
|
||||||
)
|
}
|
||||||
}
|
|
||||||
)
|
|
||||||
Input.displayName = "Input"
|
|
||||||
|
|
||||||
export { Input }
|
export { Input }
|
||||||
|
|
|
||||||
|
|
@ -1,26 +1,24 @@
|
||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import * as LabelPrimitive from "@radix-ui/react-label"
|
import { Label as LabelPrimitive } from "radix-ui"
|
||||||
import { cva, type VariantProps } from "class-variance-authority"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
const labelVariants = cva(
|
function Label({
|
||||||
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
className,
|
||||||
)
|
...props
|
||||||
|
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
|
||||||
const Label = React.forwardRef<
|
return (
|
||||||
React.ElementRef<typeof LabelPrimitive.Root>,
|
<LabelPrimitive.Root
|
||||||
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
|
data-slot="label"
|
||||||
VariantProps<typeof labelVariants>
|
className={cn(
|
||||||
>(({ className, ...props }, ref) => (
|
"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",
|
||||||
<LabelPrimitive.Root
|
className
|
||||||
ref={ref}
|
)}
|
||||||
className={cn(labelVariants(), className)}
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
)
|
||||||
))
|
}
|
||||||
Label.displayName = LabelPrimitive.Root.displayName
|
|
||||||
|
|
||||||
export { Label }
|
export { Label }
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,18 @@
|
||||||
import * as React from "react"
|
import * as React from 'react'
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from '@/lib/utils'
|
||||||
import { ChevronDownIcon } from "lucide-react"
|
import { ChevronDownIcon } from 'lucide-react'
|
||||||
|
|
||||||
type NativeSelectProps = Omit<React.ComponentProps<"select">, "size"> & {
|
type NativeSelectProps = Omit<React.ComponentProps<'select'>, 'size'> & {
|
||||||
size?: "sm" | "default"
|
size?: 'sm' | 'default'
|
||||||
}
|
}
|
||||||
|
|
||||||
function NativeSelect({
|
function NativeSelect({ className, size = 'default', ...props }: NativeSelectProps) {
|
||||||
className,
|
|
||||||
size = "default",
|
|
||||||
...props
|
|
||||||
}: NativeSelectProps) {
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"group/native-select relative w-fit has-[select:disabled]:opacity-50",
|
'group/native-select relative w-fit has-[select:disabled]:opacity-50',
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
data-slot="native-select-wrapper"
|
data-slot="native-select-wrapper"
|
||||||
data-size={size}
|
data-size={size}
|
||||||
|
|
@ -24,29 +20,24 @@ function NativeSelect({
|
||||||
<select
|
<select
|
||||||
data-slot="native-select"
|
data-slot="native-select"
|
||||||
data-size={size}
|
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}
|
{...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>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function NativeSelectOption({ ...props }: React.ComponentProps<"option">) {
|
function NativeSelectOption({ ...props }: React.ComponentProps<'option'>) {
|
||||||
return <option data-slot="native-select-option" {...props} />
|
return <option data-slot="native-select-option" {...props} />
|
||||||
}
|
}
|
||||||
|
|
||||||
function NativeSelectOptGroup({
|
function NativeSelectOptGroup({ className, ...props }: React.ComponentProps<'optgroup'>) {
|
||||||
className,
|
return <optgroup data-slot="native-select-optgroup" className={cn(className)} {...props} />
|
||||||
...props
|
|
||||||
}: React.ComponentProps<"optgroup">) {
|
|
||||||
return (
|
|
||||||
<optgroup
|
|
||||||
data-slot="native-select-optgroup"
|
|
||||||
className={cn(className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export { NativeSelect, NativeSelectOptGroup, NativeSelectOption }
|
export { NativeSelect, NativeSelectOptGroup, NativeSelectOption }
|
||||||
|
|
|
||||||
|
|
@ -1,152 +1,185 @@
|
||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import * as SelectPrimitive from '@radix-ui/react-select'
|
import { Select as SelectPrimitive } from 'radix-ui'
|
||||||
import { Check, ChevronDown, ChevronUp } from 'lucide-react'
|
|
||||||
|
|
||||||
import { cn } from '@/lib/utils'
|
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<
|
function SelectTrigger({
|
||||||
React.ElementRef<typeof SelectPrimitive.Trigger>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
|
size = 'default',
|
||||||
>(({ className, children, ...props }, ref) => (
|
children,
|
||||||
<SelectPrimitive.Trigger
|
...props
|
||||||
ref={ref}
|
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
|
||||||
className={cn(
|
size?: 'sm' | 'default'
|
||||||
'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',
|
}) {
|
||||||
className,
|
return (
|
||||||
)}
|
<SelectPrimitive.Trigger
|
||||||
{...props}
|
data-slot="select-trigger"
|
||||||
>
|
data-size={size}
|
||||||
{children}
|
|
||||||
<SelectPrimitive.Icon asChild>
|
|
||||||
<ChevronDown className="h-4 w-4 opacity-50" />
|
|
||||||
</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) => (
|
|
||||||
<SelectPrimitive.Portal>
|
|
||||||
<SelectPrimitive.Content
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
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]',
|
"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",
|
||||||
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,
|
className,
|
||||||
)}
|
)}
|
||||||
position={position}
|
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<SelectScrollUpButton />
|
{children}
|
||||||
<SelectPrimitive.Viewport
|
<SelectPrimitive.Icon asChild>
|
||||||
|
<ChevronDownIcon className="pointer-events-none size-4 text-black/60" />
|
||||||
|
</SelectPrimitive.Icon>
|
||||||
|
</SelectPrimitive.Trigger>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectContent({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
position = 'item-aligned',
|
||||||
|
align = 'center',
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
|
||||||
|
return (
|
||||||
|
<SelectPrimitive.Portal>
|
||||||
|
<SelectPrimitive.Content
|
||||||
|
data-slot="select-content"
|
||||||
|
data-align-trigger={position === 'item-aligned'}
|
||||||
className={cn(
|
className={cn(
|
||||||
"p-1",
|
'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" &&
|
position === 'popper' &&
|
||||||
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
|
'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}
|
||||||
>
|
>
|
||||||
{children}
|
<SelectScrollUpButton />
|
||||||
</SelectPrimitive.Viewport>
|
<SelectPrimitive.Viewport
|
||||||
<SelectScrollDownButton />
|
data-position={position}
|
||||||
</SelectPrimitive.Content>
|
className={cn(
|
||||||
</SelectPrimitive.Portal>
|
'data-[position=popper]:h-(--radix-select-trigger-height) data-[position=popper]:w-full data-[position=popper]:min-w-(--radix-select-trigger-width)',
|
||||||
))
|
position === 'popper' && '',
|
||||||
SelectContent.displayName = SelectPrimitive.Content.displayName
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</SelectPrimitive.Viewport>
|
||||||
|
<SelectScrollDownButton />
|
||||||
|
</SelectPrimitive.Content>
|
||||||
|
</SelectPrimitive.Portal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const SelectLabel = React.forwardRef<
|
function SelectLabel({ className, ...props }: React.ComponentProps<typeof SelectPrimitive.Label>) {
|
||||||
React.ElementRef<typeof SelectPrimitive.Label>,
|
return (
|
||||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
|
<SelectPrimitive.Label
|
||||||
>(({ className, ...props }, ref) => (
|
data-slot="select-label"
|
||||||
<SelectPrimitive.Label
|
className={cn('px-1.5 py-1 text-xs text-muted-foreground', className)}
|
||||||
ref={ref}
|
{...props}
|
||||||
className={cn('px-2 py-1.5 text-sm font-semibold', className)}
|
/>
|
||||||
{...props}
|
)
|
||||||
/>
|
}
|
||||||
))
|
|
||||||
SelectLabel.displayName = SelectPrimitive.Label.displayName
|
|
||||||
|
|
||||||
const SelectItem = React.forwardRef<
|
function SelectItem({
|
||||||
React.ElementRef<typeof SelectPrimitive.Item>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
|
children,
|
||||||
>(({ className, children, ...props }, ref) => (
|
...props
|
||||||
<SelectPrimitive.Item
|
}: React.ComponentProps<typeof SelectPrimitive.Item>) {
|
||||||
ref={ref}
|
return (
|
||||||
className={cn(
|
<SelectPrimitive.Item
|
||||||
'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',
|
data-slot="select-item"
|
||||||
className,
|
className={cn(
|
||||||
)}
|
"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",
|
||||||
{...props}
|
className,
|
||||||
>
|
)}
|
||||||
<span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
|
{...props}
|
||||||
<SelectPrimitive.ItemIndicator>
|
>
|
||||||
<Check className="h-4 w-4" />
|
<span className="pointer-events-none absolute right-2 flex size-4 items-center justify-center">
|
||||||
</SelectPrimitive.ItemIndicator>
|
<SelectPrimitive.ItemIndicator>
|
||||||
</span>
|
<CheckIcon className="pointer-events-none" />
|
||||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
</SelectPrimitive.ItemIndicator>
|
||||||
</SelectPrimitive.Item>
|
</span>
|
||||||
))
|
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||||
SelectItem.displayName = SelectPrimitive.Item.displayName
|
</SelectPrimitive.Item>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const SelectSeparator = React.forwardRef<
|
function SelectSeparator({
|
||||||
React.ElementRef<typeof SelectPrimitive.Separator>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
|
...props
|
||||||
>(({ className, ...props }, ref) => (
|
}: React.ComponentProps<typeof SelectPrimitive.Separator>) {
|
||||||
<SelectPrimitive.Separator
|
return (
|
||||||
ref={ref}
|
<SelectPrimitive.Separator
|
||||||
className={cn('-mx-1 my-1 h-px bg-muted', className)}
|
data-slot="select-separator"
|
||||||
{...props}
|
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 {
|
export {
|
||||||
Select,
|
Select,
|
||||||
SelectGroup,
|
|
||||||
SelectValue,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectContent,
|
SelectContent,
|
||||||
SelectLabel,
|
SelectGroup,
|
||||||
SelectItem,
|
SelectItem,
|
||||||
SelectSeparator,
|
SelectLabel,
|
||||||
SelectScrollUpButton,
|
|
||||||
SelectScrollDownButton,
|
SelectScrollDownButton,
|
||||||
|
SelectScrollUpButton,
|
||||||
|
SelectSeparator,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,31 +1,28 @@
|
||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import * as React from "react"
|
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"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
const Separator = React.forwardRef<
|
function Separator({
|
||||||
React.ElementRef<typeof SeparatorPrimitive.Root>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
|
orientation = "horizontal",
|
||||||
>(
|
decorative = true,
|
||||||
(
|
...props
|
||||||
{ className, orientation = "horizontal", decorative = true, ...props },
|
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
|
||||||
ref
|
return (
|
||||||
) => (
|
|
||||||
<SeparatorPrimitive.Root
|
<SeparatorPrimitive.Root
|
||||||
ref={ref}
|
data-slot="separator"
|
||||||
decorative={decorative}
|
decorative={decorative}
|
||||||
orientation={orientation}
|
orientation={orientation}
|
||||||
className={cn(
|
className={cn(
|
||||||
"shrink-0 bg-border",
|
"shrink-0 bg-border data-horizontal:h-px data-horizontal:w-full data-vertical:w-px data-vertical:self-stretch",
|
||||||
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
|
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
)
|
}
|
||||||
Separator.displayName = SeparatorPrimitive.Root.displayName
|
|
||||||
|
|
||||||
export { Separator }
|
export { Separator }
|
||||||
|
|
|
||||||
|
|
@ -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 (
|
return (
|
||||||
<textarea
|
<textarea
|
||||||
data-slot="textarea"
|
data-slot="textarea"
|
||||||
className={cn(
|
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",
|
'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
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,7 @@ export const SiteSettings: GlobalConfig = {
|
||||||
{ label: 'Instagram', value: 'instagram' },
|
{ label: 'Instagram', value: 'instagram' },
|
||||||
{ label: 'Facebook', value: 'facebook' },
|
{ label: 'Facebook', value: 'facebook' },
|
||||||
{ label: 'YouTube', value: 'youtube' },
|
{ label: 'YouTube', value: 'youtube' },
|
||||||
|
{ label: 'WhatsApp', value: 'whatsapp' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
||||||
12
src/lib/schemas/comment.ts
Normal 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>
|
||||||
34
src/lib/schemas/contact.ts
Normal 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>
|
||||||
|
|
@ -1,40 +1,7 @@
|
||||||
import { CitiesByState } from '@/lib/cities'
|
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
|
import { contactSchema } from './contact'
|
||||||
|
import { commentSchema } from './comment'
|
||||||
|
|
||||||
type StateKey = keyof typeof CitiesByState
|
export const petitionFormSchema = contactSchema.extend(commentSchema.shape)
|
||||||
|
|
||||||
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 type PetitionFormInput = z.infer<typeof petitionFormSchema>
|
export type PetitionFormInput = z.infer<typeof petitionFormSchema>
|
||||||
|
|
|
||||||
9
src/lib/schemas/whatsapp-form.ts
Normal 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>
|
||||||
|
|
@ -73,6 +73,7 @@ export interface Config {
|
||||||
contact: Contact;
|
contact: Contact;
|
||||||
consent: Consent;
|
consent: Consent;
|
||||||
signature: Signature;
|
signature: Signature;
|
||||||
|
subscription: Subscription;
|
||||||
'payload-kv': PayloadKv;
|
'payload-kv': PayloadKv;
|
||||||
'payload-jobs': PayloadJob;
|
'payload-jobs': PayloadJob;
|
||||||
'payload-locked-documents': PayloadLockedDocument;
|
'payload-locked-documents': PayloadLockedDocument;
|
||||||
|
|
@ -87,6 +88,7 @@ export interface Config {
|
||||||
contact: ContactSelect<false> | ContactSelect<true>;
|
contact: ContactSelect<false> | ContactSelect<true>;
|
||||||
consent: ConsentSelect<false> | ConsentSelect<true>;
|
consent: ConsentSelect<false> | ConsentSelect<true>;
|
||||||
signature: SignatureSelect<false> | SignatureSelect<true>;
|
signature: SignatureSelect<false> | SignatureSelect<true>;
|
||||||
|
subscription: SubscriptionSelect<false> | SubscriptionSelect<true>;
|
||||||
'payload-kv': PayloadKvSelect<false> | PayloadKvSelect<true>;
|
'payload-kv': PayloadKvSelect<false> | PayloadKvSelect<true>;
|
||||||
'payload-jobs': PayloadJobsSelect<false> | PayloadJobsSelect<true>;
|
'payload-jobs': PayloadJobsSelect<false> | PayloadJobsSelect<true>;
|
||||||
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
|
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
|
||||||
|
|
@ -270,7 +272,6 @@ export interface Contact {
|
||||||
| 'TO';
|
| 'TO';
|
||||||
city: string;
|
city: string;
|
||||||
postalCode?: string | null;
|
postalCode?: string | null;
|
||||||
comment?: string | null;
|
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
}
|
}
|
||||||
|
|
@ -283,6 +284,19 @@ export interface Signature {
|
||||||
contact: number | Contact;
|
contact: number | Contact;
|
||||||
petition: string | Petition;
|
petition: string | Petition;
|
||||||
consent: number | Consent;
|
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;
|
updatedAt: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
}
|
}
|
||||||
|
|
@ -425,6 +439,10 @@ export interface PayloadLockedDocument {
|
||||||
| ({
|
| ({
|
||||||
relationTo: 'signature';
|
relationTo: 'signature';
|
||||||
value: number | Signature;
|
value: number | Signature;
|
||||||
|
} | null)
|
||||||
|
| ({
|
||||||
|
relationTo: 'subscription';
|
||||||
|
value: number | Subscription;
|
||||||
} | null);
|
} | null);
|
||||||
globalSlug?: string | null;
|
globalSlug?: string | null;
|
||||||
user: {
|
user: {
|
||||||
|
|
@ -540,7 +558,6 @@ export interface ContactSelect<T extends boolean = true> {
|
||||||
state?: T;
|
state?: T;
|
||||||
city?: T;
|
city?: T;
|
||||||
postalCode?: T;
|
postalCode?: T;
|
||||||
comment?: T;
|
|
||||||
updatedAt?: T;
|
updatedAt?: T;
|
||||||
createdAt?: T;
|
createdAt?: T;
|
||||||
}
|
}
|
||||||
|
|
@ -561,6 +578,18 @@ export interface SignatureSelect<T extends boolean = true> {
|
||||||
contact?: T;
|
contact?: T;
|
||||||
petition?: T;
|
petition?: T;
|
||||||
consent?: 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;
|
updatedAt?: T;
|
||||||
createdAt?: T;
|
createdAt?: T;
|
||||||
}
|
}
|
||||||
|
|
@ -644,7 +673,7 @@ export interface SiteSetting {
|
||||||
headerTitle?: string | null;
|
headerTitle?: string | null;
|
||||||
socialLinks?:
|
socialLinks?:
|
||||||
| {
|
| {
|
||||||
platform: 'instagram' | 'facebook' | 'youtube';
|
platform: 'instagram' | 'facebook' | 'youtube' | 'whatsapp';
|
||||||
url: string;
|
url: string;
|
||||||
/**
|
/**
|
||||||
* Rótulo opcional para acessibilidade. Se estiver vazio, usamos um rótulo padrão da plataforma.
|
* Rótulo opcional para acessibilidade. Se estiver vazio, usamos um rótulo padrão da plataforma.
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ import { Contact } from './collections/Contact'
|
||||||
import { Consent } from './collections/Consent'
|
import { Consent } from './collections/Consent'
|
||||||
import { Signature } from './collections/Signature'
|
import { Signature } from './collections/Signature'
|
||||||
import { vercelBlobStorage } from '@payloadcms/storage-vercel-blob'
|
import { vercelBlobStorage } from '@payloadcms/storage-vercel-blob'
|
||||||
|
import { Subscription } from './collections/Subscription'
|
||||||
|
|
||||||
const filename = fileURLToPath(import.meta.url)
|
const filename = fileURLToPath(import.meta.url)
|
||||||
const dirname = path.dirname(filename)
|
const dirname = path.dirname(filename)
|
||||||
|
|
@ -31,7 +32,7 @@ export default buildConfig({
|
||||||
globals: [SiteSettings.slug, HomePage.slug],
|
globals: [SiteSettings.slug, HomePage.slug],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
collections: [Users, Media, Petition, Contact, Consent, Signature],
|
collections: [Users, Media, Petition, Contact, Consent, Signature, Subscription],
|
||||||
globals: [SiteSettings, HomePage, Metadata],
|
globals: [SiteSettings, HomePage, Metadata],
|
||||||
editor: lexicalEditor(),
|
editor: lexicalEditor(),
|
||||||
secret: process.env.PAYLOAD_SECRET || '',
|
secret: process.env.PAYLOAD_SECRET || '',
|
||||||
|
|
|
||||||