From 985f5ffb6944a93355522b2ea59bfd77f6564d10 Mon Sep 17 00:00:00 2001 From: Xavier Oliveira Date: Fri, 29 May 2026 17:59:35 +0100 Subject: [PATCH] feat: videos navugation by order --- .../components/animatedNumberCount/index.tsx | 23 ++ .../animatedNumberCount/styles.module.css | 0 .../components/animatedProgressBar/index.tsx | 31 +++ .../animatedProgressBar/styles.module.css | 0 .../src/components/chatbot/index.tsx | 66 +++++- .../src/components/chatbot/n8n-chat-theme.css | 6 +- .../src/components/header/index.tsx | 8 +- .../src/components/header/styles.module.css | 4 - .../src/components/sidebar/index.tsx | 200 ++++++++++++------ .../src/components/sidebar/styles.module.css | 4 +- .../src/pages/private/dashboard/index.tsx | 9 +- .../src/pages/private/profile/index.tsx | 4 +- .../src/pages/private/video/[id].tsx | 21 +- .../src/pages/private/video/styles.module.css | 19 +- frontend-plataforma-tutoriais/src/types.tsx | 2 + 15 files changed, 306 insertions(+), 91 deletions(-) create mode 100644 frontend-plataforma-tutoriais/src/components/animatedNumberCount/index.tsx create mode 100644 frontend-plataforma-tutoriais/src/components/animatedNumberCount/styles.module.css create mode 100644 frontend-plataforma-tutoriais/src/components/animatedProgressBar/index.tsx create mode 100644 frontend-plataforma-tutoriais/src/components/animatedProgressBar/styles.module.css diff --git a/frontend-plataforma-tutoriais/src/components/animatedNumberCount/index.tsx b/frontend-plataforma-tutoriais/src/components/animatedNumberCount/index.tsx new file mode 100644 index 0000000..06c3f7d --- /dev/null +++ b/frontend-plataforma-tutoriais/src/components/animatedNumberCount/index.tsx @@ -0,0 +1,23 @@ +import { useMotionValue, useSpring } from "framer-motion"; +import { useEffect, useState } from "react"; + +const AnimatedCounter = ({ value }: { value: number }) => { + const motionValue = useMotionValue(0); + const spring = useSpring(motionValue, { stiffness: 60, damping: 20 }); + const [display, setDisplay] = useState(0); + + useEffect(() => { + motionValue.set(value); + }, [value]); + + useEffect(() => { + const unsubscribe = spring.on("change", (v) => { + setDisplay(Math.round(v)); + }); + return unsubscribe; + }, [spring]); + + return {display}; +}; + +export default AnimatedCounter; \ No newline at end of file diff --git a/frontend-plataforma-tutoriais/src/components/animatedNumberCount/styles.module.css b/frontend-plataforma-tutoriais/src/components/animatedNumberCount/styles.module.css new file mode 100644 index 0000000..e69de29 diff --git a/frontend-plataforma-tutoriais/src/components/animatedProgressBar/index.tsx b/frontend-plataforma-tutoriais/src/components/animatedProgressBar/index.tsx new file mode 100644 index 0000000..3eacb63 --- /dev/null +++ b/frontend-plataforma-tutoriais/src/components/animatedProgressBar/index.tsx @@ -0,0 +1,31 @@ +import { useMotionValue, useSpring } from "framer-motion"; +import { useEffect, useState } from "react"; +import { ProgressBar } from "react-bootstrap"; + +const AnimatedProgressBar = ({ value, className }: { value: number; className?: string }) => { + const motionValue = useMotionValue(0); + const spring = useSpring(motionValue, { stiffness: 60, damping: 20 }); + const [display, setDisplay] = useState(0); + + useEffect(() => { + motionValue.set(value); + }, [value]); + + useEffect(() => { + // ✅ subscribe mantém o state sincronizado com o spring + const unsubscribe = spring.on("change", (v) => { + setDisplay(Math.round(v)); + }); + return unsubscribe; + }, [spring]); + + return ( + + ); +}; + +export default AnimatedProgressBar; \ No newline at end of file diff --git a/frontend-plataforma-tutoriais/src/components/animatedProgressBar/styles.module.css b/frontend-plataforma-tutoriais/src/components/animatedProgressBar/styles.module.css new file mode 100644 index 0000000..e69de29 diff --git a/frontend-plataforma-tutoriais/src/components/chatbot/index.tsx b/frontend-plataforma-tutoriais/src/components/chatbot/index.tsx index e186711..2d2ef55 100644 --- a/frontend-plataforma-tutoriais/src/components/chatbot/index.tsx +++ b/frontend-plataforma-tutoriais/src/components/chatbot/index.tsx @@ -2,10 +2,25 @@ import { useEffect } from 'react'; import '@n8n/chat/style.css'; import { createChat } from '@n8n/chat'; import './n8n-chat-theme.css'; +import { motion } from 'framer-motion'; export default function N8nChat() { useEffect(() => { const style = document.createElement('style'); + style.innerHTML = ` + /* 1. TORNAMOS O BOTÃO DA N8N TRANSPARENTE */ + #n8n-chat .n8n-chat-toggle-button { + background-color: transparent !important; + background-image: none !important; + border: none !important; + box-shadow: none !important; + } + + /* Garante que o ícone branco fica bem visível sobre a nossa nova sombra */ + #n8n-chat .n8n-chat-toggle-button svg { + filter: drop-shadow(0px 2px 4px rgba(0, 0, 0, 0.2)); + } + `; document.head.appendChild(style); const app = createChat({ @@ -27,13 +42,56 @@ export default function N8nChat() { return () => { app.unmount(); + document.head.removeChild(style); }; }, []); return ( -
+
+ + {/* 2. ESTA AGORA É A VERDADEIRA E ÚNICA FORMA DO BOTÃO */} + + + {/* CONTAINER DO CHAT DA N8N */} +
+
); } \ No newline at end of file diff --git a/frontend-plataforma-tutoriais/src/components/chatbot/n8n-chat-theme.css b/frontend-plataforma-tutoriais/src/components/chatbot/n8n-chat-theme.css index 0fe3c28..ba252c8 100644 --- a/frontend-plataforma-tutoriais/src/components/chatbot/n8n-chat-theme.css +++ b/frontend-plataforma-tutoriais/src/components/chatbot/n8n-chat-theme.css @@ -17,8 +17,7 @@ --chat--message--border-radius: var(--border-radius); --chat--message--border-color: var(--shadow-primary); --chat--input--send--button--color: var(--primary-color); - --chat--toggle--background: var(--primary-color); - --chat--toggle--border: 2px solid var(--primary-color); + --chat--toggle--background: transparent; } .chat-window-wrapper .chat-window-toggle { @@ -29,8 +28,7 @@ } .chat-window-wrapper .chat-window-toggle:hover { - background-color: var(--bg-grey) !important; - color: var(--text-primary-color) !important; + background-color: transparent !important; } .chat-layout .chat-header h1 { diff --git a/frontend-plataforma-tutoriais/src/components/header/index.tsx b/frontend-plataforma-tutoriais/src/components/header/index.tsx index 66d5d7b..0de9128 100644 --- a/frontend-plataforma-tutoriais/src/components/header/index.tsx +++ b/frontend-plataforma-tutoriais/src/components/header/index.tsx @@ -15,7 +15,7 @@ import { useGetWorkshopsSearch } from "../../hooks/useGetWorkshopsSearch"; import { imageSkeletonFadeStyle, onImageSkeletonLoad } from "../../utils/imageSkeleton"; import { motion, AnimatePresence } from "framer-motion"; import { useGetCurrentUser } from "../../hooks/useGetCurrentUser"; - +import AnimatedCounter from "../animatedNumberCount"; export default function Header() { const [showMenu, setShowMenu] = useState(false); @@ -166,7 +166,7 @@ export default function Header() { {role !== 1 && videosCount > 0 && (
Vídeos assistidos - {videosWatched}/{videosCount} + /
)} @@ -183,7 +183,7 @@ export default function Header() { <>
Vídeos assistidos - {videosWatched}/{videosCount} + /
)} @@ -225,7 +225,7 @@ export default function Header() {
{videosSearched.length > 0 && ( - Vídeos encontrados: {videosSearched.length} + Vídeos encontrados: )} {videosSearched.map((video) => ( diff --git a/frontend-plataforma-tutoriais/src/components/header/styles.module.css b/frontend-plataforma-tutoriais/src/components/header/styles.module.css index 4c28d7e..cb091e9 100644 --- a/frontend-plataforma-tutoriais/src/components/header/styles.module.css +++ b/frontend-plataforma-tutoriais/src/components/header/styles.module.css @@ -61,12 +61,8 @@ .logo { width: 180px; - aspect-ratio: 3 / 1; height: auto; object-fit: contain; - background: linear-gradient(90deg, #e0e0e0 25%, #f0f0f0 50%, #e0e0e0 75%); - background-size: 200% 100%; - animation: skeleton 1.5s infinite; } .nav { diff --git a/frontend-plataforma-tutoriais/src/components/sidebar/index.tsx b/frontend-plataforma-tutoriais/src/components/sidebar/index.tsx index c38a3d7..a4d9e0c 100644 --- a/frontend-plataforma-tutoriais/src/components/sidebar/index.tsx +++ b/frontend-plataforma-tutoriais/src/components/sidebar/index.tsx @@ -8,13 +8,52 @@ import { LuMail } from "react-icons/lu"; import { LuLayoutDashboard } from "react-icons/lu"; import { LuLogOut } from "react-icons/lu"; import { useGetCurrentUser } from "../../hooks/useGetCurrentUser"; +import { motion, AnimatePresence } from "framer-motion"; + +// ✅ Fora do Sidebar — evita recriar a cada render +const MenuLabel = ({ show, children }: { show: boolean; children: React.ReactNode }) => ( + + {show && ( + + {children} + + )} + +); export default function Sidebar() { - const { getCurrentUser } = useGetCurrentUser(); const [role, setRole] = useState(0); const [sideMenu, setSideMenu] = useState(true); + const containerVariants = { + initial: {}, + animate: { + transition: { + staggerChildren: 0.1, + staggerDirection: 1 + } + }, + exit: { + transition: { + staggerChildren: 0.1, + staggerDirection: -1 + } + } + }; + + const itemVariants = { + initial: { opacity: 0, x: -10 }, + animate: { opacity: 1, x: 0 }, + exit: { opacity: 0, x: -10 } + }; + useEffect(() => { const fetchCurrentUser = async () => { const currentUser = await getCurrentUser(); @@ -23,74 +62,107 @@ export default function Sidebar() { fetchCurrentUser(); }, []); - return sideMenu ? ( - - ) : ( -
- -
- + ); } \ No newline at end of file diff --git a/frontend-plataforma-tutoriais/src/components/sidebar/styles.module.css b/frontend-plataforma-tutoriais/src/components/sidebar/styles.module.css index 15dc290..a3b0718 100644 --- a/frontend-plataforma-tutoriais/src/components/sidebar/styles.module.css +++ b/frontend-plataforma-tutoriais/src/components/sidebar/styles.module.css @@ -1,5 +1,4 @@ .sidebar { - width: 260px; height: 100vh; background: #ffffff; border-right: 1px solid #e9ecef; @@ -65,6 +64,9 @@ color: #495057; border-radius: 8px; padding: 10px 12px; + overflow: hidden; + display: flex; + align-items: center; transition: background-color 0.2s ease, color 0.2s ease; &:hover{ background-color: var(--bg-grey); diff --git a/frontend-plataforma-tutoriais/src/pages/private/dashboard/index.tsx b/frontend-plataforma-tutoriais/src/pages/private/dashboard/index.tsx index 794ac0f..d40b8ab 100644 --- a/frontend-plataforma-tutoriais/src/pages/private/dashboard/index.tsx +++ b/frontend-plataforma-tutoriais/src/pages/private/dashboard/index.tsx @@ -5,7 +5,8 @@ import styles from "./styles.module.css"; import { LuArrowUpRight, LuCalendar, LuClock3, LuPencil } from "react-icons/lu"; import { Link } from "react-router"; import { CgSpinner } from "react-icons/cg"; -import { ProgressBar } from "react-bootstrap"; +import AnimatedProgressBar from "../../../components/animatedProgressBar"; +import AnimatedCounter from "../../../components/animatedNumberCount"; /* import { useGetCurrentUser } from "../../../hooks/useGetCurrentUser" import { useGetVideosLength } from "../../../hooks/useGetVideosLength"; @@ -197,7 +198,7 @@ export default function Home() { {role !== 1 && ( <> - + {progressoVideos === 100 && (
@@ -348,12 +349,12 @@ export default function Home() {
{role === 1 ? "Vídeos ativos" : "Vídeos assistidos"} - {role === 1 ? videosCount : `${videosWatched}/${videosCount}`} + {role === 1 ? : }/
{role === 1 ? "Workshops agendados" : "Workshops inscrito"} - {role === 1 ? workshopsCount : `${workshopsInscribed}/${workshopsCount}`} + {role === 1 ? : }/{}
diff --git a/frontend-plataforma-tutoriais/src/pages/private/profile/index.tsx b/frontend-plataforma-tutoriais/src/pages/private/profile/index.tsx index 8002bc2..5d1b08a 100644 --- a/frontend-plataforma-tutoriais/src/pages/private/profile/index.tsx +++ b/frontend-plataforma-tutoriais/src/pages/private/profile/index.tsx @@ -5,7 +5,7 @@ import styles from "./styles.module.css"; import { LuCheck, LuKeyRound, LuPencil, LuX, LuArrowUpRight, LuClock3, LuCalendar } from "react-icons/lu"; import { CgSpinner } from "react-icons/cg"; import { Link } from "react-router"; -import { ProgressBar } from "react-bootstrap"; +import AnimatedProgressBar from "../../../components/animatedProgressBar"; import { imageSkeletonFadeStyle, onImageSkeletonLoad } from "../../../utils/imageSkeleton"; import { PiCheckCircleFill } from "react-icons/pi"; import Swal from "sweetalert2"; @@ -311,7 +311,7 @@ export default function Profile() { {role !== 1 && ( <> - + {progressoVideos === 100 && (
diff --git a/frontend-plataforma-tutoriais/src/pages/private/video/[id].tsx b/frontend-plataforma-tutoriais/src/pages/private/video/[id].tsx index 23e7387..d60465e 100644 --- a/frontend-plataforma-tutoriais/src/pages/private/video/[id].tsx +++ b/frontend-plataforma-tutoriais/src/pages/private/video/[id].tsx @@ -3,7 +3,7 @@ import { useEffect, useState } from "react"; import { Link, useParams } from "react-router"; import type { ApiErrorResponse, Video } from "../../../types"; import styles from "./styles.module.css"; -import { LuArrowLeft } from "react-icons/lu"; +import { LuArrowLeft, LuChevronLeft, LuChevronRight } from "react-icons/lu"; import { Plyr } from "plyr-react"; import "plyr-react/plyr.css"; import { useVideoWatch } from "../../../hooks/useVideoWatch"; @@ -16,7 +16,8 @@ export default function Video() { const [loading, setLoading] = useState(true); const [playerReady, setPlayerReady] = useState(false); const { watched, markAsWatched } = useVideoWatch(Number(id), video?.watched ?? false); - + const [nextVideo, setNextVideo] = useState(null); + const [previousVideo, setPreviousVideo] = useState(null); useEffect(() => { getVideo(); }, [id]); @@ -79,6 +80,9 @@ export default function Video() { async function getVideo() { setLoading(true); setPlayerReady(false); + setNextVideo(null); + setPreviousVideo(null); + setVideo(null); try { const response = await fetch(`${API_URL}/api/video/${id}`, { method: "GET", @@ -93,6 +97,8 @@ export default function Video() { if (response.ok) { setVideo(data.data); + setNextVideo(data.nextVideo as number); + setPreviousVideo(data.previousVideo as number); setError(null); } else { setVideo(null); @@ -112,7 +118,7 @@ export default function Video() { const pageSkeleton = (
- diff --git a/frontend-plataforma-tutoriais/src/pages/private/video/styles.module.css b/frontend-plataforma-tutoriais/src/pages/private/video/styles.module.css index 9a98453..1f2024b 100644 --- a/frontend-plataforma-tutoriais/src/pages/private/video/styles.module.css +++ b/frontend-plataforma-tutoriais/src/pages/private/video/styles.module.css @@ -12,7 +12,7 @@ .LinkIcon, .linkText { color: var(--text-black); - font-size: var(--size-font-text); + font-size: var(--size-font-text); } .button:hover .LinkIcon, @@ -20,6 +20,23 @@ color: var(--primary-color); } +.previousButton, +.nextButton { + display: block; + width: fit-content; + border-radius: var(--border-radius); + transition: all 0.3s ease; + font-weight: 600; + text-decoration: none; + padding: 10px 20px; + background-color: var(--bg-white); + color: var(--text-primary-color); + transition: all .3s ease; + &:hover { + background-color: var(--bg-primary-color-opacity); + } +} + .animateSpin { animation: spin 1s linear infinite; } diff --git a/frontend-plataforma-tutoriais/src/types.tsx b/frontend-plataforma-tutoriais/src/types.tsx index dfe80a9..36941ae 100644 --- a/frontend-plataforma-tutoriais/src/types.tsx +++ b/frontend-plataforma-tutoriais/src/types.tsx @@ -77,6 +77,8 @@ export type Video = { watched: boolean; users: User[]; role: number; + nextVideo: number | null; + previousVideo: number | null; } export type NextVideosResponse = {