feat: videos navugation by order
This commit is contained in:
@@ -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 <span>{display}</span>;
|
||||
};
|
||||
|
||||
export default AnimatedCounter;
|
||||
@@ -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 (
|
||||
<ProgressBar
|
||||
className={className}
|
||||
now={display}
|
||||
label={`${display}%`}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default AnimatedProgressBar;
|
||||
@@ -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 (
|
||||
<div
|
||||
id="n8n-chat"
|
||||
style={{ color: 'var(--text-primary-color)', textAlign: 'start'}}
|
||||
/>
|
||||
<div style={{ position: 'relative' }}>
|
||||
|
||||
{/* 2. ESTA AGORA É A VERDADEIRA E ÚNICA FORMA DO BOTÃO */}
|
||||
<motion.div
|
||||
style={{
|
||||
position: 'fixed',
|
||||
bottom: '16px', // Ajustado ligeiramente para centrar com o ícone
|
||||
right: '16px',
|
||||
width: '76px', // Tamanho ideal para cobrir a área do chat
|
||||
height: '76px',
|
||||
// GRADIENTE RADIAL: O centro é a tua cor escura, que se funde suavemente com a cor primária e desaparece
|
||||
background: 'radial-gradient(circle, #410002 0%, #B20112 45%, #C4534A 70%, rgba(196, 83, 74, 0) 100%)',
|
||||
filter: 'blur(4px)', // Blur reduzido para não desmanchar o centro do botão, mantendo a borda super suave
|
||||
zIndex: 9998,
|
||||
pointerEvents: 'none',
|
||||
}}
|
||||
animate={{
|
||||
// Rotação abstrata
|
||||
rotate: [0, 360],
|
||||
// A distorção líquida das bordas para dar o efeito AI
|
||||
borderRadius: [
|
||||
"44% 56% 62% 38% / 48% 42% 58% 52%",
|
||||
"62% 38% 52% 48% / 55% 45% 55% 45%",
|
||||
"44% 56% 62% 38% / 48% 42% 58% 52%"
|
||||
],
|
||||
// Pulsação de tamanho muito leve e fluida
|
||||
scale: [0.96, 1.04, 0.96]
|
||||
}}
|
||||
transition={{
|
||||
repeat: Infinity,
|
||||
duration: 5,
|
||||
ease: "easeInOut", // Mudei para easeInOut para tornar o movimento mais orgânico que o linear
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* CONTAINER DO CHAT DA N8N */}
|
||||
<div
|
||||
id="n8n-chat"
|
||||
style={{
|
||||
color: 'var(--text-primary-color)',
|
||||
textAlign: 'start',
|
||||
position: 'relative',
|
||||
zIndex: 9999
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 && (
|
||||
<div className="d-flex d-sm-none justify-content-center">
|
||||
<span className="fw-semibold">Vídeos assistidos</span>
|
||||
<span className={`${styles.badge} badge align-content-center ms-3`}>{videosWatched}/{videosCount}</span>
|
||||
<span className={`${styles.badge} badge align-content-center ms-3`}><AnimatedCounter value={videosWatched} />/<AnimatedCounter value={videosCount} /></span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -183,7 +183,7 @@ export default function Header() {
|
||||
<>
|
||||
<div className="d-none d-sm-flex align-items-center gap-1 text-muted">
|
||||
<span className="fw-semibold">Vídeos assistidos</span>
|
||||
<span className={`${styles.badge} badge align-content-center`}>{videosWatched}/{videosCount}</span>
|
||||
<span className={`${styles.badge} badge align-content-center`}><AnimatedCounter value={videosWatched} />/<AnimatedCounter value={videosCount} /></span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
@@ -225,7 +225,7 @@ export default function Header() {
|
||||
|
||||
<div className="row">
|
||||
{videosSearched.length > 0 && (
|
||||
<span className="text-muted text-start d-block">Vídeos encontrados: {videosSearched.length}</span>
|
||||
<span className="text-muted text-start d-block">Vídeos encontrados: <AnimatedCounter value={videosSearched.length} /></span>
|
||||
)}
|
||||
|
||||
{videosSearched.map((video) => (
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 }) => (
|
||||
<AnimatePresence>
|
||||
{show && (
|
||||
<motion.span
|
||||
initial={{ opacity: 0, width: 0 }}
|
||||
animate={{ opacity: 1, width: "auto" }}
|
||||
exit={{ opacity: 0, width: 0 }}
|
||||
transition={{ duration: 0.25, ease: "easeInOut" }}
|
||||
style={{ overflow: "hidden", whiteSpace: "nowrap", display: "inline-block" }}
|
||||
>
|
||||
{children}
|
||||
</motion.span>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
);
|
||||
|
||||
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 ? (
|
||||
<aside className={styles.sidebar}>
|
||||
<div className="d-flex">
|
||||
<Link className={`${styles.logoLink} justify-content-center mx-auto`} to="/dashboard">
|
||||
<img className={styles.logo} src="/src/assets/logo.png" alt="Logo" />
|
||||
</Link>
|
||||
return (
|
||||
// ✅ motion.aside anima a largura suavemente
|
||||
<motion.aside
|
||||
className={styles.sidebar}
|
||||
animate={{ width: sideMenu ? 260 : 100 }}
|
||||
transition={{ duration: 0.3, ease: "easeInOut" }}
|
||||
>
|
||||
<div className="d-flex justify-content-center" style={{ minHeight: '100px' }}>
|
||||
<AnimatePresence mode="wait">
|
||||
{sideMenu ? (
|
||||
<motion.div
|
||||
key="logo"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
<Link className={`${styles.logoLink} justify-content-center`} to="/dashboard">
|
||||
<img className={`${styles.logo}`} src="/src/assets/logo.png" alt="Logo" />
|
||||
</Link>
|
||||
</motion.div>
|
||||
) : (
|
||||
<motion.div
|
||||
key="open-btn"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="d-flex px-2 mb-4 mt-2"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className={`${styles.sidebarOpen} align-self-start bg-transparent border-0 justify-content-center`}
|
||||
onClick={() => setSideMenu(true)}
|
||||
>
|
||||
<LuPanelLeftOpen size={30} />
|
||||
</button>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
<button type="button" className={`${styles.sidebarClose} align-items-center justify-content-center`} onClick={() => setSideMenu(false)} style={{ position: 'absolute', left: '280px', top: '8px' }}> <LuPanelLeftClose size={30} /> </button>
|
||||
{sideMenu ? (
|
||||
<AnimatePresence>
|
||||
<motion.button
|
||||
key="close-btn"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.2, delay: 0.3 }}
|
||||
className={`${styles.sidebarClose} align-items-center justify-content-center`} onClick={() => setSideMenu(prev => !prev)} style={{ position: 'absolute', left: '280px', top: '8px' }}> {sideMenu ? <LuPanelLeftClose size={30} /> : <LuPanelLeftOpen size={30} />} </motion.button>
|
||||
</AnimatePresence>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<nav className={`${styles.nav}`}>
|
||||
<ul className={styles.navList}>
|
||||
<li className={`${styles.navItem} text-start`}>
|
||||
<NavLink className={({ isActive }) => isActive ? `${styles.navLink} ${styles.navLinkActive}` : styles.navLink} to="/dashboard"> <LuLayoutDashboard className="me-2" size={24} /> {sideMenu ? "Dashboard" : ""} </NavLink>
|
||||
</li>
|
||||
<li className={`${styles.navItem} text-start`}>
|
||||
<NavLink className={({ isActive }) => isActive ? `${styles.navLink} ${styles.navLinkActive}` : styles.navLink} to="/videos"> <LuTvMinimalPlay className="me-2" size={24} />Videos</NavLink>
|
||||
</li>
|
||||
<li className={`${styles.navItem} text-start`}>
|
||||
<NavLink className={({ isActive }) => isActive ? `${styles.navLink} ${styles.navLinkActive}` : styles.navLink} to="/workshops"> <LuGraduationCap className="me-2" size={24} /> {sideMenu ? "Workshops" : ""} </NavLink>
|
||||
</li>
|
||||
{role === 1 ? (
|
||||
<li className={`${styles.navItem} text-start`}>
|
||||
<NavLink className={({ isActive }) => isActive ? `${styles.navLink} ${styles.navLinkActive}` : styles.navLink} to="/admin/users"> <LuUsers className="me-2" size={24} /> {sideMenu ? "Utilizadores" : ""} </NavLink>
|
||||
</li>
|
||||
) : null}
|
||||
<li className={`${styles.navItem} text-start`}>
|
||||
<NavLink className={({ isActive }) => isActive ? `${styles.navLink} ${styles.navLinkActive}` : styles.navLink} to="/contactos"> <LuMail className="me-2" size={24} /> {sideMenu ? "Contactos" : ""} </NavLink>
|
||||
</li>
|
||||
</ul>
|
||||
<nav className={styles.nav}>
|
||||
<motion.ul className={styles.navList}
|
||||
variants={containerVariants}
|
||||
initial="initial"
|
||||
animate="animate"
|
||||
exit="exit">
|
||||
<motion.li variants={itemVariants} className={`${styles.navItem} px-2 ${sideMenu ? "text-start" : "text-center"}`}>
|
||||
<NavLink className={({ isActive }) => isActive ? `${styles.navLink} ${styles.navLinkActive}` : styles.navLink} to="/dashboard">
|
||||
<LuLayoutDashboard className={sideMenu ? "me-2" : "w-100"} size={24} />
|
||||
<MenuLabel show={sideMenu}>Dashboard</MenuLabel>
|
||||
</NavLink>
|
||||
</motion.li>
|
||||
<motion.li variants={itemVariants} className={`${styles.navItem} px-2 ${sideMenu ? "text-start" : "text-center"}`}>
|
||||
<NavLink className={({ isActive }) => isActive ? `${styles.navLink} ${styles.navLinkActive}` : styles.navLink} to="/videos">
|
||||
<LuTvMinimalPlay className={sideMenu ? "me-2" : "w-100"} size={24} />
|
||||
<MenuLabel show={sideMenu}>Videos</MenuLabel>
|
||||
</NavLink>
|
||||
</motion.li>
|
||||
<motion.li variants={itemVariants} className={`${styles.navItem} px-2 ${sideMenu ? "text-start" : "text-center"}`}>
|
||||
<NavLink className={({ isActive }) => isActive ? `${styles.navLink} ${styles.navLinkActive}` : styles.navLink} to="/workshops">
|
||||
<LuGraduationCap className={sideMenu ? "me-2" : "w-100"} size={24} />
|
||||
<MenuLabel show={sideMenu}>Workshops</MenuLabel>
|
||||
</NavLink>
|
||||
</motion.li>
|
||||
{role === 1 && (
|
||||
<motion.li variants={itemVariants} className={`${styles.navItem} px-2 ${sideMenu ? "text-start" : "text-center"}`}>
|
||||
<NavLink className={({ isActive }) => isActive ? `${styles.navLink} ${styles.navLinkActive}` : styles.navLink} to="/admin/users">
|
||||
<LuUsers className={sideMenu ? "me-2" : "w-100"} size={24} />
|
||||
<MenuLabel show={sideMenu}>Utilizadores</MenuLabel>
|
||||
</NavLink>
|
||||
</motion.li>
|
||||
)}
|
||||
<motion.li variants={itemVariants} className={`${styles.navItem} px-2 ${sideMenu ? "text-start" : "text-center"}`}>
|
||||
<NavLink className={({ isActive }) => isActive ? `${styles.navLink} ${styles.navLinkActive}` : styles.navLink} to="/contactos">
|
||||
<LuMail className={sideMenu ? "me-2" : "w-100"} size={24} />
|
||||
<MenuLabel show={sideMenu}>Contactos</MenuLabel>
|
||||
</NavLink>
|
||||
</motion.li>
|
||||
</motion.ul>
|
||||
</nav>
|
||||
|
||||
<div className={styles.logoutWrapper}>
|
||||
<Link className={styles.logoutButton} to="/logout"> <LuLogOut className="me-2" size={24} />Logout</Link>
|
||||
<div className={`${styles.logoutWrapper} px-2`}>
|
||||
<Link className={styles.logoutButton} to="/logout">
|
||||
<LuLogOut className={sideMenu ? "me-2" : "w-100"} size={24} />
|
||||
<MenuLabel show={sideMenu}>Logout</MenuLabel>
|
||||
</Link>
|
||||
</div>
|
||||
</aside>
|
||||
) : (
|
||||
<div className={`${styles.sidebarIconsMenu}`}>
|
||||
<aside >
|
||||
<div className="d-flex px-2 mb-4 mt-2">
|
||||
<button type="button" className={`${styles.sidebarOpen} align-items-center bg-transparent border-0 justify-content-center`} onClick={() => setSideMenu(true)}> <LuPanelLeftOpen size={30} /> </button>
|
||||
</div>
|
||||
|
||||
<nav className={`${styles.nav} px-2`}>
|
||||
<ul className={styles.navList}>
|
||||
<li className={`${styles.navItem}`} title="Dashboard">
|
||||
<NavLink className={({ isActive }) => isActive ? `${styles.navLink} ${styles.navLinkActive}` : styles.navLink} to="/dashboard"> <LuLayoutDashboard size={24} /></NavLink>
|
||||
</li>
|
||||
<li className={`${styles.navItem}`} title="Vídeos">
|
||||
<NavLink className={({ isActive }) => isActive ? `${styles.navLink} ${styles.navLinkActive}` : styles.navLink} to="/videos"> <LuTvMinimalPlay size={24} /></NavLink>
|
||||
</li>
|
||||
<li className={`${styles.navItem}`} title="Workshops">
|
||||
<NavLink className={({ isActive }) => isActive ? `${styles.navLink} justify-content-center ${styles.navLinkActive}` : styles.navLink} to="/workshops"> <LuGraduationCap size={24} /></NavLink>
|
||||
</li>
|
||||
<li className={`${styles.navItem}`} title="Utilizadores">
|
||||
<NavLink className={({ isActive }) => isActive ? `${styles.navLink} ${styles.navLinkActive} justify-content-center` : styles.navLink} to="/admin/users"> <LuUsers size={24} /></NavLink>
|
||||
</li>
|
||||
<li className={`${styles.navItem}`} title="Contactos">
|
||||
<NavLink className={({ isActive }) => isActive ? `${styles.navLink} ${styles.navLinkActive}` : styles.navLink} to="/contactos"> <LuMail size={24} /></NavLink>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<div className="d-flex align-self-end mx-2 position-absolute bottom-0 mb-2">
|
||||
<Link className={styles.logoutButton} to="/logout" title="Logout"> <LuLogOut className="" size={24} /></Link>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
</motion.aside>
|
||||
);
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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 && (
|
||||
<>
|
||||
<ProgressBar className={`${styles.progressBar}`} now={progressoVideos} label={`${progressoVideos}%`} />
|
||||
<AnimatedProgressBar value={progressoVideos} className={`${styles.progressBar}`} />
|
||||
|
||||
{progressoVideos === 100 && (
|
||||
<div className="text-center mt-3">
|
||||
@@ -348,12 +349,12 @@ export default function Home() {
|
||||
<div className={`${styles.userVideosWatched} text-start justify-content-evenly d-flex flex-column h-100 p-4 mx-sm-2 ms-lg-2 me-lg-0`}>
|
||||
<div className="d-flex flex-column">
|
||||
<span className="fw-normal fs-3 text-white" > {role === 1 ? "Vídeos ativos" : "Vídeos assistidos"}</span>
|
||||
<span className=" fw-bold text-white" style={{ fontSize: "4rem" }}>{role === 1 ? videosCount : `${videosWatched}/${videosCount}`}</span>
|
||||
<span className=" fw-bold text-white" style={{ fontSize: "4rem" }}>{role === 1 ? <AnimatedCounter value={videosCount} /> : <AnimatedCounter value={videosWatched} />}/<AnimatedCounter value={videosCount} /></span>
|
||||
</div>
|
||||
|
||||
<div className="d-flex flex-column">
|
||||
<span className="fw-normal fs-3" style={{ color: "var(--bg-grey)" }}>{role === 1 ? "Workshops agendados" : "Workshops inscrito"}</span>
|
||||
<span className="fw-bold text-white" style={{ fontSize: "4rem" }}>{role === 1 ? workshopsCount : `${workshopsInscribed}/${workshopsCount}`}</span>
|
||||
<span className="fw-bold text-white" style={{ fontSize: "4rem" }}>{role === 1 ? <AnimatedCounter value={workshopsCount} /> : <AnimatedCounter value={workshopsInscribed} />}/{<AnimatedCounter value={workshopsCount} />}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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 && (
|
||||
<>
|
||||
<ProgressBar className={`${styles.progressBar}`} now={progressoVideos} label={`${progressoVideos}%`} />
|
||||
<AnimatedProgressBar value={progressoVideos} className={`${styles.progressBar}`} />
|
||||
|
||||
{progressoVideos === 100 && (
|
||||
<div className="text-center mt-3">
|
||||
|
||||
@@ -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<number | null>(null);
|
||||
const [previousVideo, setPreviousVideo] = useState<number | null>(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 = (
|
||||
<div className="my-3 d-flex flex-column gap-2">
|
||||
<div className={`${styles.titleSkeleton} mb-3`} aria-hidden="true" style={{ maxWidth: "1300px", margin: "0 auto" }} />
|
||||
<div className={`${styles.titleSkeleton} mb-3`} aria-hidden="true" style={{ maxWidth: "1300px", margin: "0 auto" }} />
|
||||
<div style={{ maxWidth: "1300px", margin: "0 auto", width: "100%" }}>
|
||||
<div className={styles.playerWrapper}>
|
||||
{playerSkeleton}
|
||||
@@ -157,6 +163,15 @@ export default function Video() {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="d-flex justify-content-between mt-3">
|
||||
{previousVideo && (
|
||||
<Link to={`/video/${previousVideo}`} className={`${styles.previousButton} fs-6 text-start`}><b><LuChevronLeft size={25} title="Vídeo anterior" /> Anterior</b></Link>
|
||||
)}
|
||||
|
||||
{nextVideo && (
|
||||
<Link to={`/video/${nextVideo}`} className={`${styles.nextButton} fs-6 text-end`}><b>Próximo <LuChevronRight size={25} title="Próximo vídeo" /> </b></Link>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-start mt-3 fs-6">{video.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -77,6 +77,8 @@ export type Video = {
|
||||
watched: boolean;
|
||||
users: User[];
|
||||
role: number;
|
||||
nextVideo: number | null;
|
||||
previousVideo: number | null;
|
||||
}
|
||||
|
||||
export type NextVideosResponse = {
|
||||
|
||||
Reference in New Issue
Block a user