From f9a2a3113a2d85951997d87b0d046207f490e895 Mon Sep 17 00:00:00 2001 From: Ruan Bueno Date: Fri, 12 Dec 2025 15:49:06 -0300 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20reajustando=20componentes=20errados?= =?UTF-8?q?=20+=20adicionando=20novas=20anima=C3=A7=C3=B5es=20e=20fallback?= =?UTF-8?q?=20para=20localiza=C3=A7=C3=A3o=20do=20componente=20widget=20de?= =?UTF-8?q?=20tempo=20+=20ajuste=20no=20componente=20de=20scroll=20de=20v?= =?UTF-8?q?=C3=ADdeos=20na=20vers=C3=A3o=20mobile?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/(authenticated)/dashboard/page.tsx | 267 ++++++++++++------ .../dashboard/suggestions-wrapper.tsx | 19 +- .../admin/suggestion/suggestion-card.tsx | 47 ++- src/components/dashboard/videos-carousel.tsx | 28 +- src/components/dashboard/weather-widget.tsx | 207 +++++++++++++- src/styles/globals.css | 64 +++++ 6 files changed, 477 insertions(+), 155 deletions(-) diff --git a/src/app/(authenticated)/dashboard/page.tsx b/src/app/(authenticated)/dashboard/page.tsx index 24bd1f88..0cb92eb2 100644 --- a/src/app/(authenticated)/dashboard/page.tsx +++ b/src/app/(authenticated)/dashboard/page.tsx @@ -12,8 +12,7 @@ import { routeItems } from "@/const/routes" import { FaInstagram, FaFacebook, FaYoutube } from "react-icons/fa6" import Image from "next/image" import { DashboardShell } from "@/components/dashboard-shell" -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" -import { WeatherWidget } from "@/components/dashboard/weather-widget" +import { Separator } from "@/components/ui/separator" import { SuggestionsWrapper } from "./suggestions-wrapper" import { CompleteProfileModal } from "@/components/complete-profile-modal" @@ -136,95 +135,187 @@ export default function DashboardPage() { {/* Links Úteis e Ideias - Dividido em duas colunas */}
- {/* Links Úteis */} + {/* Links Úteis - Desktop: lado a lado, Mobile: Sites primeiro, depois Treinamento */}
Links Úteis
- - - - Treinamento - - - Sites - - - - - Umentor - Umentor - - - - CristalUni - - - - - Site Box - Site Box - - - Cristallux - Cristallux - - - Central do Colaborador - Central do Colaborador - - - + + {/* Mobile: Sites primeiro, Divisor, Treinamento */} +
+ {/* Sites */} +
+

Sites

+
+ + Site Box + Site Box + + + Cristallux + Cristallux + + + Central do Colaborador + Central do Colaborador + +
+
+ + {/* Divisor */} + + + {/* Treinamento */} +
+

Treinamento

+
+ + Umentor + Umentor + + + + CristalUni + +
+
+
+ + {/* Desktop: Sites e Treinamento lado a lado */} +
+ {/* Sites */} +
+

Sites

+
+ + Site Box + Site Box + + + Cristallux + Cristallux + + + Central do Colaborador + Central do Colaborador + +
+
+ + {/* Treinamento */} +
+

Treinamento

