feat: Adding animations

This commit is contained in:
Xavier Oliveira
2026-06-01 16:52:28 +01:00
parent 985f5ffb69
commit bcaff422a0
19 changed files with 528 additions and 233 deletions

View File

@@ -1,14 +1,26 @@
import { useMotionValue, useSpring } from "framer-motion"; import { useInView, useMotionValue, useSpring } from "framer-motion";
import { useEffect, useState } from "react"; import { useEffect, useRef, useState } from "react";
const AnimatedCounter = ({ value }: { value: number }) => { type AnimatedCounterProps = {
value: number;
/** Só anima na primeira vez que entra no ecrã (default: true) */
once?: boolean;
};
const AnimatedCounter = ({ value, once = true }: AnimatedCounterProps) => {
const ref = useRef<HTMLSpanElement>(null);
const isInView = useInView(ref, { once, amount: 0.8 });
const motionValue = useMotionValue(0); const motionValue = useMotionValue(0);
const spring = useSpring(motionValue, { stiffness: 60, damping: 20 }); const spring = useSpring(motionValue, { stiffness: 60, damping: 20 });
const [display, setDisplay] = useState(0); const [display, setDisplay] = useState(0);
useEffect(() => { useEffect(() => {
motionValue.set(value); if (isInView) {
}, [value]); motionValue.set(value);
} else if (!once) {
motionValue.set(0);
}
}, [isInView, value, motionValue, once]);
useEffect(() => { useEffect(() => {
const unsubscribe = spring.on("change", (v) => { const unsubscribe = spring.on("change", (v) => {
@@ -17,7 +29,7 @@ const AnimatedCounter = ({ value }: { value: number }) => {
return unsubscribe; return unsubscribe;
}, [spring]); }, [spring]);
return <span>{display}</span>; return <span ref={ref}>{display}</span>;
}; };
export default AnimatedCounter; export default AnimatedCounter;

View File

@@ -1,18 +1,29 @@
import { useMotionValue, useSpring } from "framer-motion"; import { useInView, useMotionValue, useSpring } from "framer-motion";
import { useEffect, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { ProgressBar } from "react-bootstrap"; import { ProgressBar } from "react-bootstrap";
const AnimatedProgressBar = ({ value, className }: { value: number; className?: string }) => { type AnimatedProgressBarProps = {
value: number;
className?: string;
once?: boolean;
};
const AnimatedProgressBar = ({ value, className, once = true }: AnimatedProgressBarProps) => {
const ref = useRef<HTMLDivElement>(null);
const isInView = useInView(ref, { once, amount: 0.5 });
const motionValue = useMotionValue(0); const motionValue = useMotionValue(0);
const spring = useSpring(motionValue, { stiffness: 60, damping: 20 }); const spring = useSpring(motionValue, { stiffness: 60, damping: 20 });
const [display, setDisplay] = useState(0); const [display, setDisplay] = useState(0);
useEffect(() => { useEffect(() => {
motionValue.set(value); if (isInView) {
}, [value]); motionValue.set(value);
} else if (!once) {
motionValue.set(0);
}
}, [isInView, value, motionValue, once]);
useEffect(() => { useEffect(() => {
// ✅ subscribe mantém o state sincronizado com o spring
const unsubscribe = spring.on("change", (v) => { const unsubscribe = spring.on("change", (v) => {
setDisplay(Math.round(v)); setDisplay(Math.round(v));
}); });
@@ -20,11 +31,13 @@ const AnimatedProgressBar = ({ value, className }: { value: number; className?:
}, [spring]); }, [spring]);
return ( return (
<ProgressBar <div ref={ref}>
className={className} <ProgressBar
now={display} className={className}
label={`${display}%`} now={display}
/> label={`${display}%`}
/>
</div>
); );
}; };

View File

@@ -14,6 +14,7 @@ import { PiCheckCircleFill } from "react-icons/pi";
import { useGetWorkshopsSearch } from "../../hooks/useGetWorkshopsSearch"; import { useGetWorkshopsSearch } from "../../hooks/useGetWorkshopsSearch";
import { imageSkeletonFadeStyle, onImageSkeletonLoad } from "../../utils/imageSkeleton"; import { imageSkeletonFadeStyle, onImageSkeletonLoad } from "../../utils/imageSkeleton";
import { motion, AnimatePresence } from "framer-motion"; import { motion, AnimatePresence } from "framer-motion";
import { dropdownTransition, dropdownVariants, slideFromTopTransition, slideFromTopVariants } from "../../utils/pageMotionVariants";
import { useGetCurrentUser } from "../../hooks/useGetCurrentUser"; import { useGetCurrentUser } from "../../hooks/useGetCurrentUser";
import AnimatedCounter from "../animatedNumberCount"; import AnimatedCounter from "../animatedNumberCount";
@@ -220,7 +221,7 @@ export default function Header() {
<> <>
{videosSearched.length > 0 ? ( {videosSearched.length > 0 ? (
<div className="d-flex flex-column"> <motion.div variants={slideFromTopVariants} initial="initial" animate="animate" transition={slideFromTopTransition} className="d-flex flex-column">
<span className="fs-1 text-center fw-bold mb-1 mt-3">Vídeos</span> <span className="fs-1 text-center fw-bold mb-1 mt-3">Vídeos</span>
<div className="row"> <div className="row">
@@ -249,12 +250,12 @@ export default function Header() {
))} ))}
</div> </div>
</div> </motion.div>
) : null} ) : null}
{workshopsSearched.length > 0 ? ( {workshopsSearched.length > 0 ? (
<div className="d-flex flex-column"> <motion.div variants={slideFromTopVariants} initial="initial" animate="animate" transition={slideFromTopTransition} className="d-flex flex-column">
<span className="fs-1 text-center fw-bold mb-1 mt-3">Workshops</span> <span className="fs-1 text-center fw-bold mb-1 mt-3">Workshops</span>
<div className="row g-3 mb-3"> <div className="row g-3 mb-3">
{workshopsSearched.length > 0 && ( {workshopsSearched.length > 0 && (
@@ -295,7 +296,7 @@ export default function Header() {
))} ))}
</div> </div>
</div> </motion.div>
) : null} ) : null}
</> </>
@@ -319,12 +320,13 @@ export default function Header() {
<AnimatePresence> <AnimatePresence>
{showDropdown && ( {showDropdown && (
<motion.ul <motion.ul
initial={{ opacity: 0, y: -10, scale: 0.98 }} variants={dropdownVariants}
animate={{ opacity: 1, y: 0, scale: 1 }} initial="initial"
exit={{ opacity: 0, y: -10, scale: 0.98 }} animate="animate"
transition={{ duration: 0.2, ease: "easeOut" }} exit="exit"
transition={dropdownTransition}
className="dropdown-menu dropdown-menu-end" className="dropdown-menu dropdown-menu-end"
style={{ display: "block", zIndex: 2000, right: 0, top: "100%", marginTop: "0.25rem" }}> style={{ display: "block", zIndex: 2000, right: 0, top: "100%", marginTop: "0.25rem" }}>
<li><Link className="dropdown-item" to="/profile"><LuCircleUser className="mb-1 me-2" size={24} />A minha conta</Link></li> <li><Link className="dropdown-item" to="/profile"><LuCircleUser className="mb-1 me-2" size={24} />A minha conta</Link></li>
<li><Link className="dropdown-item mt-2" to="/logout" style={{ color: "var(--text-primary-color)" }}> <LuLogOut className="mb-1 me-2" size={24} />Sair</Link></li> <li><Link className="dropdown-item mt-2" to="/logout" style={{ color: "var(--text-primary-color)" }}> <LuLogOut className="mb-1 me-2" size={24} />Sair</Link></li>
</motion.ul> </motion.ul>

View File

@@ -6,6 +6,8 @@ import { LuArrowLeft } from "react-icons/lu";
import { LuPlus } from "react-icons/lu"; import { LuPlus } from "react-icons/lu";
import { CgSpinner } from "react-icons/cg"; import { CgSpinner } from "react-icons/cg";
import Swal from "sweetalert2"; import Swal from "sweetalert2";
import { motion } from "framer-motion";
import { slideFromLeftVariants, slideFromLeftOnMountTransition, pageTitleTransition, pageTitleVariants, slideFromBottomVariants, slideFromBottomTransition } from "../../../../utils/pageMotionVariants";
export default function CreateUser() { export default function CreateUser() {
const [name, setName] = useState<string>(""); const [name, setName] = useState<string>("");
@@ -15,6 +17,7 @@ export default function CreateUser() {
const [role_id, setRoleId] = useState("2"); const [role_id, setRoleId] = useState("2");
const [creating, setCreating] = useState(false); const [creating, setCreating] = useState(false);
async function create() { async function create() {
//validação dos campos //validação dos campos
@@ -113,13 +116,13 @@ export default function CreateUser() {
return ( return (
<div className={`${styles.container} p-3 p-xl-0`}> <div className={`${styles.container} p-3 p-xl-0`}>
<div className={"text-start"}> <motion.div variants={slideFromLeftVariants} initial="initial" animate="animate" transition={slideFromLeftOnMountTransition} className={"text-start"}>
<button className={`${styles.button} border-0 bg-transparent fs-5`}><Link className={`${styles.link} text-decoration-none`} to="/admin/users"><LuArrowLeft className={`${styles.LinkIcon} mb-1 fs-4 align-items-center`} /> <span className={styles.linkText}>Utilizadores</span> </Link></button> <button className={`${styles.button} border-0 bg-transparent fs-5`}><Link className={`${styles.link} text-decoration-none`} to="/admin/users"><LuArrowLeft className={`${styles.LinkIcon} mb-1 fs-4 align-items-center`} /> <span className={styles.linkText}>Utilizadores</span> </Link></button>
</div> </motion.div>
<h1 className={styles.title}>Criar Utilizador</h1> <motion.h1 variants={pageTitleVariants} initial="initial" animate="animate" transition={pageTitleTransition} className={styles.title}>Criar Utilizador</motion.h1>
<div> <div>
<div> <motion.div variants={slideFromBottomVariants} initial="initial" animate="animate" transition={slideFromBottomTransition} >
<div className="row g-3"> <div className="row g-3">
<div className="col-12 col-md-6 text-start"> <div className="col-12 col-md-6 text-start">
<label className="form-label fw-bold" htmlFor="name">Nome</label> <label className="form-label fw-bold" htmlFor="name">Nome</label>
@@ -149,7 +152,7 @@ export default function CreateUser() {
<div className="buttonsContainer mt-4 d-flex gap-2 justify-content-center" > <div className="buttonsContainer mt-4 d-flex gap-2 justify-content-center" >
<button type="submit" className={`${styles.createButton}`} onClick={() => { create(); setCreating(true); }} disabled={creating}>{creating ? <CgSpinner className={`${styles.animateSpin} text-2xl fs-3`} /> : <LuPlus className="mb-1 text-white" />} Adicionar</button> <button type="submit" className={`${styles.createButton}`} onClick={() => { create(); setCreating(true); }} disabled={creating}>{creating ? <CgSpinner className={`${styles.animateSpin} text-2xl fs-3`} /> : <LuPlus className="mb-1 text-white" />} Adicionar</button>
</div> </div>
</div> </motion.div>
</div> </div>
</div> </div>
) )

View File

@@ -7,6 +7,15 @@ import { LuArrowLeft, LuPlus } from "react-icons/lu";
import { LuUpload } from "react-icons/lu"; import { LuUpload } from "react-icons/lu";
import Swal from "sweetalert2"; import Swal from "sweetalert2";
import { CgSpinner } from "react-icons/cg"; import { CgSpinner } from "react-icons/cg";
import { motion } from "framer-motion";
import {
pageTitleTransition,
pageTitleVariants,
slideFromBottomTransition,
slideFromBottomVariants,
slideFromLeftOnMountTransition,
slideFromLeftVariants,
} from "../../../../utils/pageMotionVariants";
export default function CreateVideo() { export default function CreateVideo() {
const [title, setTitle] = useState<string>(""); const [title, setTitle] = useState<string>("");
@@ -208,10 +217,12 @@ export default function CreateVideo() {
return ( return (
<div className={`${styles.container} p-3 p-xl-0 px-2 px-md-5`}> <div className={`${styles.container} p-3 p-xl-0 px-2 px-md-5`}>
<div className={"text-start"}> <motion.div variants={slideFromLeftVariants} initial="initial" animate="animate" transition={slideFromLeftOnMountTransition} className="text-start">
<button className={`${styles.button} border-0 bg-transparent fs-5`}><Link className={`${styles.link} text-decoration-none`} to="/videos"><LuArrowLeft className={`${styles.LinkIcon} mb-1 fs-4 align-items-center`} /> <span className={styles.linkText}>Vídeos</span> </Link></button> <button className={`${styles.button} border-0 bg-transparent fs-5`}><Link className={`${styles.link} text-decoration-none`} to="/videos"><LuArrowLeft className={`${styles.LinkIcon} mb-1 fs-4 align-items-center`} /> <span className={styles.linkText}>Vídeos</span> </Link></button>
</div> </motion.div>
<h1 className={styles.title}>Adicionar Vídeo</h1> <motion.h1 variants={pageTitleVariants} initial="initial" animate="animate" transition={pageTitleTransition} className={styles.title}>Adicionar Vídeo</motion.h1>
<div>
<motion.div variants={slideFromBottomVariants} initial="initial" animate="animate" transition={slideFromBottomTransition}>
<div className="row mx-auto g-4"> <div className="row mx-auto g-4">
<div className="col-12 px-1 text-start"> <div className="col-12 px-1 text-start">
<label className="form-label fw-bold" htmlFor="title">Título</label> <label className="form-label fw-bold" htmlFor="title">Título</label>
@@ -302,6 +313,8 @@ export default function CreateVideo() {
)} )}
<button type="button" className={`${styles.btnAdicionarVideo} btn btn-primary mt-5`} onClick={() => { createVideo() }} disabled={creating}>{creating ? <CgSpinner className={`${styles.animateSpin} text-2xl fs-3`} /> : <LuPlus />} Adicionar vídeo</button> <button type="button" className={`${styles.btnAdicionarVideo} btn btn-primary mt-5`} onClick={() => { createVideo() }} disabled={creating}>{creating ? <CgSpinner className={`${styles.animateSpin} text-2xl fs-3`} /> : <LuPlus />} Adicionar vídeo</button>
</motion.div>
</div>
</div> </div>
) )
} }

View File

@@ -8,6 +8,15 @@ import "react-datepicker/dist/react-datepicker.css";
import { pt } from "date-fns/locale"; import { pt } from "date-fns/locale";
import { CgSpinner } from "react-icons/cg"; import { CgSpinner } from "react-icons/cg";
import Swal from "sweetalert2"; import Swal from "sweetalert2";
import { motion } from "framer-motion";
import {
pageTitleTransition,
pageTitleVariants,
slideFromBottomTransition,
slideFromBottomVariants,
slideFromLeftOnMountTransition,
slideFromLeftVariants,
} from "../../../../utils/pageMotionVariants";
export default function CreateWorkshop() { export default function CreateWorkshop() {
const [title, setTitle] = useState<string>(""); const [title, setTitle] = useState<string>("");
@@ -92,10 +101,12 @@ export default function CreateWorkshop() {
return ( return (
<div className={`${styles.container} p-3 p-xl-0 px-2 px-md-5`}> <div className={`${styles.container} p-3 p-xl-0 px-2 px-md-5`}>
<div className={"text-start"}> <motion.div variants={slideFromLeftVariants} initial="initial" animate="animate" transition={slideFromLeftOnMountTransition} className="text-start">
<button className={`${styles.button} border-0 bg-transparent fs-5`}><Link className={`${styles.link} text-decoration-none`} to="/workshops"><LuArrowLeft className={`${styles.LinkIcon} mb-1 fs-4 align-items-center`} /> <span className={styles.linkText}>Workshops</span> </Link></button> <button className={`${styles.button} border-0 bg-transparent fs-5`}><Link className={`${styles.link} text-decoration-none`} to="/workshops"><LuArrowLeft className={`${styles.LinkIcon} mb-1 fs-4 align-items-center`} /> <span className={styles.linkText}>Workshops</span> </Link></button>
</div> </motion.div>
<h1 className={styles.title}>Adicionar Workshop</h1> <motion.h1 variants={pageTitleVariants} initial="initial" animate="animate" transition={pageTitleTransition} className={styles.title}>Adicionar Workshop</motion.h1>
<div>
<motion.div variants={slideFromBottomVariants} initial="initial" animate="animate" transition={slideFromBottomTransition}>
<div className="row mx-auto g-4"> <div className="row mx-auto g-4">
<div className="col-12 px-1 text-start"> <div className="col-12 px-1 text-start">
<label className={`${styles.label} form-label fw-bold`} htmlFor="title">Título</label> <label className={`${styles.label} form-label fw-bold`} htmlFor="title">Título</label>
@@ -171,6 +182,8 @@ export default function CreateWorkshop() {
<div className="buttonsContainer mt-4 d-flex gap-2 justify-content-center" > <div className="buttonsContainer mt-4 d-flex gap-2 justify-content-center" >
<button type="submit" onClick={createWorkshop} className={`${styles.btnAdicionarWorkshop}`} disabled={isLoading}><LuPlus className="mb-1 text-white" /> {creating ? <CgSpinner className={`${styles.animateSpin} text-2xl fs-3`} /> : "Adicionar Workshop"}</button> <button type="submit" onClick={createWorkshop} className={`${styles.btnAdicionarWorkshop}`} disabled={isLoading}><LuPlus className="mb-1 text-white" /> {creating ? <CgSpinner className={`${styles.animateSpin} text-2xl fs-3`} /> : "Adicionar Workshop"}</button>
</div> </div>
</motion.div>
</div>
</div> </div>
) )
} }

View File

@@ -6,6 +6,8 @@ import styles from "./styles.module.css";
import { CgSpinner } from "react-icons/cg"; import { CgSpinner } from "react-icons/cg";
import { LuUpload, LuPlus, LuCheck, LuTrash2, LuArrowLeft } from "react-icons/lu"; import { LuUpload, LuPlus, LuCheck, LuTrash2, LuArrowLeft } from "react-icons/lu";
import Swal from "sweetalert2"; import Swal from "sweetalert2";
import { slideFromLeftVariants, slideFromLeftOnMountTransition, pageTitleTransition, pageTitleVariants, slideFromBottomVariants, slideFromBottomTransition } from "../../../../utils/pageMotionVariants";
import { motion } from "framer-motion";
export default function editVideo() { export default function editVideo() {
const { id } = useParams(); const { id } = useParams();
@@ -247,17 +249,15 @@ export default function editVideo() {
</div> </div>
) : ( ) : (
<div className={`${styles.container} p-3 p-xl-0`}> <div className={`${styles.container} p-3 p-xl-0`}>
<div className={"text-start"}> <motion.div variants={slideFromLeftVariants} initial="initial" animate="animate" transition={slideFromLeftOnMountTransition} className={"text-start"}>
<button className={`${styles.button} border-0 bg-transparent fs-5`}><Link className={`${styles.link} text-decoration-none`} to="/videos"><LuArrowLeft className={`${styles.LinkIcon} mb-1 fs-4 align-items-center`} /> <span className={styles.linkText}>Vídeos</span> </Link></button> <button className={`${styles.button} border-0 bg-transparent fs-5`}><Link className={`${styles.link} text-decoration-none`} to="/videos"><LuArrowLeft className={`${styles.LinkIcon} mb-1 fs-4 align-items-center`} /> <span className={styles.linkText}>Vídeos</span> </Link></button>
</div> </motion.div>
{video ? ( {video ? (
<div className="my-3"> <div className="my-3">
<div className="mt-5"> <div className="mt-5">
<span className={styles.title}>Alterar dados do vídeo</span> <motion.h1 variants={pageTitleVariants} initial="initial" animate="animate" transition={pageTitleTransition} className={styles.title}>Alterar dados do vídeo</motion.h1>
<div className="row mx-auto g-4"> <motion.div variants={slideFromBottomVariants} initial="initial" animate="animate" transition={slideFromBottomTransition} className="row mx-auto g-4">
<form method="patch" onSubmit={update}> <form method="patch" onSubmit={update}>
<input type="hidden" name="video_id" value={video?.id} /> <input type="hidden" name="video_id" value={video?.id} />
<div className="col-12 px-1 text-start mb-3"> <div className="col-12 px-1 text-start mb-3">
@@ -337,7 +337,7 @@ export default function editVideo() {
<button type="button" className={`${styles.deleteButton}`} onClick={() => handleDeleteVideo()}>Apagar <LuTrash2 className="mb-1 text-white" /></button> <button type="button" className={`${styles.deleteButton}`} onClick={() => handleDeleteVideo()}>Apagar <LuTrash2 className="mb-1 text-white" /></button>
</div> </div>
</form> </form>
</div> </motion.div>
</div> </div>

View File

@@ -10,6 +10,8 @@ import "react-datepicker/dist/react-datepicker.css";
import Swal from "sweetalert2"; import Swal from "sweetalert2";
import styles from "./styles.module.css"; import styles from "./styles.module.css";
import { imageSkeletonFadeStyle, onImageSkeletonLoad } from "../../../../utils/imageSkeleton"; import { imageSkeletonFadeStyle, onImageSkeletonLoad } from "../../../../utils/imageSkeleton";
import { motion } from "framer-motion";
import { slideFromLeftVariants, slideFromLeftOnMountTransition, slideFromLeftTransition, slideFromRightVariants, slideFromRightTransition, slideFromTopTransition, slideFromTopVariants } from "../../../../utils/pageMotionVariants";
export default function Workshop() { export default function Workshop() {
const { id } = useParams(); const { id } = useParams();
@@ -192,19 +194,19 @@ export default function Workshop() {
return ( return (
<div className={`${styles.container} p-3 p-xl-0`}> <div className={`${styles.container} p-3 p-xl-0`}>
<div className={"text-start mb-3"}> <motion.div variants={slideFromLeftVariants} initial="initial" animate="animate" transition={slideFromLeftOnMountTransition} className={"text-start mb-3"}>
<button className={`${styles.button} border-0 bg-transparent fs-5`}> <button className={`${styles.button} border-0 bg-transparent fs-5`}>
<Link className={`${styles.link} text-decoration-none`} to="/workshops"> <Link className={`${styles.link} text-decoration-none`} to="/workshops">
<LuArrowLeft className={`${styles.LinkIcon} mb-1 fs-4 align-items-center`} /> <span className={styles.linkText}>Workshops</span> <LuArrowLeft className={`${styles.LinkIcon} mb-1 fs-4 align-items-center`} /> <span className={styles.linkText}>Workshops</span>
</Link> </Link>
</button> </button>
</div> </motion.div>
{workshop ? ( {workshop ? (
<> <>
<div className={`${styles.container} d-flex flex-column gap-2`}> <div className={`${styles.container} d-flex flex-column gap-2`}>
<div className="row mt-4 g-3 gx-md-4 gx-lg-5 ms-0"> <div className="row mt-4 g-3 gx-md-4 gx-lg-5 ms-0">
<div className={`${styles.thumbnailWrapper} col-12 col-lg-3 align-self-center align-self-sm-center px-0 mt-0`}> <motion.div variants={slideFromLeftVariants} initial="initial" animate="animate" transition={slideFromLeftTransition} className={`${styles.thumbnailWrapper} col-12 col-lg-3 align-self-center align-self-sm-center px-0 mt-0`}>
<img <img
src={`${API_URL}/storage/${workshop.image}`} src={`${API_URL}/storage/${workshop.image}`}
alt={workshop.title} alt={workshop.title}
@@ -212,8 +214,8 @@ export default function Workshop() {
style={imageSkeletonFadeStyle} style={imageSkeletonFadeStyle}
onLoad={onImageSkeletonLoad} onLoad={onImageSkeletonLoad}
/> />
</div> </motion.div>
<div className="col-12 col-lg-9 d-flex flex-column justify-content-between gap-2"> <motion.div variants={slideFromRightVariants} initial="initial" animate="animate" transition={slideFromRightTransition} className="col-12 col-lg-9 d-flex flex-column justify-content-between gap-2">
<div className="d-flex flex-column flex-wrap gap-1 justify-content-center justify-content-md-start"> <div className="d-flex flex-column flex-wrap gap-1 justify-content-center justify-content-md-start">
{role === 1 ? ( {role === 1 ? (
<span className={`${styles.statusWorkshop} text-center d-inline-block py-1 mb-1 mb-sm-0 ${workshop.status === "pending" ? "alert alert-primary" : workshop.status === "realized" ? "alert alert-success" : "alert alert-danger"}`} style={{ width: "fit-content" }}> <span className={`${styles.statusWorkshop} text-center d-inline-block py-1 mb-1 mb-sm-0 ${workshop.status === "pending" ? "alert alert-primary" : workshop.status === "realized" ? "alert alert-success" : "alert alert-danger"}`} style={{ width: "fit-content" }}>
@@ -253,12 +255,12 @@ export default function Workshop() {
) : null} ) : null}
</div> </div>
</div> </div>
</div> </motion.div>
</div> </div>
{listagemInscritos ? ( {listagemInscritos ? (
<div className={`${styles.users} mt-4`}> <motion.div variants={slideFromTopVariants} initial="initial" animate="animate" transition={slideFromTopTransition} className={`${styles.users} mt-4`}>
<div className="table-responsive"> <div className="table-responsive">
<button type="button" className={`${styles.btnClose} d-flex float-end p-1 mb-1`} onClick={() => setListagemInscritos(false)}><LuX className={`${styles.iconClose}`} /> </button> <button type="button" className={`${styles.btnClose} d-flex float-end p-1 mb-1`} onClick={() => setListagemInscritos(false)}><LuX className={`${styles.iconClose}`} /> </button>
<table className="table table-striped table-hover align-middle mt-3"> <table className="table table-striped table-hover align-middle mt-3">
@@ -292,12 +294,12 @@ export default function Workshop() {
</tbody> </tbody>
</table> </table>
</div> </div>
</div> </motion.div>
) : null} ) : null}
</div> </div>
{!formEdit && role === 1 ? ( {!formEdit && role === 1 ? (
<div className={`${styles.buttonsContainer} d-flex flex-wrap gap-2 justify-content-center mt-5`}> <motion.div variants={slideFromTopVariants} initial="initial" animate="animate" transition={slideFromTopTransition} className={`${styles.buttonsContainer} d-flex flex-wrap gap-2 justify-content-center mt-5`}>
<button <button
type="button" type="button"
className={`${styles.updateButton} bg-primary`} className={`${styles.updateButton} bg-primary`}
@@ -317,14 +319,14 @@ export default function Workshop() {
{workshop.status === "pending" ? ( {workshop.status === "pending" ? (
<button type="button" className={`${styles.cancelButton}`} onClick={handleCancelWorkshop}><LuTrash2 className="mb-1 btn-danger" /> Cancelar Workshop</button> <button type="button" className={`${styles.cancelButton}`} onClick={handleCancelWorkshop}><LuTrash2 className="mb-1 btn-danger" /> Cancelar Workshop</button>
) : null} ) : null}
</div> </motion.div>
) : null} ) : null}
{formEdit ? ( {formEdit ? (
<form className={`${styles.formEdit} mt-5`} onSubmit={update}> <motion.form variants={slideFromTopVariants} initial="initial" animate="animate" transition={slideFromTopTransition} className={`${styles.formEdit} mt-5`} onSubmit={update}>
<span className={styles.subtitle}>Alterar detalhes do workshop</span> <span className={styles.subtitle}>Alterar detalhes do workshop</span>
<div className="row mx-auto g-4 mt-1"> <div className="row mx-auto g-4 mt-1">
<div className="col-12 px-1 text-start"> <div className="col-12 px-1 text-start">
@@ -453,7 +455,7 @@ export default function Workshop() {
<button type="submit" className={`${styles.updateButton} bg-primary`} ><LuCheck className="mb-1" /> Submeter dados</button> <button type="submit" className={`${styles.updateButton} bg-primary`} ><LuCheck className="mb-1" /> Submeter dados</button>
</div> </div>
</div> </div>
</form> </motion.form>
) : null} ) : null}
</> </>
) : ( ) : (

View File

@@ -7,6 +7,8 @@ import { LuCheck, LuTrash2, LuX, LuPencil, LuArrowLeft, LuCalendar, LuClock3 } f
import { imageSkeletonFadeStyle, onImageSkeletonLoad } from "../../../../utils/imageSkeleton"; import { imageSkeletonFadeStyle, onImageSkeletonLoad } from "../../../../utils/imageSkeleton";
import { CgSpinner } from "react-icons/cg"; import { CgSpinner } from "react-icons/cg";
import Swal from "sweetalert2"; import Swal from "sweetalert2";
import { motion } from "framer-motion";
import { slideFromLeftVariants, slideFromLeftOnMountTransition, pageTitleTransition, pageTitleVariants, slideFromBottomVariants, slideFromBottomTransition, slideFromRightVariants, slideFromRightTransition, slideFromTopVariants, slideFromTopTransition, viewportOnce, cardInViewTransition, cardInViewVariants } from "../../../../utils/pageMotionVariants";
export default function User() { export default function User() {
const { id } = useParams(); const { id } = useParams();
@@ -161,17 +163,17 @@ export default function User() {
return ( return (
<div className={`${styles.container} p-1 p-xl-0`}> <div className={`${styles.container} p-1 p-xl-0`}>
<div className={"text-start mt-5"}> <motion.div variants={slideFromLeftVariants} initial="initial" animate="animate" transition={slideFromLeftOnMountTransition} className={"text-start mt-5"}>
<button className={`${styles.button} border-0 bg-transparent fs-5`}><Link className={`${styles.link} text-decoration-none`} to="/admin/users"><LuArrowLeft className={`${styles.LinkIcon} mb-1 fs-4 align-items-center`} /> <span className={styles.linkText}>Utilizadores</span> </Link></button> <button className={`${styles.button} border-0 bg-transparent fs-5`}><Link className={`${styles.link} text-decoration-none`} to="/admin/users"><LuArrowLeft className={`${styles.LinkIcon} mb-1 fs-4 align-items-center`} /> <span className={styles.linkText}>Utilizadores</span> </Link></button>
</div> </motion.div>
{user ? ( {user ? (
<div className="my-3"> <div className="my-3">
<span className={styles.title}>Dados do Utilizador </span> <motion.h1 variants={pageTitleVariants} initial="initial" animate="animate" transition={pageTitleTransition} className={styles.title}>Dados do Utilizador </motion.h1>
<div className="row mt-5 justify-content-between px-2"> <div className="row mt-5 justify-content-between px-2">
<div className="col-12 col-lg-8 p-2 rounded-3"> <motion.div variants={slideFromLeftVariants} initial="initial" animate="animate" transition={slideFromLeftOnMountTransition} className="col-12 col-lg-8 p-2 rounded-3">
<div className={`${styles.userCard} d-flex flex-column flex-md-row g-5 p-4 bg-white h-100`}> <div className={`${styles.userCard} d-flex flex-column flex-md-row g-5 p-4 bg-white h-100`}>
<div className="col-12 col-md-6 d-flex flex-column text-start flex-column mt-xl-0"> <div className="col-12 col-md-6 d-flex flex-column text-start flex-column mt-xl-0">
<span className={`badge text-uppercase bg-primary fw-bold px-4 py-3 fs-6 mb-3 d-flex d-md-none align-self-center ${user.role_id === 1 ? 'bg-primary bg-gradient text-white' : 'bg-secondary bg-gradient text-white'} `} style={{ width: "fit-content" }}>{user.role_id === 1 ? "Administrador" : "Utilizador"}</span> <span className={`badge text-uppercase bg-primary fw-bold px-4 py-3 fs-6 mb-3 d-flex d-md-none align-self-center ${user.role_id === 1 ? 'bg-primary bg-gradient text-white' : 'bg-secondary bg-gradient text-white'} `} style={{ width: "fit-content" }}>{user.role_id === 1 ? "Administrador" : "Utilizador"}</span>
@@ -192,8 +194,8 @@ export default function User() {
</div> </div>
</div> </div>
</div> </div>
</div> </motion.div>
<div className="col-12 col-lg-4 p-2"> <motion.div variants={slideFromRightVariants} initial="initial" animate="animate" transition={slideFromRightTransition} className="col-12 col-lg-4 p-2">
<div className={`${styles.userVideosWatched} text-start d-flex flex-column h-100 gap-3 p-4`}> <div className={`${styles.userVideosWatched} text-start d-flex flex-column h-100 gap-3 p-4`}>
<div className="d-flex flex-column"> <div className="d-flex flex-column">
<span className="fw-normal fs-6" style={{ color: "var(--bg-primary-color-opacity)" }}>Vídeos assistidos</span> <span className="fw-normal fs-6" style={{ color: "var(--bg-primary-color-opacity)" }}>Vídeos assistidos</span>
@@ -206,15 +208,49 @@ export default function User() {
</div> </div>
</div> </div>
</div> </motion.div>
<div className="col-12 p-2 mt-4 mb-3"> <motion.div className="mt-3">
{formEdit ? (
<motion.div variants={slideFromTopVariants} initial="initial" animate="animate" transition={slideFromTopTransition} className="my-3">
<span className={styles.title}>Editar Utilizador</span>
<form method="patch" onSubmit={update} id="formEditUser">
<div className="row g-3 mt-2">
<input type="hidden" name="user_id" value={user?.id} />
<div className="col-12 col-md-6 text-start">
<label className="form-label fw-bold" htmlFor="name">Nome</label>
<input type="text" className="form-control py-2" id="name" name="name" defaultValue={user?.name} />
</div>
<div className="col-12 col-md-6 text-start">
<label className="form-label fw-bold" htmlFor="email">Email</label>
<input type="email" className="form-control py-2" id="email" name="email" defaultValue={user?.email} />
</div>
<div className="col-12 col-md-6 text-start">
<label className="form-label fw-bold" htmlFor="role_id">Cargo</label>
<select className="form-select py-2" id="role_id" name="role_id" defaultValue={user?.role_id}>
<option value="1">Administrador</option>
<option value="2">Utilizador</option>
</select>
</div>
</div>
<div className="buttonsContainer mt-4 d-flex gap-2 justify-content-center" >
<button type="button" className={`${styles.closeFormButton}`} onClick={() => setFormEdit(false)}> Cancelar <LuX className={`${styles.icon} mb-1`} /></button>
<button type="submit" className={`${styles.submitFormButton}`} >Submeter dados <LuCheck className="mb-1" /></button>
</div>
</form>
</motion.div>
) : null}
</motion.div>
<motion.div variants={slideFromBottomVariants} initial="initial" animate="animate" transition={slideFromBottomTransition} className="col-12 p-2 mt-4 mb-3">
<span className={`${styles.subtitle} text-start`}>Workshops inscrito</span> <span className={`${styles.subtitle} text-start`}>Workshops inscrito</span>
</div> </motion.div>
{workshopsParticipated.length > 0 ? ( {workshopsParticipated.length > 0 ? (
workshopsParticipated.map((workshop) => ( workshopsParticipated.map((workshop) => (
<div key={workshop.id} className="col-12 col-sm-6 col-lg-4 p-2"> <div key={workshop.id} className="col-12 col-sm-6 col-lg-4 p-2">
<div className={`${styles.boxWorkshop} text-start pb-3`}> <motion.div variants={cardInViewVariants} initial="initial" whileInView="animate" viewport={viewportOnce} transition={cardInViewTransition} className={`${styles.boxWorkshop} text-start pb-3`}>
<div className={`${styles.thumbnailWorkshop} position-relative`}> <div className={`${styles.thumbnailWorkshop} position-relative`}>
<img <img
src={`${API_URL}/storage/${workshop.image}`} src={`${API_URL}/storage/${workshop.image}`}
@@ -243,49 +279,17 @@ export default function User() {
Ver detalhes Ver detalhes
</Link> </Link>
</div> </div>
</div> </motion.div>
</div> </div>
)) ))
) : ( ) : (
<div className="col-12 text-center ps-1"> <motion.div variants={slideFromBottomVariants} whileInView="animate" viewport={viewportOnce} transition={slideFromBottomTransition} className="col-12 text-center ps-1">
<span className="text-muted fs-5">Este utilizador ainda não participou em nenhum workshop</span> <span className="text-muted fs-5">Este utilizador ainda não participou em nenhum workshop</span>
</div> </motion.div>
)} )}
</div> </div>
<div className="mt-5">
{formEdit ? (
<div className="my-3">
<span className={styles.title}>Editar Utilizador</span>
<form method="patch" onSubmit={update} id="formEditUser">
<div className="row g-3 mt-2">
<input type="hidden" name="user_id" value={user?.id} />
<div className="col-12 col-md-6 text-start">
<label className="form-label fw-bold" htmlFor="name">Nome</label>
<input type="text" className="form-control py-2" id="name" name="name" defaultValue={user?.name} />
</div>
<div className="col-12 col-md-6 text-start">
<label className="form-label fw-bold" htmlFor="email">Email</label>
<input type="email" className="form-control py-2" id="email" name="email" defaultValue={user?.email} />
</div>
<div className="col-12 col-md-6 text-start">
<label className="form-label fw-bold" htmlFor="role_id">Cargo</label>
<select className="form-select py-2" id="role_id" name="role_id" defaultValue={user?.role_id}>
<option value="1">Administrador</option>
<option value="2">Utilizador</option>
</select>
</div>
</div>
<div className="buttonsContainer mt-4 d-flex gap-2 justify-content-center" >
<button type="button" className={`${styles.closeFormButton}`} onClick={() => setFormEdit(false)}> Cancelar <LuX className={`${styles.icon} mb-1`} /></button>
<button type="submit" className={`${styles.submitFormButton}`} >Submeter dados <LuCheck className="mb-1" /></button>
</div>
</form>
</div>
) : null}
</div>
</div> </div>
) : ( ) : (
<div className="text-center alert alert-danger mt-5 align-items-center"> <div className="text-center alert alert-danger mt-5 align-items-center">

View File

@@ -8,6 +8,22 @@ import { LuPencil, LuSettings2, LuPlus } from "react-icons/lu";
import { CgSpinner } from "react-icons/cg"; import { CgSpinner } from "react-icons/cg";
import { Form, Pagination, Table } from "react-bootstrap"; import { Form, Pagination, Table } from "react-bootstrap";
import { AnimatePresence, motion } from "framer-motion"; import { AnimatePresence, motion } from "framer-motion";
import {
dropdownTransition,
dropdownVariants,
fadeTransition,
fadeVariants,
listContentTransition,
listContentVariants,
pageTitleTransition,
pageTitleVariants,
slideFromLeftTransition,
slideFromLeftVariants,
slideFromRightOnMountTransition,
slideFromRightTransition,
slideFromRightVariants,
viewportOnce,
} from "../../../../utils/pageMotionVariants";
export default function Users() { export default function Users() {
@@ -22,6 +38,7 @@ export default function Users() {
const [listTotal, setListTotal] = useState(0); const [listTotal, setListTotal] = useState(0);
const [loadingUsers, setLoadingUsers] = useState(false); const [loadingUsers, setLoadingUsers] = useState(false);
const debouncedSearch = useDebounce(search, 500); const debouncedSearch = useDebounce(search, 500);
const listContentKey = `${currentPage}-${debouncedSearch}-${selectedUsers}`;
useEffect(() => { useEffect(() => {
index(debouncedSearch); index(debouncedSearch);
@@ -79,19 +96,18 @@ export default function Users() {
} }
return ( return (
<div className={`${styles.container} p-3 p-xl-0`}> <div className={`${styles.container} p-2 p-sm-4 p-lg-0`}>
<h1 className={styles.title}>Utilizadores</h1> <motion.h1 variants={pageTitleVariants} initial="initial" animate="animate" transition={pageTitleTransition} className={styles.title}>Utilizadores</motion.h1>
<div className="row py-3 g-4 justify-content-between"> <div className="row py-3 g-4 justify-content-between">
<div className="col-12 col-sm-7 col-md-8 col-lg-6 d-flex gap-2 text-start px-2 "> <motion.div variants={slideFromLeftVariants} initial="initial" whileInView="animate" viewport={viewportOnce} transition={slideFromLeftTransition} className="col-12 col-sm-7 col-md-8 col-lg-6 d-flex gap-2 text-start px-2">
<Form.Control type="text" placeholder="Pesquise por nome ou email..." <Form.Control type="text" placeholder="Pesquise por nome ou email..."
value={search} onChange={(e) => { value={search} onChange={(e) => {
setSearch(e.target.value); setSearch(e.target.value);
}} /> }} />
</motion.div>
</div> <motion.div variants={slideFromRightVariants} initial="initial" whileInView="animate" viewport={viewportOnce} transition={slideFromRightTransition} className="col-12 col-sm-5 col-md-4 col-lg-3 d-flex mt-2 mt-sm-4">
<div className="col-12 col-sm-5 col-md-4 col-lg-3 d-flex mt-2 mt-sm-4">
<div <div
className="btn-group flex-grow-1 position-relative" className="btn-group flex-grow-1 position-relative"
onMouseEnter={() => setShowFilterDropdown(true)} onMouseEnter={() => setShowFilterDropdown(true)}
@@ -104,10 +120,11 @@ export default function Users() {
<AnimatePresence> <AnimatePresence>
{showFilterDropdown && ( {showFilterDropdown && (
<motion.ul <motion.ul
initial={{ opacity: 0, y: -10, scale: 0.98 }} variants={dropdownVariants}
animate={{ opacity: 1, y: 0, scale: 1 }} initial="initial"
exit={{ opacity: 0, y: -10, scale: 0.98 }} animate="animate"
transition={{ duration: 0.2, ease: "easeOut" }} exit="exit"
transition={dropdownTransition}
className="dropdown-menu text-center w-100" className="dropdown-menu text-center w-100"
style={{ display: "block", zIndex: 4000, top: "100%", marginTop: "0.25rem" }} style={{ display: "block", zIndex: 4000, top: "100%", marginTop: "0.25rem" }}
> >
@@ -142,18 +159,29 @@ export default function Users() {
)} )}
</AnimatePresence> </AnimatePresence>
</div> </div>
</div> </motion.div>
<div className="col-12 col-sm col-md-12 col-lg-3 text-end align-content-center mt-4 mt-lg-4"> <motion.div variants={slideFromRightVariants} initial="initial" animate="animate" transition={slideFromRightOnMountTransition} className="col-12 col-sm col-md-12 col-lg-3 text-end align-content-center mt-4 mt-lg-4">
<Link to="/admin/create-user" className={`${styles.createButton} text-decoration-none`}><LuPlus className="mb-1" /> Novo Utilizador</Link> <Link to="/admin/create-user" className={`${styles.createButton} text-decoration-none`}><LuPlus className="mb-1" /> Novo Utilizador</Link>
</div> </motion.div>
</div> </div>
{loadingUsers ? ( <div className="mt-3">
<div className="text-center mt-5"> <AnimatePresence mode="wait">
<CgSpinner className={`${styles.animateSpin} text-2xl fs-3`} /> {loadingUsers ? (
</div> <motion.div key="loading" variants={fadeVariants} initial="initial" animate="animate" exit="exit" transition={fadeTransition} className="text-center mt-5">
) : users.length > 0 ? ( <CgSpinner className={`${styles.animateSpin} text-2xl fs-3`} />
</motion.div>
) : (
<motion.div
key={listContentKey}
variants={listContentVariants}
initial="initial"
animate="animate"
exit="exit"
transition={listContentTransition}
>
{users.length > 0 ? (
<div> <div>
<div className={`${styles.table} mt-3 mb-2`}> <div className={`${styles.table} mt-3 mb-2`}>
<Table responsive className="mb-0"> <Table responsive className="mb-0">
@@ -229,6 +257,10 @@ export default function Users() {
)} )}
</div> </div>
)} )}
</motion.div>
)}
</AnimatePresence>
</div>
</div> </div>
) )
} }

View File

@@ -6,6 +6,18 @@ import { useState } from "react";
import type { ApiErrorResponse } from "../../../types"; import type { ApiErrorResponse } from "../../../types";
import { Navigate } from "react-router"; import { Navigate } from "react-router";
import AccordionFAQS from "../../../components/accordion FAQ´s/accordionFAQS"; import AccordionFAQS from "../../../components/accordion FAQ´s/accordionFAQS";
import { motion } from "framer-motion";
import {
pageTitleTransition,
pageTitleVariants,
slideFromBottomTransition,
slideFromBottomVariants,
slideFromLeftTransition,
slideFromLeftVariants,
slideFromRightTransition,
slideFromRightVariants,
viewportOnce,
} from "../../../utils/pageMotionVariants";
export default function Contactos() { export default function Contactos() {
const token = localStorage.getItem("token"); const token = localStorage.getItem("token");
@@ -64,9 +76,9 @@ export default function Contactos() {
return ( return (
<div className={styles.container} > <div className={styles.container} >
<h1 className={styles.title}>Contactos</h1> <motion.h1 variants={pageTitleVariants} initial="initial" animate="animate" transition={pageTitleTransition} className={styles.title}>Contactos</motion.h1>
<div className="row"> <div className="row">
<div className="col-12 col-lg-6 text-start d-flex flex-column gap-3 px-4 mt-3 mt-lg-0"> <motion.div variants={slideFromLeftVariants} initial="initial" whileInView="animate" viewport={viewportOnce} transition={slideFromLeftTransition} className="col-12 col-lg-6 text-start d-flex flex-column gap-3 px-4 mt-3 mt-lg-0">
<div className="text-center text-sm-start"> <div className="text-center text-sm-start">
<h2 className={styles.subtitle}>Os nossos dados</h2> <h2 className={styles.subtitle}>Os nossos dados</h2>
<span >Pode contactar-nos através do nosso email, telefone ou morada.</span> <span >Pode contactar-nos através do nosso email, telefone ou morada.</span>
@@ -91,8 +103,8 @@ export default function Contactos() {
<span className={styles.contactData}>Rua da Escola, 123, Lisboa, Portugal</span></div> <span className={styles.contactData}>Rua da Escola, 123, Lisboa, Portugal</span></div>
</a> </a>
</div> </div>
</div> </motion.div>
<div className="col-12 col-lg-6 text-start d-flex flex-column gap-4 px-4 mt-5 mt-lg-0"> <motion.div variants={slideFromRightVariants} initial="initial" whileInView="animate" viewport={viewportOnce} transition={slideFromRightTransition} className="col-12 col-lg-6 text-start d-flex flex-column gap-4 px-4 mt-5 mt-lg-0">
<div className="text-center text-sm-start"> <div className="text-center text-sm-start">
<h2 className={styles.subtitle}>Formulário de contacto</h2> <h2 className={styles.subtitle}>Formulário de contacto</h2>
<span>Preencha o formulário abaixo para entrar em contacto connosco</span> <span>Preencha o formulário abaixo para entrar em contacto connosco</span>
@@ -142,16 +154,16 @@ export default function Contactos() {
</div> </div>
</form> </form>
</div> </div>
</div> </motion.div>
<div className="col-12 mt-5 text-center"> <motion.div variants={slideFromBottomVariants} initial="initial" whileInView="animate" viewport={viewportOnce} transition={slideFromBottomTransition} className="col-12 mt-5 text-center">
<span className={`${styles.title}`}>Perguntas Frequentes</span> <span className={`${styles.title}`}>Perguntas Frequentes</span>
<div className="row mt-4"> <div className="row mt-4">
<div className="col-12 mx-auto"> <div className="col-12 mx-auto">
<AccordionFAQS /> <AccordionFAQS />
</div> </div>
</div> </div>
</div> </motion.div>
</div> </div>
</div > </div >
); );

View File

@@ -7,6 +7,16 @@ import { Link } from "react-router";
import { CgSpinner } from "react-icons/cg"; import { CgSpinner } from "react-icons/cg";
import AnimatedProgressBar from "../../../components/animatedProgressBar"; import AnimatedProgressBar from "../../../components/animatedProgressBar";
import AnimatedCounter from "../../../components/animatedNumberCount"; import AnimatedCounter from "../../../components/animatedNumberCount";
import { motion } from "framer-motion";
import {
slideFromLeftOnMountTransition,
slideFromLeftInViewTransition03,
slideFromLeftVariants,
slideFromRightOnMountTransition02,
slideFromRightInViewTransition04,
slideFromRightVariants,
viewportOnce,
} from "../../../utils/pageMotionVariants";
/* import { useGetCurrentUser } from "../../../hooks/useGetCurrentUser" /* import { useGetCurrentUser } from "../../../hooks/useGetCurrentUser"
import { useGetVideosLength } from "../../../hooks/useGetVideosLength"; import { useGetVideosLength } from "../../../hooks/useGetVideosLength";
@@ -190,7 +200,7 @@ export default function Home() {
return ( return (
<div className={`${styles.container} mx-2 mx-sm-0 p-2 p-sm-4 p-lg-0`}> <div className={`${styles.container} mx-2 mx-sm-0 p-2 p-sm-4 p-lg-0`}>
<div className={`${styles.containerVideos} px-2 p-sm-4 mx-sm-1 mx-lg-0`}> <motion.div variants={slideFromLeftVariants} initial="initial" animate="animate" transition={slideFromLeftOnMountTransition} className={`${styles.containerVideos} px-2 p-sm-4 mx-sm-1 mx-lg-0`}>
<div className="d-flex flex-column flex-sm-row justify-content-between align-items-center px-0" > <div className="d-flex flex-column flex-sm-row justify-content-between align-items-center px-0" >
<h2 className={`${styles.subtitle} text-center text-sm-start mt-4 mt-sm-3 mb-4 px-0 `}>{role === 1 ? "Vídeos ativos" : "Continuar Formação"}</h2> <h2 className={`${styles.subtitle} text-center text-sm-start mt-4 mt-sm-3 mb-4 px-0 `}>{role === 1 ? "Vídeos ativos" : "Continuar Formação"}</h2>
<Link to="/videos" className={`${styles.link} text-decoration-none fw-semibold fs-5 mt-sm-3 mb-4`}>Ver vídeos <LuArrowUpRight className="mb-1 me-2" size={25} /></Link> <Link to="/videos" className={`${styles.link} text-decoration-none fw-semibold fs-5 mt-sm-3 mb-4`}>Ver vídeos <LuArrowUpRight className="mb-1 me-2" size={25} /></Link>
@@ -198,13 +208,13 @@ export default function Home() {
{role !== 1 && ( {role !== 1 && (
<> <>
<AnimatedProgressBar value={progressoVideos} className={`${styles.progressBar}`} /> <AnimatedProgressBar value={progressoVideos ?? 0} className={`${styles.progressBar}`} />
{progressoVideos === 100 && ( {progressoVideos === 100 && (
<div className="text-center mt-3"> <div className="text-center mt-3">
<span className="text-black fw-bold fs-6">Parabéns! A sua formação está completa!</span> <span className="text-black fw-bold fs-6">Parabéns! A sua formação está completa!</span>
</div> </div>
)} )}
</> </>
)} )}
@@ -237,9 +247,9 @@ export default function Home() {
</div> </div>
) : null} ) : null}
</div> </div>
</div> </motion.div>
<div className="ms-0 px-lg-4 mt-4"> <motion.div variants={slideFromRightVariants} initial="initial" whileInView="animate" viewport={viewportOnce} transition={slideFromRightOnMountTransition02} className="ms-0 px-lg-4 mt-4">
<div className="d-flex flex-column flex-sm-row justify-content-between align-items-center px-0" > <div className="d-flex flex-column flex-sm-row justify-content-between align-items-center px-0" >
<h2 className={`${styles.subtitle} text-center text-sm-start mt-4 mt-sm-3 mb-4`}>Próximos workshops</h2> <h2 className={`${styles.subtitle} text-center text-sm-start mt-4 mt-sm-3 mb-4`}>Próximos workshops</h2>
<Link to="/workshops" className={`${styles.link} text-decoration-none fw-semibold fs-5 mt-sm-3 mb-sm-4`}>Ver workshops <LuArrowUpRight className="mb-1 me-2" size={25} /></Link> <Link to="/workshops" className={`${styles.link} text-decoration-none fw-semibold fs-5 mt-sm-3 mb-sm-4`}>Ver workshops <LuArrowUpRight className="mb-1 me-2" size={25} /></Link>
@@ -261,11 +271,11 @@ export default function Home() {
<p className={`${styles.timeWorkshop}`}> <LuClock3 className={`${styles.icon} mb-1 me-2`} />{workshop.time_start.slice(0, 5)} - {workshop.time_end.slice(0, 5)}</p> <p className={`${styles.timeWorkshop}`}> <LuClock3 className={`${styles.icon} mb-1 me-2`} />{workshop.time_start.slice(0, 5)} - {workshop.time_end.slice(0, 5)}</p>
</div> </div>
</div> </div>
<h2 className={`${styles.titleWorkshop} d-block text-start mt-3 px-3 flex-grow-1 mb-0`}>{workshop.title}</h2> <h2 className={`${styles.titleWorkshop} d-block text-start mt-3 px-3 flex-grow-1 mb-0`}>{workshop.title}</h2>
<div className="position-relative d-flex flex-column d-md-none d-xl-flex d-xxl-none mt-3 px-3"> <div className="position-relative d-flex flex-column d-md-none d-xl-flex d-xxl-none mt-3 px-3">
<span className={`${styles.dateWorkshopMobile} text-start mb-1`}><LuCalendar className={`${styles.icon} mb-1 me-2`} />{workshop.date.split('-').reverse().join('-')}</span> <span className={`${styles.dateWorkshopMobile} text-start mb-1`}><LuCalendar className={`${styles.icon} mb-1 me-2`} />{workshop.date.split('-').reverse().join('-')}</span>
<span className={`${styles.timeWorkshopMobile} text-start`}><LuClock3 className={`${styles.icon} mb-1 me-2`} />{workshop.time_start.slice(0, 5).split(':').join('h')} - {workshop.time_end.slice(0, 5).split(':').join('h')}</span> <span className={`${styles.timeWorkshopMobile} text-start`}><LuClock3 className={`${styles.icon} mb-1 me-2`} />{workshop.time_start.slice(0, 5).split(':').join('h')} - {workshop.time_end.slice(0, 5).split(':').join('h')}</span>
</div> </div>
<div className="d-flex flex-wrap justify-content-evenly px-3 gap-2 mt-3"> <div className="d-flex flex-wrap justify-content-evenly px-3 gap-2 mt-3">
<Link to={`${role === 1 ? `/admin/edit-workshop/${workshop.id}` : `/workshop/${workshop.id}`}`} className={`${styles.linkWorkshop} d-block text-center py-2 px-5 text-decoration-none`}> Detalhes </Link> <Link to={`${role === 1 ? `/admin/edit-workshop/${workshop.id}` : `/workshop/${workshop.id}`}`} className={`${styles.linkWorkshop} d-block text-center py-2 px-5 text-decoration-none`}> Detalhes </Link>
@@ -290,11 +300,11 @@ export default function Home() {
</div> </div>
) : null} ) : null}
</div> </div>
</div> </motion.div>
<div className="row"> <div className="row">
<div className="col-12 col-lg-8 mt-5 px-3"> <motion.div variants={slideFromLeftVariants} initial="initial" whileInView="animate" viewport={viewportOnce} transition={slideFromLeftInViewTransition03} className="col-12 col-lg-8 mt-5 px-3">
<form onSubmit={sendMail} className="pt-2 px-1"> <form onSubmit={sendMail} className="pt-2 px-1">
<div className="row g-3 bg-white p-4 mt-3" style={{ borderRadius: "var(--border-radius)" }}> <div className="row g-3 bg-white p-4 mt-3" style={{ borderRadius: "var(--border-radius)" }}>
<div className="text-center text-sm-start mb-4"> <div className="text-center text-sm-start mb-4">
@@ -342,23 +352,33 @@ export default function Home() {
</div> </div>
</div> </div>
</form> </form>
</div> </motion.div>
<div className="col-12 col-lg-4 mt-lg-5 px-sm-0 ps-lg-4 pe-lg-3 px-2"> <motion.div variants={slideFromRightVariants} initial="initial" whileInView="animate" viewport={viewportOnce} transition={slideFromRightInViewTransition04} className="col-12 col-lg-4 mt-lg-5 px-sm-0 ps-lg-4 pe-lg-3 px-2">
<div className="h-100 pt-4 px-1 px-lg-0"> <div className="h-100 pt-4 px-1 px-lg-0">
<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={`${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"> <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-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 ? <AnimatedCounter value={videosCount} /> : <AnimatedCounter value={videosWatched} />}/<AnimatedCounter value={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>
<div className="d-flex flex-column"> <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-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 ? <AnimatedCounter value={workshopsCount} /> : <AnimatedCounter value={workshopsInscribed} />}/{<AnimatedCounter value={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> </div>
</div> </div>
</div> </motion.div>
</div> </div>
</div> </div>

View File

@@ -6,9 +6,27 @@ import { LuCheck, LuKeyRound, LuPencil, LuX, LuArrowUpRight, LuClock3, LuCalenda
import { CgSpinner } from "react-icons/cg"; import { CgSpinner } from "react-icons/cg";
import { Link } from "react-router"; import { Link } from "react-router";
import AnimatedProgressBar from "../../../components/animatedProgressBar"; import AnimatedProgressBar from "../../../components/animatedProgressBar";
import AnimatedCounter from "../../../components/animatedNumberCount";
import { imageSkeletonFadeStyle, onImageSkeletonLoad } from "../../../utils/imageSkeleton"; import { imageSkeletonFadeStyle, onImageSkeletonLoad } from "../../../utils/imageSkeleton";
import { PiCheckCircleFill } from "react-icons/pi"; import { PiCheckCircleFill } from "react-icons/pi";
import Swal from "sweetalert2"; import Swal from "sweetalert2";
import { motion } from "framer-motion";
import {
cardInViewTransition,
cardInViewVariants,
pageTitleTransition,
pageTitleVariants,
slideFromBottomInViewTransition,
slideFromBottomVariants,
slideFromLeftTransitionNoDelay,
slideFromLeftVariants,
slideFromRightTransition,
slideFromRightVariants,
slideFromTopTransition,
slideFromTopTransitionDelayed,
slideFromTopVariants,
viewportOnce,
} from "../../../utils/pageMotionVariants";
export default function Profile() { export default function Profile() {
const [role, setRole] = useState(0); const [role, setRole] = useState(0);
@@ -197,11 +215,11 @@ export default function Profile() {
return ( return (
<div className="container"> <div className="container">
<span className={styles.title}>Os meus dados</span> <motion.h1 variants={pageTitleVariants} initial="initial" animate="animate" transition={pageTitleTransition} className={styles.title}>Os meus dados</motion.h1>
<div> <div>
<div className="my-3"> <div className="my-3">
<div className="row mt-5 justify-content-between"> <div className="row mt-5 justify-content-between">
<div className="col-12 col-lg-8 p-3 rounded-3"> <motion.div variants={slideFromLeftVariants} initial="initial" whileInView="animate" viewport={viewportOnce} transition={slideFromLeftTransitionNoDelay} className="col-12 col-lg-8 p-3 rounded-3">
<div className={`${styles.userCard} p-4 bg-white h-100`}> <div className={`${styles.userCard} p-4 bg-white h-100`}>
<div className="row justify-content-between"> <div className="row justify-content-between">
<div className="col-12 d-flex text-start mt-xl-0 justify-content-between flex-grow-1 mb-3"> <div className="col-12 d-flex text-start mt-xl-0 justify-content-between flex-grow-1 mb-3">
@@ -213,46 +231,46 @@ export default function Profile() {
<div className="col-12 d-flex flex-column flex-md-row gap-0 gap-md-2 text-end align-items-start align-items-md-end mt-md-0 mx-auto mx-md-0 flex-wrap"> <div className="col-12 d-flex flex-column flex-md-row gap-0 gap-md-2 text-end align-items-start align-items-md-end mt-md-0 mx-auto mx-md-0 flex-wrap">
<span className="d-flex justify-content-center flex-wrap flex-sm-nowrap border border-secondary-subtle py-2 px-2 rounded-3 fs-6 fw-medium py-0" style={{ maxWidth: "fit-content", height: "fit-content" }}>Membro desde: <b className="fw-bold fs-6">{new Date(userData?.created_at as string).toLocaleDateString('pt-PT')}</b></span> <span className="d-flex justify-content-center flex-wrap flex-sm-nowrap border border-secondary-subtle py-2 px-2 rounded-3 fs-6 fw-medium py-0" style={{ maxWidth: "fit-content", height: "fit-content" }}>Membro desde: <b className="fw-bold fs-6">{new Date(userData?.created_at as string).toLocaleDateString('pt-PT')}</b></span>
<div className="d-flex flex-wrap flex-grow-1 justify-content-evenly justify-content-md-end flex-wrap mt-3 mx-auto"> <div className="d-flex flex-wrap flex-grow-1 justify-content-evenly justify-content-md-end flex-wrap mt-3 mx-auto">
<a className={`${styles.updateButton} text-decoration-none text-primary text-center px-3`} onClick={() => { setFormEdit(true); }}> Editar <LuPencil className={`${styles.icon} mb-1 text-primary`} /></a> <a className={`${styles.updateButton} text-decoration-none text-primary text-center px-3`} onClick={() => { setFormEdit(true); setFormEditPassword(false); }}> Editar <LuPencil className={`${styles.icon} mb-1 text-primary`} /></a>
<a className={`${styles.passwordButton} text-decoration-none text-danger px-1 px-md-3 <a className={`${styles.passwordButton} text-decoration-none text-danger px-1 px-md-3
`} onClick={() => { setFormEditPassword(true); }}> Alterar password <LuKeyRound className={`${styles.icon} mb-1 text-danger`} /></a> `} onClick={() => { setFormEditPassword(true); setFormEdit(false); }}> Alterar password <LuKeyRound className={`${styles.icon} mb-1 text-danger`} /></a>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </motion.div>
<div className="col-12 col-lg-4 p-3"> <motion.div variants={slideFromRightVariants} initial="initial" whileInView="animate" viewport={viewportOnce} transition={slideFromRightTransition} className="col-12 col-lg-4 p-3">
{role === 1 ? ( {role === 1 ? (
<div className={`${styles.userVideosWatched} text-start d-flex flex-column h-100 gap-3 p-4`}> <div className={`${styles.userVideosWatched} text-start d-flex flex-column h-100 gap-3 p-4`}>
<div className="d-flex flex-column"> <div className="d-flex flex-column">
<span className="fw-normal fs-6" style={{ color: "var(--bg-grey)" }}>Vídeos ativos</span> <span className="fw-normal fs-6" style={{ color: "var(--bg-grey)" }}>Vídeos ativos</span>
<span className="fs-2 fw-bold text-white">{videosCount}</span> <span className="fs-2 fw-bold text-white"><AnimatedCounter value={videosCount} /></span>
</div> </div>
<div className="d-flex flex-column"> <div className="d-flex flex-column">
<span className="fw-normal fs-6" style={{ color: "var(--bg-grey)" }}>Workshops agendados</span> <span className="fw-normal fs-6" style={{ color: "var(--bg-grey)" }}>Workshops agendados</span>
<span className="fs-2 fw-bold text-white">{workshopsCount}</span> <span className="fs-2 fw-bold text-white"><AnimatedCounter value={workshopsCount} /></span>
</div> </div>
</div> </div>
) : ( ) : (
<div className={`${styles.userVideosWatched} text-start d-flex flex-column h-100 gap-3 p-4`}> <div className={`${styles.userVideosWatched} text-start d-flex flex-column h-100 gap-3 p-4`}>
<div className="d-flex flex-column"> <div className="d-flex flex-column">
<span className="fw-normal fs-6" style={{ color: "var(--bg-grey)" }}>Vídeos assistidos</span> <span className="fw-normal fs-6" style={{ color: "var(--bg-grey)" }}>Vídeos assistidos</span>
<span className="fs-2 fw-bold text-white">{videosWatched}/{videosCount}</span> <span className="fs-2 fw-bold text-white"><AnimatedCounter value={videosWatched} />/<AnimatedCounter value={videosCount} /></span>
</div> </div>
<div className="d-flex flex-column"> <div className="d-flex flex-column">
<span className="fw-normal fs-6" style={{ color: "var(--bg-grey)" }}>Workshops inscrito</span> <span className="fw-normal fs-6" style={{ color: "var(--bg-grey)" }}>Workshops inscrito</span>
<span className="fs-2 fw-bold text-white">{workshopsCount}</span> <span className="fs-2 fw-bold text-white"><AnimatedCounter value={workshopsCount} /></span>
</div> </div>
</div> </div>
)} )}
</div> </motion.div>
</div> </div>
</div> </div>
</div> </div>
{formEdit ? ( {formEdit ? (
<div className={`${styles.containerForm} my-3 px-2 p-sm-4`}> <motion.div variants={slideFromTopVariants} initial="initial" whileInView="animate" transition={slideFromTopTransition} className={`${styles.containerForm} my-3 px-2 p-sm-4`}>
<span className={styles.title}>Editar dados</span> <span className={styles.title}>Editar dados</span>
<form method="patch" onSubmit={update} id="formEditUser"> <form method="patch" onSubmit={update} id="formEditUser">
@@ -273,9 +291,9 @@ export default function Profile() {
<button type="submit" className={`${styles.submitFormButton}`} >Submeter dados <LuCheck className="mb-1" /></button> <button type="submit" className={`${styles.submitFormButton}`} >Submeter dados <LuCheck className="mb-1" /></button>
</div> </div>
</form> </form>
</div> </motion.div>
) : formEditPassword ? ( ) : formEditPassword ? (
<div className={`${styles.containerForm} my-3 px-2 p-sm-4`}> <motion.div variants={slideFromTopVariants} initial="initial" whileInView="animate" transition={slideFromTopTransitionDelayed} className={`${styles.containerForm} my-3 px-2 p-sm-4`}>
<span className={styles.title}>Alterar password</span> <span className={styles.title}>Alterar password</span>
<form method="patch" onSubmit={updatePassword} id="formEditPassword"> <form method="patch" onSubmit={updatePassword} id="formEditPassword">
@@ -300,10 +318,10 @@ export default function Profile() {
<button type="submit" className={`${styles.submitFormButton}`} >Submeter dados <LuCheck className="mb-1" /></button> <button type="submit" className={`${styles.submitFormButton}`} >Submeter dados <LuCheck className="mb-1" /></button>
</div> </div>
</form> </form>
</div> </motion.div>
) : null} ) : null}
<div className={`${styles.containerVideos} mt-4 px-2 p-sm-4 mx-1`}> <motion.div variants={slideFromBottomVariants} initial="initial" whileInView="animate" viewport={viewportOnce} transition={slideFromBottomInViewTransition} className={`${styles.containerVideos} mt-4 px-2 p-sm-4 mx-1`}>
<div className="d-flex flex-column flex-sm-row justify-content-between align-items-center px-0" > <div className="d-flex flex-column flex-sm-row justify-content-between align-items-center px-0" >
<h2 className={`${styles.subtitle} text-center text-sm-start mt-4 mt-sm-3 mb-4 px-0 `}>{role === 1 ? "Vídeos ativos" : "Continuar Formação"}</h2> <h2 className={`${styles.subtitle} text-center text-sm-start mt-4 mt-sm-3 mb-4 px-0 `}>{role === 1 ? "Vídeos ativos" : "Continuar Formação"}</h2>
<Link to="/videos" className={`${styles.link} text-decoration-none fw-semibold fs-5 mt-sm-3 mb-4`}>Ver vídeos <LuArrowUpRight className="mb-1 me-2" size={25} /></Link> <Link to="/videos" className={`${styles.link} text-decoration-none fw-semibold fs-5 mt-sm-3 mb-4`}>Ver vídeos <LuArrowUpRight className="mb-1 me-2" size={25} /></Link>
@@ -311,7 +329,7 @@ export default function Profile() {
{role !== 1 && ( {role !== 1 && (
<> <>
<AnimatedProgressBar value={progressoVideos} className={`${styles.progressBar}`} /> <AnimatedProgressBar value={progressoVideos ?? 0} className={`${styles.progressBar}`} />
{progressoVideos === 100 && ( {progressoVideos === 100 && (
<div className="text-center mt-3"> <div className="text-center mt-3">
@@ -323,8 +341,8 @@ export default function Profile() {
<div className="row mt-4 px-2"> <div className="row mt-4 px-2">
{videosCount > 0 ? videos.map((video: Video) => ( {videosCount > 0 ? videos.map((video: Video) => (
<div className="col-12 col-sm-6 col-lg-4 p-2"> <motion.div key={video.id} variants={cardInViewVariants} initial="initial" whileInView="animate" viewport={viewportOnce} transition={cardInViewTransition} className="col-12 col-sm-6 col-lg-4 p-2">
<Link to={`/video/${video.id}`} className={`${styles.linkVideo} text-decoration-none text-black`} key={video.id}> <Link to={`/video/${video.id}`} className={`${styles.linkVideo} text-decoration-none text-black`}>
<div className={`${styles.boxVideo} position-relative`} data-category={video.categories?.map((category) => category.id).join(', ')} > <div className={`${styles.boxVideo} position-relative`} data-category={video.categories?.map((category) => category.id).join(', ')} >
{role === 1 && ( {role === 1 && (
<Link to={`/admin/edit-video/${video.id}`}> <LuPencil className={`${styles.iconEdit} text-decoration-none`} /> </Link> <Link to={`/admin/edit-video/${video.id}`}> <LuPencil className={`${styles.iconEdit} text-decoration-none`} /> </Link>
@@ -342,23 +360,23 @@ export default function Profile() {
{video.watched && <span className="d-block badge text-success text-start fs-6 position-absolute top-0 start-0 bg-success-subtle text-success px-3 py-2" style={{ borderRadius: "0 0 var(--border-radius) 0" }}><PiCheckCircleFill className="mb-1 me-1" /> Visto</span>} {video.watched && <span className="d-block badge text-success text-start fs-6 position-absolute top-0 start-0 bg-success-subtle text-success px-3 py-2" style={{ borderRadius: "0 0 var(--border-radius) 0" }}><PiCheckCircleFill className="mb-1 me-1" /> Visto</span>}
</div> </div>
</Link> </Link>
</div> </motion.div>
)) : videosCount === 0 ? ( )) : videosCount === 0 ? (
<div className="col-12 text-start ps-1"> <div className="col-12 text-start ps-1">
<span className="text-muted fs-5">Nenhum vídeo encontrado</span> <span className="text-muted fs-5">Nenhum vídeo encontrado</span>
</div> </div>
) : null} ) : null}
</div> </div>
</div> </motion.div>
<div className="ms-0 px-lg-4 mt-4"> <div className="ms-0 px-lg-4 mt-4">
<div className="d-flex flex-column flex-sm-row justify-content-between align-items-center px-0" > <motion.div variants={cardInViewVariants} initial="initial" whileInView="animate" viewport={viewportOnce} transition={cardInViewTransition} className="d-flex flex-column flex-sm-row justify-content-between align-items-center px-0" >
<h2 className={`${styles.subtitle} text-center text-sm-start mt-4 mt-sm-3 mb-4`}>{role === 1 ? "Workshops agendados" : "Workshops Inscrito"}</h2> <h2 className={`${styles.subtitle} text-center text-sm-start mt-4 mt-sm-3 mb-4`}>{role === 1 ? "Workshops agendados" : "Workshops Inscrito"}</h2>
<Link to="/workshops" className={`${styles.link} text-decoration-none fw-semibold fs-5 mt-sm-3 mb-sm-4`}>Ver workshops <LuArrowUpRight className="mb-1 me-2" size={25} /></Link> <Link to="/workshops" className={`${styles.link} text-decoration-none fw-semibold fs-5 mt-sm-3 mb-sm-4`}>Ver workshops <LuArrowUpRight className="mb-1 me-2" size={25} /></Link>
</div> </motion.div>
<div className="row mt-4 mt-sm-1 px-2 mb-5"> <div className="row mt-4 mt-sm-1 px-2 mb-5">
{role !== 1 && workshopsCount > 0 ? workshops.map((workshop: Workshop) => ( {role !== 1 && workshopsCount > 0 ? workshops.map((workshop: Workshop) => (
<div className="col-12 col-sm-6 col-lg-4 p-2 position-relative"> <motion.div key={workshop.id} variants={cardInViewVariants} initial="initial" whileInView="animate" viewport={viewportOnce} transition={cardInViewTransition} className="col-12 col-sm-6 col-lg-4 p-2 position-relative">
<div className={`${styles.boxWorkshop} d-flex flex-column text-start pb-3 h-100`}> <div className={`${styles.boxWorkshop} d-flex flex-column text-start pb-3 h-100`}>
<div className={`${styles.thumbnailWorkshop} position-relative`}> <div className={`${styles.thumbnailWorkshop} position-relative`}>
<img <img
@@ -382,13 +400,13 @@ export default function Profile() {
<Link to={`${role === 1 ? `/admin/edit-workshop/${workshop.id}` : `/workshop/${workshop.id}`}`} className={`${styles.linkWorkshop} d-block text-center py-2 px-5 text-decoration-none`}> Detalhes </Link> <Link to={`${role === 1 ? `/admin/edit-workshop/${workshop.id}` : `/workshop/${workshop.id}`}`} className={`${styles.linkWorkshop} d-block text-center py-2 px-5 text-decoration-none`}> Detalhes </Link>
{role !== 1 ? ( {role !== 1 ? (
<button type="button" className={`${styles.btncancelarInscricao} btn text-center py-2 text-decoration-none`} onClick={() => cancelarInscricao(workshop.id)} key={workshop.id}> <button type="button" className={`${styles.btncancelarInscricao} btn text-center py-2 text-decoration-none`} onClick={() => cancelarInscricao(workshop.id)}>
Anular inscrição Anular inscrição
</button> </button>
) : null} ) : null}
</div> </div>
</div> </div>
</div> </motion.div>
) )
) : workshopsCount === 0 ? ( ) : workshopsCount === 0 ? (
<div className="col-12 text-center px-0 mt-3"> <div className="col-12 text-center px-0 mt-3">
@@ -397,7 +415,7 @@ export default function Profile() {
) : null} ) : null}
{role === 1 && workshopsCount > 0 ? nextWorkshops.map((workshop: Workshop) => ( {role === 1 && workshopsCount > 0 ? nextWorkshops.map((workshop: Workshop) => (
<div className="col-12 col-sm-6 col-lg-4 p-2 position-relative"> <motion.div key={workshop.id} variants={cardInViewVariants} initial="initial" whileInView="animate" viewport={viewportOnce} transition={cardInViewTransition} className="col-12 col-sm-6 col-lg-4 p-2 position-relative">
<div className={`${styles.boxWorkshop} d-flex flex-column text-start pb-3 h-100`}> <div className={`${styles.boxWorkshop} d-flex flex-column text-start pb-3 h-100`}>
<div className={`${styles.thumbnailWorkshop} position-relative`}> <div className={`${styles.thumbnailWorkshop} position-relative`}>
<img <img
@@ -421,7 +439,7 @@ export default function Profile() {
<Link to={`${role === 1 ? `/admin/edit-workshop/${workshop.id}` : `/workshop/${workshop.id}`}`} className={`${styles.linkWorkshop} d-block text-center py-2 px-5 text-decoration-none`}> Detalhes </Link> <Link to={`${role === 1 ? `/admin/edit-workshop/${workshop.id}` : `/workshop/${workshop.id}`}`} className={`${styles.linkWorkshop} d-block text-center py-2 px-5 text-decoration-none`}> Detalhes </Link>
</div> </div>
</div> </div>
</div> </motion.div>
) )
) : workshopsCount === 0 ? ( ) : workshopsCount === 0 ? (
<div className="col-12 text-center px-0 mt-3"> <div className="col-12 text-center px-0 mt-3">

View File

@@ -169,6 +169,24 @@
color: var(--text-white); color: var(--text-white);
} }
.iconEdit {
color: var(--text-white);
background-color: var(--bg-primary-color);
font-size: 2.3rem;
font-weight: 500;
transition: all 0.3s ease;
z-index: 1000;
position: absolute;
top: 10px;
right: 10px;
border-radius: var(--border-radius);
padding: 10px;
transition: all 0.3s ease;
&:hover {
background-color: var(--neutral-color);
}
}
.titleVideo { .titleVideo {
color: var(--text-white); color: var(--text-white);
font-size: var(--size-font-text); font-size: var(--size-font-text);
@@ -227,10 +245,6 @@
height: 150px; height: 150px;
} }
.icon{
color: var(--text-primary-color);
}
.linkWorkshop { .linkWorkshop {
display: block; display: block;
width: 160px; width: 160px;

View File

@@ -9,6 +9,9 @@ import { useGetVideosSearch } from "../../../hooks/useGetVideosSearch";
import { useGetWorkshopsSearch } from "../../../hooks/useGetWorkshopsSearch"; import { useGetWorkshopsSearch } from "../../../hooks/useGetWorkshopsSearch";
import { PiCheckCircleFill } from "react-icons/pi"; import { PiCheckCircleFill } from "react-icons/pi";
import { imageSkeletonFadeStyle, onImageSkeletonLoad } from "../../../utils/imageSkeleton"; import { imageSkeletonFadeStyle, onImageSkeletonLoad } from "../../../utils/imageSkeleton";
import { motion } from "framer-motion";
import { pageTitleTransition, pageTitleVariants, slideFromBottomTransition, slideFromBottomVariants, slideFromTopTransition, slideFromTopVariants, viewportOnce } from "../../../utils/pageMotionVariants";
export default function Search() { export default function Search() {
const [videos, setVideos] = useState<Video[]>([]); const [videos, setVideos] = useState<Video[]>([]);
@@ -76,14 +79,14 @@ export default function Search() {
return ( return (
<div className={styles.container}> <div className={styles.container}>
<h1 className={`${styles.subtitle} mb-4`}>Resultados da pesquisa: "{query}"</h1> <motion.h1 variants={pageTitleVariants} initial="initial" animate="animate" transition={pageTitleTransition} className={`${styles.subtitle} mb-4`}>Resultados da pesquisa: "{query}"</motion.h1>
{videos.length > 0 ? ( {videos.length > 0 ? (
<div className="row g-3 p-0"> <div className="row g-3 p-0">
<h2 className={`${styles.title} text-center text-md-start`}>Vídeos</h2> <motion.h2 variants={pageTitleVariants} initial="initial" animate="animate" transition={pageTitleTransition} className={`${styles.title} text-start`}>Vídeos</motion.h2>
<span className="text-muted text-start mt-0">{videos.length === 1 ? `Foi encontrado ${videos.length} vídeo na sua pesquisa.` : `Foram encontrados ${videos.length} vídeos na sua pesquisa.`}</span> <motion.span variants={slideFromTopVariants} initial="initial" animate="animate" transition={slideFromTopTransition} className="text-muted text-start mt-0">{videos.length === 1 ? `Foi encontrado ${videos.length} vídeo na sua pesquisa.` : `Foram encontrados ${videos.length} vídeos na sua pesquisa.`}</motion.span>
{videos.map((video) => ( {videos.map((video) => (
<div className="col-12 col-sm-6 col-lg-4 p-2"> <motion.div variants={slideFromBottomVariants} initial="initial" whileInView="animate" viewport={viewportOnce} transition={slideFromBottomTransition} className="col-12 col-sm-6 col-lg-4 p-2">
<Link to={`/video/${video.id}`} className={`${styles.linkVideo} text-decoration-none text-black`} key={video.id}> <Link to={`/video/${video.id}`} className={`${styles.linkVideo} text-decoration-none text-black`} key={video.id}>
<div className={`${styles.boxVideo} position-relative`} data-category={video.categories?.map((category) => category.id).join(', ')} > <div className={`${styles.boxVideo} position-relative`} data-category={video.categories?.map((category) => category.id).join(', ')} >
{isAdmin && ( {isAdmin && (
@@ -102,17 +105,17 @@ export default function Search() {
{video.watched && <span className="d-block badge text-success text-start fs-6 position-absolute top-0 start-0 bg-success-subtle text-success px-3 py-2" style={{ borderRadius: "0 0 var(--border-radius) 0" }}><PiCheckCircleFill className="mb-1 me-1" /> Visto</span>} {video.watched && <span className="d-block badge text-success text-start fs-6 position-absolute top-0 start-0 bg-success-subtle text-success px-3 py-2" style={{ borderRadius: "0 0 var(--border-radius) 0" }}><PiCheckCircleFill className="mb-1 me-1" /> Visto</span>}
</div> </div>
</Link> </Link>
</div> </motion.div>
))} ))}
</div> </div>
) : null} ) : null}
{workshops.length > 0 ? ( {workshops.length > 0 ? (
<div className="row g-3 p-0 mt-3"> <div className="row g-3 p-0 mt-3">
<h2 className={`${styles.title} text-center text-md-start`}>Workshops</h2> <motion.h2 variants={pageTitleVariants} initial="initial" whileInView="animate" viewport={viewportOnce} transition={pageTitleTransition} className={`${styles.title} text-start`}>Workshops</motion.h2>
<span className="text-muted text-start mt-0">{workshops.length === 1 ? `Foi encontrado ${workshops.length} workshop na sua pesquisa.` : `Foram encontrados ${workshops.length} workshops na sua pesquisa.`}</span> <motion.span variants={slideFromTopVariants} initial="initial" whileInView="animate" viewport={viewportOnce} transition={slideFromTopTransition} className="text-muted text-start mt-0">{workshops.length === 1 ? `Foi encontrado ${workshops.length} workshop na sua pesquisa.` : `Foram encontrados ${workshops.length} workshops na sua pesquisa.`}</motion.span>
{workshops.map((workshop) => ( {workshops.map((workshop) => (
<div className="col-12 col-sm-6 col-lg-4 p-2 position-relative" key={workshop.id}> <motion.div variants={slideFromBottomVariants} initial="initial" whileInView="animate" viewport={viewportOnce} transition={slideFromBottomTransition} className="col-12 col-sm-6 col-lg-4 p-2 position-relative" key={workshop.id}>
<div className={`${styles.boxWorkshop} text-start pb-3`}> <div className={`${styles.boxWorkshop} text-start pb-3`}>
<div className={`${styles.thumbnailWorkshop} position-relative`}> <div className={`${styles.thumbnailWorkshop} position-relative`}>
<img <img
@@ -136,7 +139,7 @@ export default function Search() {
</div> </div>
<Link to={`/workshop/${workshop.id}`} className={`${styles.linkWorkshop} d-block text-center mt-3 py-2 px-5 mx-auto text-decoration-none`}> Detalhes </Link> <Link to={`/workshop/${workshop.id}`} className={`${styles.linkWorkshop} d-block text-center mt-3 py-2 px-5 mx-auto text-decoration-none`}> Detalhes </Link>
</div> </div>
</div> </motion.div>
))} ))}
</div> </div>
) : null} ) : null}

View File

@@ -168,6 +168,8 @@ export default function Video() {
<Link to={`/video/${previousVideo}`} className={`${styles.previousButton} fs-6 text-start`}><b><LuChevronLeft size={25} title="Vídeo anterior" /> Anterior</b></Link> <Link to={`/video/${previousVideo}`} className={`${styles.previousButton} fs-6 text-start`}><b><LuChevronLeft size={25} title="Vídeo anterior" /> Anterior</b></Link>
)} )}
<div></div>
{nextVideo && ( {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> <Link to={`/video/${nextVideo}`} className={`${styles.nextButton} fs-6 text-end`}><b>Próximo <LuChevronRight size={25} title="Próximo vídeo" /> </b></Link>
)} )}

View File

@@ -12,6 +12,24 @@ import { useDebounce } from "../../../hooks/useDebounce";
import { PiCheckCircleFill } from "react-icons/pi"; import { PiCheckCircleFill } from "react-icons/pi";
import { imageSkeletonFadeStyle, onImageSkeletonLoad } from "../../../utils/imageSkeleton"; import { imageSkeletonFadeStyle, onImageSkeletonLoad } from "../../../utils/imageSkeleton";
import { AnimatePresence, motion } from "framer-motion"; import { AnimatePresence, motion } from "framer-motion";
import {
cardInViewTransition,
cardInViewVariants,
dropdownTransition,
dropdownVariants,
fadeTransition,
fadeVariants,
listContentTransition,
listContentVariants,
pageTitleTransition,
pageTitleVariants,
slideFromLeftTransition,
slideFromRightOnMountTransition,
slideFromRightTransition,
slideFromRightVariants,
slideFromLeftVariants,
viewportOnce,
} from "../../../utils/pageMotionVariants";
export default function Videos() { export default function Videos() {
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
@@ -28,6 +46,7 @@ export default function Videos() {
const [loadingVideos, setLoadingVideos] = useState(false); const [loadingVideos, setLoadingVideos] = useState(false);
const videosToShow = videos; const videosToShow = videos;
const [role, setRole] = useState(0); const [role, setRole] = useState(0);
const listContentKey = `${currentPage}-${debouncedSearch}-${selectedCategoryId}`;
useEffect(() => { useEffect(() => {
const fetchVideos = async () => { const fetchVideos = async () => {
@@ -92,7 +111,7 @@ export default function Videos() {
return ( return (
<div className={`${styles.container} p-2 p-sm-4 p-lg-0`}> <div className={`${styles.container} p-2 p-sm-4 p-lg-0`}>
<h1 className={`${styles.title} mt-1`}>Vídeos</h1> <motion.h1 variants={pageTitleVariants} initial="initial" animate="animate" transition={pageTitleTransition} className={`${styles.title} mt-1`}>Vídeos</motion.h1>
{error && ( {error && (
<div className="text-center mt-5 d-flex flex-column gap-2 align-items-center"> <div className="text-center mt-5 d-flex flex-column gap-2 align-items-center">
@@ -101,14 +120,14 @@ export default function Videos() {
)} )}
<div className="row py-3 g-4 justify-content-between"> <div className="row py-3 g-4 justify-content-between">
<div className="col-12 col-sm-7 col-md-8 col-lg-6 d-flex gap-2 text-start px-2"> <motion.div variants={slideFromLeftVariants} initial="initial" whileInView="animate" viewport={viewportOnce} transition={slideFromLeftTransition} className="col-12 col-sm-7 col-md-8 col-lg-6 d-flex gap-2 text-start px-2">
<Form.Control type="text" placeholder="Pesquisar vídeos..." <Form.Control type="text" placeholder="Pesquisar vídeos..."
value={search} value={search}
onChange={(e) => setSearch(e.target.value)} onChange={(e) => setSearch(e.target.value)}
/> />
</div> </motion.div>
<div className="col-12 col-sm-5 col-md-4 col-lg-3 d-flex mt-2 mt-sm-4"> <motion.div variants={slideFromRightVariants} initial="initial" whileInView="animate" viewport={viewportOnce} transition={slideFromRightTransition} className="col-12 col-sm-5 col-md-4 col-lg-3 d-flex mt-2 mt-sm-4">
<div <div
className="btn-group flex-grow-1 position-relative" className="btn-group flex-grow-1 position-relative"
onMouseEnter={() => setShowFilterDropdown(true)} onMouseEnter={() => setShowFilterDropdown(true)}
@@ -126,10 +145,11 @@ export default function Videos() {
<AnimatePresence> <AnimatePresence>
{showFilterDropdown && ( {showFilterDropdown && (
<motion.ul <motion.ul
initial={{ opacity: 0, y: -10, scale: 0.98 }} variants={dropdownVariants}
animate={{ opacity: 1, y: 0, scale: 1 }} initial="initial"
exit={{ opacity: 0, y: -10, scale: 0.98 }} animate="animate"
transition={{ duration: 0.2, ease: "easeOut" }} exit="exit"
transition={dropdownTransition}
className="dropdown-menu text-center w-100" className="dropdown-menu text-center w-100"
style={{ display: "block", zIndex: 4000, top: "100%", marginTop: "0.25rem" }} style={{ display: "block", zIndex: 4000, top: "100%", marginTop: "0.25rem" }}
> >
@@ -204,27 +224,37 @@ export default function Videos() {
)} )}
</AnimatePresence> </AnimatePresence>
</div> </div>
</div> </motion.div>
{role === 1 && ( {role === 1 && (
<div className="col-12 col-sm col-md-12 col-lg-3 text-end align-content-center mt-4 mt-lg-4"> <motion.div variants={slideFromRightVariants} initial="initial" animate="animate" transition={slideFromRightOnMountTransition} className="col-12 col-sm col-md-12 col-lg-3 text-end align-content-center mt-4 mt-lg-4">
<Link to="/admin/create-video" className={`${styles.btnAdicionarVideo} text-decoration-none`}><LuPlus className="mb-1" /> Adicionar vídeo</Link> <Link to="/admin/create-video" className={`${styles.btnAdicionarVideo} text-decoration-none`}><LuPlus className="mb-1" /> Adicionar vídeo</Link>
</div> </motion.div>
)} )}
</div> </div>
<div className={`${styles.containerVideos} mt-3`}> <div className={`${styles.containerVideos} mt-3`}>
<div className="row g-3 p-0"> <AnimatePresence mode="wait">
{ loadingVideos ? ( {loadingVideos ? (
<div className="col-12 text-center mt-5"> <motion.div key="loading" variants={fadeVariants} initial="initial" animate="animate" exit="exit" transition={fadeTransition} className="col-12 text-center mt-5">
<CgSpinner className={`${styles.animateSpin} text-2xl fs-3`} /> <CgSpinner className={`${styles.animateSpin} text-2xl fs-3`} />
</div> </motion.div>
) : videosToShow.length > 0 ? ( ) : (
<motion.div
key={listContentKey}
variants={listContentVariants}
initial="initial"
animate="animate"
exit="exit"
transition={listContentTransition}
className="row g-3 p-0"
>
{videosToShow.length > 0 ? (
<div> <div>
<div className="row g-3 p-0"> <div className="row g-3 p-0">
{videos.map((video) => ( {videos.map((video) => (
<div className="col-12 col-sm-6 col-lg-4 p-2"> <motion.div key={video.id} variants={cardInViewVariants} initial="initial" whileInView="animate" viewport={viewportOnce} transition={cardInViewTransition} className="col-12 col-sm-6 col-lg-4 p-2">
<Link to={`/video/${video.id}`} className={`${styles.linkVideo} text-decoration-none text-black`} key={video.id}> <Link to={`/video/${video.id}`} className={`${styles.linkVideo} text-decoration-none text-black`}>
<div className={`${styles.boxVideo} position-relative`} data-category={video.categories?.map((category) => category.id).join(', ')} > <div className={`${styles.boxVideo} position-relative`} data-category={video.categories?.map((category) => category.id).join(', ')} >
{role === 1 && ( {role === 1 && (
<Link to={`/admin/edit-video/${video.id}`}> <LuPencil className={`${styles.iconEdit} text-decoration-none`} /> </Link> <Link to={`/admin/edit-video/${video.id}`}> <LuPencil className={`${styles.iconEdit} text-decoration-none`} /> </Link>
@@ -243,7 +273,7 @@ export default function Videos() {
{video.watched && <span className="d-block badge text-success text-start fs-6 position-absolute top-0 start-0 bg-success-subtle text-success px-3 py-2" style={{ borderRadius: "0 0 var(--border-radius) 0" }}><PiCheckCircleFill className="mb-1 me-1" /> Visto</span>} {video.watched && <span className="d-block badge text-success text-start fs-6 position-absolute top-0 start-0 bg-success-subtle text-success px-3 py-2" style={{ borderRadius: "0 0 var(--border-radius) 0" }}><PiCheckCircleFill className="mb-1 me-1" /> Visto</span>}
</div> </div>
</Link> </Link>
</div> </motion.div>
))} ))}
</div> </div>
<div> <div>
@@ -287,11 +317,9 @@ export default function Videos() {
)} )}
</> </>
)} )}
</motion.div>
)}
</AnimatePresence>
</div>
</div> </div>
</div> </div>
) )

View File

@@ -11,6 +11,24 @@ import Swal from "sweetalert2";
import { Form, Pagination } from "react-bootstrap"; import { Form, Pagination } from "react-bootstrap";
import { imageSkeletonFadeStyle, onImageSkeletonLoad } from "../../../utils/imageSkeleton"; import { imageSkeletonFadeStyle, onImageSkeletonLoad } from "../../../utils/imageSkeleton";
import { AnimatePresence, motion } from "framer-motion"; import { AnimatePresence, motion } from "framer-motion";
import {
cardInViewTransition,
cardInViewVariants,
dropdownTransition,
dropdownVariants,
fadeTransition,
fadeVariants,
listContentTransition,
listContentVariants,
pageTitleTransition,
pageTitleVariants,
slideFromLeftTransition,
slideFromLeftVariants,
slideFromRightOnMountTransition,
slideFromRightTransition,
slideFromRightVariants,
viewportOnce,
} from "../../../utils/pageMotionVariants";
export default function Workshops() { export default function Workshops() {
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
@@ -25,6 +43,7 @@ export default function Workshops() {
const [loadingWorkshops, setLoadingWorkshops] = useState(false); const [loadingWorkshops, setLoadingWorkshops] = useState(false);
const [role, setRole] = useState(0); const [role, setRole] = useState(0);
const [userId, setUserId] = useState(0); const [userId, setUserId] = useState(0);
const listContentKey = `${currentPage}-${debouncedSearch}-${selectedWorkshopStatus}`;
/* const [searchLoading, setSearchLoading] = useState(false); */ /* const [searchLoading, setSearchLoading] = useState(false); */
useEffect(() => { useEffect(() => {
@@ -152,17 +171,17 @@ export default function Workshops() {
} }
return ( return (
<div className={`${styles.container} p-4 p-lg-0`}> <div className={`${styles.container} p-2 p-sm-4 p-lg-0`}>
<h1 className={`${styles.title} mt-1 mb-0 mb-sm-3`}>Workshops</h1> <motion.h1 variants={pageTitleVariants} initial="initial" animate="animate" transition={pageTitleTransition} className={`${styles.title} mt-1 mb-0 mb-sm-3`}>Workshops</motion.h1>
<div className="row py-3 g-4 justify-content-between"> <div className="row py-3 g-4 justify-content-between">
<div className="col-12 col-sm-7 col-md-8 col-lg-6 d-flex gap-2 text-start px-2"> <motion.div variants={slideFromLeftVariants} initial="initial" whileInView="animate" viewport={viewportOnce} transition={slideFromLeftTransition} className="col-12 col-sm-7 col-md-8 col-lg-6 d-flex gap-2 text-start px-2">
<Form.Control type="text" placeholder="Pesquisar workshops..." <Form.Control type="text" placeholder="Pesquisar workshops..."
value={search} value={search}
onChange={(e) => setSearch(e.target.value)} onChange={(e) => setSearch(e.target.value)}
/> />
</div> </motion.div>
<div className="col-12 col-sm-5 col-md-4 col-lg-3 d-flex mt-2 mt-sm-4"> <motion.div variants={slideFromRightVariants} initial="initial" whileInView="animate" viewport={viewportOnce} transition={slideFromRightTransition} className="col-12 col-sm-5 col-md-4 col-lg-3 d-flex mt-2 mt-sm-4">
{/* <label htmlFor="filter" className="form-label fw-bold text-start">Filtrar workshops</label> */} {/* <label htmlFor="filter" className="form-label fw-bold text-start">Filtrar workshops</label> */}
<div <div
className="btn-group flex-grow-1 position-relative" className="btn-group flex-grow-1 position-relative"
@@ -176,10 +195,11 @@ export default function Workshops() {
<AnimatePresence> <AnimatePresence>
{showFilterDropdown && ( {showFilterDropdown && (
<motion.ul <motion.ul
initial={{ opacity: 0, y: -10, scale: 0.98 }} variants={dropdownVariants}
animate={{ opacity: 1, y: 0, scale: 1 }} initial="initial"
exit={{ opacity: 0, y: -10, scale: 0.98 }} animate="animate"
transition={{ duration: 0.2, ease: "easeOut" }} exit="exit"
transition={dropdownTransition}
className="dropdown-menu text-center w-100" className="dropdown-menu text-center w-100"
style={{ display: "block", zIndex: 4000, top: "100%", marginTop: "0.25rem" }} style={{ display: "block", zIndex: 4000, top: "100%", marginTop: "0.25rem" }}
> >
@@ -228,23 +248,34 @@ export default function Workshops() {
)} )}
</AnimatePresence> </AnimatePresence>
</div> </div>
</div> </motion.div>
{role === 1 ? ( {role === 1 ? (
<div className="col-12 col-sm col-md-12 col-lg-3 text-end align-content-center mt-4 mt-lg-4" style={{ minHeight: '40px' }}> <motion.div variants={slideFromRightVariants} initial="initial" animate="animate" transition={slideFromRightOnMountTransition} className="col-12 col-sm col-md-12 col-lg-3 text-end align-content-center mt-4 mt-lg-4" style={{ minHeight: '40px' }}>
<Link to="/admin/create-workshop" className={`${styles.btnAdicionarWorkshop} text-decoration-none`}><LuPlus className="mb-1" />Adicionar workshop</Link> <Link to="/admin/create-workshop" className={`${styles.btnAdicionarWorkshop} text-decoration-none`}><LuPlus className="mb-1" />Adicionar workshop</Link>
</div> </motion.div>
) : null} ) : null}
</div> </div>
<div className="row py-3 g-4"> <div className="row py-3 g-4">
{loadingWorkshops ? ( <AnimatePresence mode="wait">
<div className="col-12 text-center mt-5"> {loadingWorkshops ? (
<CgSpinner className={`${styles.animateSpin} text-2xl fs-3`} /> <motion.div key="loading" variants={fadeVariants} initial="initial" animate="animate" exit="exit" transition={fadeTransition} className="col-12 text-center mt-5">
</div> <CgSpinner className={`${styles.animateSpin} text-2xl fs-3`} />
) : workshops.length > 0 ? ( </motion.div>
) : (
<motion.div
key={listContentKey}
variants={listContentVariants}
initial="initial"
animate="animate"
exit="exit"
transition={listContentTransition}
className="row g-4 p-0 w-100 mx-0 mt-0"
>
{workshops.length > 0 ? (
<> <>
{workshops.map((workshop) => ( {workshops.map((workshop) => (
<div className="col-12 col-sm-6 col-lg-4 p-2 position-relative"> <motion.div key={workshop.id} variants={cardInViewVariants} initial="initial" whileInView="animate" viewport={viewportOnce} transition={cardInViewTransition} className="col-12 col-sm-6 col-lg-4 p-2 position-relative">
<div className={`${styles.boxWorkshop} d-flex flex-column text-start pb-3 h-100`}> <div className={`${styles.boxWorkshop} d-flex flex-column text-start pb-3 h-100`}>
<div className={`${styles.thumbnailWorkshop} position-relative`}> <div className={`${styles.thumbnailWorkshop} position-relative`}>
<img <img
@@ -306,7 +337,7 @@ export default function Workshops() {
) : null} ) : null}
</div> </div>
</div> </div>
</div> </motion.div>
))} ))}
<div className="d-flex justify-content-center align-items-center gap-3 py-3"> <div className="d-flex justify-content-center align-items-center gap-3 py-3">
<Pagination> <Pagination>
@@ -348,6 +379,9 @@ export default function Workshops() {
<span className="text-muted fs-5">Nenhum workshop encontrado com o filtro selecionado</span> <span className="text-muted fs-5">Nenhum workshop encontrado com o filtro selecionado</span>
</div> </div>
) : null} ) : null}
</motion.div>
)}
</AnimatePresence>
</div> </div>
</div> </div>
) )

View File

@@ -0,0 +1,70 @@
import type { Transition, Variants } from "framer-motion";
export const viewportOnce = { once: true };
export const pageTitleVariants: Variants = {
initial: { opacity: 0, y: -50 },
animate: { opacity: 1, y: 0 },
};
export const slideFromLeftVariants: Variants = {
initial: { opacity: 0, x: -50 },
animate: { opacity: 1, x: 0 },
};
export const slideFromRightVariants: Variants = {
initial: { opacity: 0, x: 50 },
animate: { opacity: 1, x: 0 },
};
export const slideFromBottomVariants: Variants = {
initial: { opacity: 0, y: 70 },
animate: { opacity: 1, y: 0 },
};
export const slideFromTopVariants: Variants = {
initial: { opacity: 0, y: -50 },
animate: { opacity: 1, y: 0 },
};
export const fadeVariants: Variants = {
initial: { opacity: 0 },
animate: { opacity: 1 },
exit: { opacity: 0 },
};
export const cardInViewVariants: Variants = {
initial: { opacity: 0, y: 50 },
animate: { opacity: 1, y: 0 },
};
export const dropdownVariants: Variants = {
initial: { opacity: 0, y: -10, scale: 0.98 },
animate: { opacity: 1, y: 0, scale: 1 },
exit: { opacity: 0, y: -10, scale: 0.98 },
};
export const listContentVariants: Variants = {
initial: { opacity: 0, y: 70 },
animate: { opacity: 1, y: 0 },
exit: { opacity: 0, y: 30 },
};
export const pageTitleTransition: Transition = { duration: 0.5, delay: 0.1 };
export const slideFromLeftTransition: Transition = { duration: 0.5, delay: 0.2 };
export const slideFromLeftTransitionNoDelay: Transition = { duration: 0.5 };
export const slideFromLeftOnMountTransition: Transition = { duration: 0.5, delay: 0.1 };
export const slideFromRightOnMountTransition02: Transition = { duration: 0.5, delay: 0.2 };
export const slideFromLeftInViewTransition03: Transition = { duration: 0.5, delay: 0.3 };
export const slideFromRightTransition: Transition = { duration: 0.5, delay: 0.3 };
export const slideFromRightInViewTransition04: Transition = { duration: 0.5, delay: 0.4 };
export const slideFromBottomTransition: Transition = { duration: 0.5, delay: 0.4 };
export const slideFromBottomInViewTransition: Transition = { duration: 0.5, delay: 0.1 };
export const slideFromRightOnMountTransition: Transition = { duration: 0.5, delay: 0.5 };
export const slideFromTopTransition: Transition = { duration: 0.5 };
export const slideFromTopTransitionDelayed: Transition = { duration: 0.5, delay: 0.1 };
export const cardInViewTransition: Transition = { duration: 0.5, delay: 0.1 };
export const dropdownTransition: Transition = { duration: 0.2, ease: "easeOut" };
export const listContentTransition: Transition = { duration: 0.4, ease: "easeOut" };
export const fadeTransition: Transition = { duration: 0.15 };