+
+ + Umentor + Umentor + + + + CristalUni + +
+
+
{/* Card de Ideias */} @@ -236,11 +327,11 @@ export default function DashboardPage() {
-
+
{/* Seção de Conteúdo - Video e Clima */} { videos.length > 0 && ( - + ) }
diff --git a/src/app/(authenticated)/dashboard/suggestions-wrapper.tsx b/src/app/(authenticated)/dashboard/suggestions-wrapper.tsx index e25b55b0..222bc9ca 100644 --- a/src/app/(authenticated)/dashboard/suggestions-wrapper.tsx +++ b/src/app/(authenticated)/dashboard/suggestions-wrapper.tsx @@ -3,27 +3,20 @@ import { useState } from "react" import { SuggestionsPreview, SuggestionsModal } from "@/components/admin/suggestion/suggestion-card" import { api } from "@/trpc/react" -import { Card, CardContent } from "@/components/ui/card" export function SuggestionsWrapper() { const [suggestionsModalOpen, setSuggestionsModalOpen] = useState(false) const { data: userData } = api.user.me.useQuery() const isTotem = userData?.role_config?.isTotem === true + // Não exibir nada para usuários Totem + if (isTotem) { + return null + } + return ( <> - {isTotem ? ( - - -

- Usuários Totem não podem submeter ideias. -

-
-
- ) : ( - setSuggestionsModalOpen(true)} /> - )} - + setSuggestionsModalOpen(true)} /> void }) const { data: userData } = api.user.me.useQuery() const isTotem = userData?.role_config?.isTotem === true + // Não renderizar nada para usuários Totem + if (isTotem) { + return null + } + return (
{ - if (isTotem) { - toast({ - title: "Acesso restrito", - description: "Usuários Totem não podem submeter ideias.", - variant: "destructive" - }) - return - } onOpenModal() }} className="cursor-pointer bg-muted hover:bg-muted/80 rounded-lg p-3 md:p-4 border hover:shadow-md transition-all duration-200 w-full group" @@ -187,20 +184,11 @@ export function SuggestionsCard() { const userSector = userData?.setor ?? "Não informado" + // Não renderizar nada para usuários Totem if (isTotem) { - return ( - - -
-

Caixa de Ideias

-

- Usuários Totem não podem submeter ideias. Para enviar sugestões, utilize uma conta de colaborador. -

-
-
-
- ) + return null } + return ( @@ -361,6 +349,13 @@ export function SuggestionsModal({ isOpen, onOpenChange }: { isOpen: boolean; on const { data: userData, isLoading: userLoading } = api.user.me.useQuery() const isTotem = userData?.role_config?.isTotem === true + // Fechar modal automaticamente se for usuário Totem + useEffect(() => { + if (isTotem && isOpen) { + onOpenChange(false) + } + }, [isTotem, isOpen, onOpenChange]) + // Pré-preencher o nome quando os dados do usuário chegarem useEffect(() => { if (userData) { @@ -458,16 +453,8 @@ export function SuggestionsModal({ isOpen, onOpenChange }: { isOpen: boolean; on
) : isTotem ? ( -
-

- Usuários Totem não podem submeter ideias. Para enviar sugestões, utilize uma conta de colaborador. -

-
- -
-
+ // Não renderizar nada para usuários Totem, apenas fechar o modal + null ) : (
{/* Informações do usuário */} diff --git a/src/components/dashboard/videos-carousel.tsx b/src/components/dashboard/videos-carousel.tsx index bcff3f20..6e348719 100644 --- a/src/components/dashboard/videos-carousel.tsx +++ b/src/components/dashboard/videos-carousel.tsx @@ -6,6 +6,7 @@ import { OptimizedImage } from "@/components/ui/optimized-image" import Link from "next/link" import { useEffect, useState } from "react" import { WeatherWidget } from "@/components/dashboard/weather-widget" +import { type Enterprise } from "@prisma/client" interface MainCarouselProps { itens: { @@ -13,9 +14,10 @@ interface MainCarouselProps { title: string }[], className?: string + enterprise?: Enterprise | null } -export function VideosCarousel({ itens, className }: MainCarouselProps) { +export function VideosCarousel({ itens, className, enterprise }: MainCarouselProps) { const [carouselApi, setCarouselApi] = useState() const [current, setCurrent] = useState(0) const [count, setCount] = useState(0) @@ -36,16 +38,16 @@ export function VideosCarousel({ itens, className }: MainCarouselProps) { return (
{/* Grid estático no desktop - 3 colunas */} -
+
{/* Primeira coluna - Vídeos */} {itens.map((item, index) => ( -
+
@@ -54,7 +56,7 @@ export function VideosCarousel({ itens, className }: MainCarouselProps) { ))} {/* Segunda coluna - Banner */} -
+
@@ -63,9 +65,9 @@ export function VideosCarousel({ itens, className }: MainCarouselProps) {
{/* Terceira coluna - WeatherWidget */} -
+
- +
@@ -78,13 +80,14 @@ export function VideosCarousel({ itens, className }: MainCarouselProps) { opts={{ loop: true, align: "center", - inViewThreshold: 1 + inViewThreshold: 1, + dragFree: true }} > - + {itens.map((item, index) => ( - -
+ +
@@ -105,7 +109,7 @@ export function VideosCarousel({ itens, className }: MainCarouselProps) {
- +
diff --git a/src/components/dashboard/weather-widget.tsx b/src/components/dashboard/weather-widget.tsx index 6ce1186d..407cc33a 100644 --- a/src/components/dashboard/weather-widget.tsx +++ b/src/components/dashboard/weather-widget.tsx @@ -1,9 +1,10 @@ "use client" -import { useEffect, useState } from "react" +import { useEffect, useState, useMemo } from "react" import { Cloud, CloudRain, Sun, CloudSun, Droplets, Wind, MapPin } from "lucide-react" import { Skeleton } from "@/components/ui/skeleton" import { cn } from "@/lib/utils" +import { type Enterprise } from "@prisma/client" interface WeatherData { temperature: number @@ -16,6 +17,27 @@ interface WeatherData { interface WeatherWidgetProps { className?: string + enterprise?: Enterprise | null +} + +// Função para obter coordenadas padrão baseadas na empresa +const getDefaultLocationByEnterprise = (enterprise: Enterprise | null | undefined): { lat: number; lon: number } => { + switch (enterprise) { + case "Box": + case "RHenz": + case "Cristallux": + // Santa Cruz do Sul + return { lat: -29.7175, lon: -52.4258 } + case "Box_Filial": + // Venâncio Aires + return { lat: -29.6064, lon: -52.1931 } + case "Cristallux_Filial": + // Cachoeirinha + return { lat: -29.9508, lon: -51.0939 } + default: + // Fallback padrão (Santa Cruz do Sul) + return { lat: -29.7175, lon: -52.4258 } + } } interface GeocodingResult { @@ -70,7 +92,7 @@ const getWeatherDescription = (code: number): string => { return "Condições variáveis" } -export function WeatherWidget({ className }: WeatherWidgetProps) { +export function WeatherWidget({ className, enterprise }: WeatherWidgetProps) { const [weather, setWeather] = useState(null) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) @@ -89,15 +111,17 @@ export function WeatherWidget({ className }: WeatherWidgetProps) { }, (err) => { console.error("Erro ao obter localização:", err) - // Fallback para coordenadas padrão (São Paulo) - setLocation({ lat: -23.5505, lon: -46.6333 }) + // Fallback para coordenadas baseadas na empresa + const defaultLocation = getDefaultLocationByEnterprise(enterprise) + setLocation(defaultLocation) } ) } else { - // Fallback para coordenadas padrão (São Paulo) - setLocation({ lat: -23.5505, lon: -46.6333 }) + // Fallback para coordenadas baseadas na empresa + const defaultLocation = getDefaultLocationByEnterprise(enterprise) + setLocation(defaultLocation) } - }, []) + }, [enterprise]) // Buscar nome da cidade useEffect(() => { @@ -192,6 +216,41 @@ export function WeatherWidget({ className }: WeatherWidgetProps) { void fetchWeather() }, [location]) + // Valores aleatórios para animações (calculados uma vez) - DEVE VIR ANTES DOS EARLY RETURNS + const sunParticles = useMemo(() => + Array.from({ length: 15 }).map(() => ({ + left: Math.random() * 100, + top: Math.random() * 100, + delay: Math.random() * 3, + duration: 3 + Math.random() * 2 + })), []) + + const windParticles = useMemo(() => + Array.from({ length: 20 }).map(() => ({ + left: Math.random() * 100, + top: Math.random() * 100, + duration: 2 + Math.random() * 2, + delay: Math.random() * 2, + translateX: Math.random() * 50 - 25 + })), []) + + const windLines = useMemo(() => + Array.from({ length: 8 }).map((_, i) => ({ + duration: 3 + Math.random() * 2, + delay: i * 0.3 + })), []) + + const cloudPositions = useMemo(() => + Array.from({ length: 3 }).map((_, i) => ({ + top: 10 + i * 15 + Math.random() * 10, + delay: i * 2 + Math.random() * 2 + })), []) + + const rainDrops = useMemo(() => + Array.from({ length: 30 }).map(() => ({ + duration: 0.4 + Math.random() * 0.3 + })), []) + if (loading) { return (
@@ -213,25 +272,149 @@ export function WeatherWidget({ className }: WeatherWidgetProps) { return null // Não exibe nada em caso de erro } + // Determinar tipo de clima para animações + const isSunny = weather.weatherCode === 0 + const isMostlyClear = weather.weatherCode === 1 + const isPartlyCloudy = weather.weatherCode === 2 + const isCloudy = weather.weatherCode === 3 + const isRainy = weather.weatherCode >= 51 && weather.weatherCode <= 99 + + // Gradiente baseado no clima + const getGradientClass = () => { + if (isSunny) return "bg-gradient-to-br from-yellow-50 via-orange-50 to-yellow-100 dark:from-yellow-950 dark:via-orange-950 dark:to-yellow-900" + if (isMostlyClear || isPartlyCloudy) return "bg-gradient-to-br from-blue-50 via-sky-50 to-blue-100 dark:from-blue-950 dark:via-sky-950 dark:to-blue-900" + if (isCloudy) return "bg-gradient-to-br from-gray-100 via-gray-200 to-gray-300 dark:from-gray-800 dark:via-gray-700 dark:to-gray-900" + if (isRainy) return "bg-gradient-to-br from-blue-50 to-blue-100 dark:from-blue-950 dark:to-blue-900" + return "bg-gradient-to-br from-blue-50 to-blue-100 dark:from-blue-950 dark:to-blue-900" + } + return ( -
- {/* Animação de fundo baseada no clima */} - {weather.weatherCode >= 51 && weather.weatherCode <= 99 && ( +
+ {/* Animação de chuva */} + {isRainy && (
- {Array.from({ length: 30 }).map((_, i) => ( + {rainDrops.map((drop, i) => (
))}
)} + + {/* Animação de raios de sol para dias ensolarados */} + {isSunny && ( +
+ {Array.from({ length: 12 }).map((_, i) => { + const angle = (i * 30) * (Math.PI / 180) + const radius = 60 + const x = 50 + Math.cos(angle) * radius + const y = 50 + Math.sin(angle) * radius + return ( +
+ ) + })} + {/* Partículas de luz flutuantes */} + {sunParticles.map((particle, i) => ( +
+ ))} +
+ )} + + {/* Animação de nuvens e vento para dias nublados */} + {(isCloudy || isPartlyCloudy) && ( +
+ {/* Nuvens se movendo */} + {cloudPositions.map((cloud, i) => ( +
+ +
+ ))} + {/* Partículas de vento */} + {windParticles.map((particle, i) => ( +
+ ))} + {/* Linhas de vento */} + {windLines.map((line, i) => ( +
+ ))} +
+ )} + + {/* Animação sutil de sol para dias parcialmente nublados */} + {(isMostlyClear || isPartlyCloudy) && ( +
+ {Array.from({ length: 8 }).map((_, i) => ( +
+ ))} +
+ )}
diff --git a/src/styles/globals.css b/src/styles/globals.css index 812d188b..f1e8c4e4 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -209,6 +209,70 @@ animation: rain linear infinite; } +/* Animações adicionais para Widget de Clima */ +@keyframes cloudMove { + 0% { + transform: translateX(-100px); + opacity: 0; + } + 10% { + opacity: 0.2; + } + 90% { + opacity: 0.2; + } + 100% { + transform: translateX(calc(100vw + 100px)); + opacity: 0; + } +} + +@keyframes windParticle { + 0% { + transform: translateX(0) translateY(0); + opacity: 0; + } + 10% { + opacity: 0.3; + } + 50% { + opacity: 0.5; + } + 90% { + opacity: 0.3; + } + 100% { + transform: translateX(50px) translateY(-30px); + opacity: 0; + } +} + +@keyframes windLine { + 0% { + transform: translateX(0) scaleX(0.5); + opacity: 0.1; + } + 50% { + transform: translateX(20px) scaleX(1); + opacity: 0.3; + } + 100% { + transform: translateX(40px) scaleX(0.5); + opacity: 0.1; + } +} + +@keyframes sunRays { + 0%, 100% { + opacity: 0.2; + transform: rotate(0deg) translateY(-30px) scale(1); + } + 50% { + opacity: 0.4; + transform: rotate(180deg) translateY(-30px) scale(1.2); + } +} + /* Estilos para Sonner Toaster em mobile */ [data-sonner-toaster] { z-index: 9999 !important; From c340c78539fd24d292e8d509386f230c79bc2e7e Mon Sep 17 00:00:00 2001 From: Ruan Bueno Date: Fri, 12 Dec 2025 16:18:33 -0300 Subject: [PATCH 2/2] =?UTF-8?q?feat:=20reajustando=20componentes=20errados?= =?UTF-8?q?=20+=20adicionando=20novas=20anima=C3=A7=C3=B5es=20e=20fallback?= =?UTF-8?q?=20para=20localiza=C3=A7=C3=A3o=20do=20componente=20widget=20de?= =?UTF-8?q?=20tempo=20+=20ajuste=20no=20componente=20de=20scroll=20de=20v?= =?UTF-8?q?=C3=ADdeos=20na=20vers=C3=A3o=20mobile?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/dashboard/weather-widget.tsx | 40 ++++++++++++--------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/src/components/dashboard/weather-widget.tsx b/src/components/dashboard/weather-widget.tsx index 407cc33a..17acabe1 100644 --- a/src/components/dashboard/weather-widget.tsx +++ b/src/components/dashboard/weather-widget.tsx @@ -4,7 +4,7 @@ import { useEffect, useState, useMemo } from "react" import { Cloud, CloudRain, Sun, CloudSun, Droplets, Wind, MapPin } from "lucide-react" import { Skeleton } from "@/components/ui/skeleton" import { cn } from "@/lib/utils" -import { type Enterprise } from "@prisma/client" +import { Enterprise } from "@prisma/client" interface WeatherData { temperature: number @@ -22,22 +22,30 @@ interface WeatherWidgetProps { // Função para obter coordenadas padrão baseadas na empresa const getDefaultLocationByEnterprise = (enterprise: Enterprise | null | undefined): { lat: number; lon: number } => { - switch (enterprise) { - case "Box": - case "RHenz": - case "Cristallux": - // Santa Cruz do Sul - return { lat: -29.7175, lon: -52.4258 } - case "Box_Filial": - // Venâncio Aires - return { lat: -29.6064, lon: -52.1931 } - case "Cristallux_Filial": - // Cachoeirinha - return { lat: -29.9508, lon: -51.0939 } - default: - // Fallback padrão (Santa Cruz do Sul) - return { lat: -29.7175, lon: -52.4258 } + // Tratar null/undefined primeiro + if (!enterprise) { + return { lat: -29.7175, lon: -52.4258 } // Fallback padrão (Santa Cruz do Sul) } + + // Comparar o valor do enum (Prisma enum é um tipo union de strings literais) + // Usar comparação direta com strings, pois o enum Enterprise é equivalente a: "NA" | "Box" | "RHenz" | "Cristallux" | "Box_Filial" | "Cristallux_Filial" + if (enterprise === Enterprise.Box || enterprise === Enterprise.RHenz || enterprise === Enterprise.Cristallux) { + // Santa Cruz do Sul + return { lat: -29.7175, lon: -52.4258 } + } + + if (enterprise === Enterprise.Box_Filial) { + // Venâncio Aires + return { lat: -29.6064, lon: -52.1931 } + } + + if (enterprise === Enterprise.Cristallux_Filial) { + // Cachoeirinha + return { lat: -29.9508, lon: -51.0939 } + } + + // Fallback padrão (Santa Cruz do Sul) + return { lat: -29.7175, lon: -52.4258 } } interface GeocodingResult {