feat: request optimizations
This commit is contained in:
@@ -3,17 +3,17 @@ import { LuUser, LuMenu, LuSearch, LuLogOut, LuUsers, LuMail, LuGraduationCap, L
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useNavigate, Link } from "react-router";
|
import { useNavigate, Link } from "react-router";
|
||||||
import { Button, NavLink, Offcanvas } from "react-bootstrap";
|
import { Button, NavLink, Offcanvas } from "react-bootstrap";
|
||||||
import type { User, Workshop } from "../../types";
|
import type { Workshop } from "../../types";
|
||||||
import type { Video } from "../../types";
|
import type { Video } from "../../types";
|
||||||
import { CgSpinner } from "react-icons/cg";
|
import { CgSpinner } from "react-icons/cg";
|
||||||
import { useDebounce } from "../../hooks/useDebounce";
|
import { useDebounce } from "../../hooks/useDebounce";
|
||||||
import { useGetVideos } from "../../hooks/useGetVideos";
|
import { useGetVideos } from "../../hooks/useGetVideos";
|
||||||
import { useGetVideosLength } from "../../hooks/useGetVideosLength";
|
|
||||||
import { useGetVideosSearch } from "../../hooks/useGetVideosSearch";
|
import { useGetVideosSearch } from "../../hooks/useGetVideosSearch";
|
||||||
import { PiCheckCircleFill } from "react-icons/pi";
|
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 { useGetCurrentUser } from "../../hooks/useGetCurrentUser";
|
||||||
|
|
||||||
export default function Header() {
|
export default function Header() {
|
||||||
const [showMenu, setShowMenu] = useState(false);
|
const [showMenu, setShowMenu] = useState(false);
|
||||||
@@ -23,31 +23,28 @@ export default function Header() {
|
|||||||
const [searchCompleted, setSearchCompleted] = useState(false);
|
const [searchCompleted, setSearchCompleted] = useState(false);
|
||||||
const [videosSearched, setVideosSearched] = useState<Video[]>([]);
|
const [videosSearched, setVideosSearched] = useState<Video[]>([]);
|
||||||
const [workshopsSearched, setWorkshopsSearched] = useState<Workshop[]>([]);
|
const [workshopsSearched, setWorkshopsSearched] = useState<Workshop[]>([]);
|
||||||
const user = JSON.parse(localStorage.getItem("user") || "{}") as User;
|
|
||||||
const isAdmin = user.role_id === 1;
|
|
||||||
const debouncedSearch = useDebounce(search, 500);
|
const debouncedSearch = useDebounce(search, 500);
|
||||||
const [showDropdown, setShowDropdown] = useState(false);
|
const [showDropdown, setShowDropdown] = useState(false);
|
||||||
const { getVideos } = useGetVideos();
|
const { getVideos } = useGetVideos();
|
||||||
const { getVideosLength } = useGetVideosLength();
|
|
||||||
const { getVideosSearch } = useGetVideosSearch();
|
const { getVideosSearch } = useGetVideosSearch();
|
||||||
const { getWorkshopsSearch } = useGetWorkshopsSearch();
|
const { getWorkshopsSearch } = useGetWorkshopsSearch();
|
||||||
const [videosStats, setVideosStats] = useState({
|
const { getCurrentUser } = useGetCurrentUser();
|
||||||
videos: 0,
|
const [role, setRole] = useState(0);
|
||||||
videosWatched: 0
|
const [videosWatched, setVideosWatched] = useState(0);
|
||||||
});
|
const [videosCount, setVideosCount] = useState(0);
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isAdmin) return;
|
const fetchAll = async () => {
|
||||||
|
const videosWatched = localStorage.getItem("videosWatched");
|
||||||
const fetchProgressVideos = async () => {
|
setVideosWatched(videosWatched ? parseInt(videosWatched) : 0);
|
||||||
const videosLengthData = await getVideosLength();
|
const videosCount = localStorage.getItem("videosCount");
|
||||||
if ("videos" in videosLengthData) {
|
setVideosCount(videosCount ? parseInt(videosCount) : 0);
|
||||||
setVideosStats(videosLengthData as { videos: number, videosWatched: number });
|
const userData = await getCurrentUser();
|
||||||
}
|
setRole(userData.data.role_id);
|
||||||
};
|
};
|
||||||
fetchProgressVideos();
|
fetchAll();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleCloseMenu = () => setShowMenu(false);
|
const handleCloseMenu = () => setShowMenu(false);
|
||||||
@@ -132,7 +129,13 @@ export default function Header() {
|
|||||||
<Offcanvas.Header >
|
<Offcanvas.Header >
|
||||||
<Offcanvas.Title className={"w-100 d-flex justify-content-between"}>
|
<Offcanvas.Title className={"w-100 d-flex justify-content-between"}>
|
||||||
<Link className={`${styles.logoLink} justify-content-start`} to="/dashboard">
|
<Link className={`${styles.logoLink} justify-content-start`} to="/dashboard">
|
||||||
<img className={styles.logo} src="/src/assets/logo.png" alt="Logo" />
|
<img
|
||||||
|
className={styles.logo}
|
||||||
|
src="/src/assets/logo.png"
|
||||||
|
alt="Logo"
|
||||||
|
style={imageSkeletonFadeStyle}
|
||||||
|
onLoad={onImageSkeletonLoad}
|
||||||
|
/>
|
||||||
</Link>
|
</Link>
|
||||||
<button type="button" className={`${styles.sidebarClose} align-items-center justify-content-center d-xl-none`} onClick={handleCloseMenu}> <LuPanelLeftClose size={24} /> </button>
|
<button type="button" className={`${styles.sidebarClose} align-items-center justify-content-center d-xl-none`} onClick={handleCloseMenu}> <LuPanelLeftClose size={24} /> </button>
|
||||||
</Offcanvas.Title>
|
</Offcanvas.Title>
|
||||||
@@ -150,7 +153,7 @@ export default function Header() {
|
|||||||
<li className={`${styles.navItem} text-start`}>
|
<li className={`${styles.navItem} text-start`}>
|
||||||
<NavLink className={styles.navLink} href="/workshops"> <LuGraduationCap className="me-2" size={24} />Workshops</NavLink>
|
<NavLink className={styles.navLink} href="/workshops"> <LuGraduationCap className="me-2" size={24} />Workshops</NavLink>
|
||||||
</li>
|
</li>
|
||||||
{isAdmin && (
|
{role === 1 && (
|
||||||
<li className={`${styles.navItem} text-start`}>
|
<li className={`${styles.navItem} text-start`}>
|
||||||
<NavLink className={styles.navLink} href="/admin/users"> <LuUsers className="me-2" size={24} />Utilizadores</NavLink>
|
<NavLink className={styles.navLink} href="/admin/users"> <LuUsers className="me-2" size={24} />Utilizadores</NavLink>
|
||||||
</li>
|
</li>
|
||||||
@@ -161,10 +164,10 @@ export default function Header() {
|
|||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
{!isAdmin && videosStats.videos > 0 && (
|
{role !== 1 && videosCount > 0 && (
|
||||||
<div className="d-flex d-sm-none justify-content-center">
|
<div className="d-flex d-sm-none justify-content-center">
|
||||||
<span className="fw-semibold">Vídeos assistidos</span>
|
<span className="fw-semibold">Vídeos assistidos</span>
|
||||||
<span className={`${styles.badge} badge align-content-center ms-3`}>{videosStats.videosWatched}/{videosStats.videos}</span>
|
<span className={`${styles.badge} badge align-content-center ms-3`}>{videosWatched}/{videosCount}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -177,11 +180,11 @@ export default function Header() {
|
|||||||
|
|
||||||
<nav className="navbar px-3">
|
<nav className="navbar px-3">
|
||||||
<div className={styles.headerRight}>
|
<div className={styles.headerRight}>
|
||||||
{!isAdmin && videosStats.videos > 0 && (
|
{role !== 1 && videosCount > 0 && (
|
||||||
<>
|
<>
|
||||||
<div className="d-none d-sm-flex align-items-center gap-1 text-muted">
|
<div className="d-none d-sm-flex align-items-center gap-1 text-muted">
|
||||||
<span className="fw-semibold">Vídeos assistidos</span>
|
<span className="fw-semibold">Vídeos assistidos</span>
|
||||||
<span className={`${styles.badge} badge align-content-center`}>{videosStats.videosWatched}/{videosStats.videos}</span>
|
<span className={`${styles.badge} badge align-content-center`}>{videosWatched}/{videosCount}</span>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -61,7 +61,12 @@
|
|||||||
|
|
||||||
.logo {
|
.logo {
|
||||||
width: 180px;
|
width: 180px;
|
||||||
|
aspect-ratio: 3 / 1;
|
||||||
height: auto;
|
height: auto;
|
||||||
|
object-fit: contain;
|
||||||
|
background: linear-gradient(90deg, #e0e0e0 25%, #f0f0f0 50%, #e0e0e0 75%);
|
||||||
|
background-size: 200% 100%;
|
||||||
|
animation: skeleton 1.5s infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav {
|
.nav {
|
||||||
|
|||||||
@@ -1,17 +1,27 @@
|
|||||||
import styles from "./styles.module.css";
|
import styles from "./styles.module.css";
|
||||||
import { Link, NavLink } from "react-router";
|
import { Link, NavLink } from "react-router";
|
||||||
import { useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { LuPanelLeftClose, LuPanelLeftOpen, LuTvMinimalPlay } from "react-icons/lu";
|
import { LuPanelLeftClose, LuPanelLeftOpen, LuTvMinimalPlay } from "react-icons/lu";
|
||||||
import { LuGraduationCap } from "react-icons/lu";
|
import { LuGraduationCap } from "react-icons/lu";
|
||||||
import { LuUsers } from "react-icons/lu";
|
import { LuUsers } from "react-icons/lu";
|
||||||
import { LuMail } from "react-icons/lu";
|
import { LuMail } from "react-icons/lu";
|
||||||
import { LuLayoutDashboard } from "react-icons/lu";
|
import { LuLayoutDashboard } from "react-icons/lu";
|
||||||
import { LuLogOut } from "react-icons/lu";
|
import { LuLogOut } from "react-icons/lu";
|
||||||
|
import { useGetCurrentUser } from "../../hooks/useGetCurrentUser";
|
||||||
|
|
||||||
export default function Sidebar() {
|
export default function Sidebar() {
|
||||||
|
|
||||||
|
const { getCurrentUser } = useGetCurrentUser();
|
||||||
|
const [role, setRole] = useState(0);
|
||||||
const [sideMenu, setSideMenu] = useState(true);
|
const [sideMenu, setSideMenu] = useState(true);
|
||||||
|
|
||||||
const user = JSON.parse(localStorage.getItem("user") as unknown as string);
|
useEffect(() => {
|
||||||
|
const fetchCurrentUser = async () => {
|
||||||
|
const currentUser = await getCurrentUser();
|
||||||
|
setRole(currentUser.data.role_id);
|
||||||
|
};
|
||||||
|
fetchCurrentUser();
|
||||||
|
}, []);
|
||||||
|
|
||||||
return sideMenu ? (
|
return sideMenu ? (
|
||||||
<aside className={styles.sidebar}>
|
<aside className={styles.sidebar}>
|
||||||
@@ -24,7 +34,7 @@ export default function Sidebar() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<nav className={`${styles.nav}`}>
|
<nav className={`${styles.nav}`}>
|
||||||
{user.role_id === 1 ? (
|
{role === 1 ? (
|
||||||
<ul className={styles.navList}>
|
<ul className={styles.navList}>
|
||||||
<li className={`${styles.navItem} text-start`}>
|
<li className={`${styles.navItem} text-start`}>
|
||||||
<NavLink className={({ isActive }) => isActive ? `${styles.navLink} ${styles.navLinkActive}` : styles.navLink} to="/dashboard"> <LuLayoutDashboard className="me-2" size={24} /> {sideMenu ? "Dashboard" : ""} </NavLink>
|
<NavLink className={({ isActive }) => isActive ? `${styles.navLink} ${styles.navLinkActive}` : styles.navLink} to="/dashboard"> <LuLayoutDashboard className="me-2" size={24} /> {sideMenu ? "Dashboard" : ""} </NavLink>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { ApiErrorResponse, Video } from "../types";
|
import type { ApiErrorResponse, Category, Video } from "../types";
|
||||||
|
|
||||||
type GetVideosParams = {
|
type GetVideosParams = {
|
||||||
page?: number;
|
page?: number;
|
||||||
@@ -33,7 +33,9 @@ export function useGetVideos() {
|
|||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
return {
|
return {
|
||||||
videos: data.data as Video[],
|
videos: data.data as Video[],
|
||||||
meta: data.meta
|
meta: data.meta,
|
||||||
|
role: data.role,
|
||||||
|
categories: data.categories as Category[],
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
return data as ApiErrorResponse;
|
return data as ApiErrorResponse;
|
||||||
|
|||||||
@@ -30,7 +30,9 @@ export function useGetWorkshops() {
|
|||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
return {
|
return {
|
||||||
workshops: data.data as Workshop[],
|
workshops: data.data as Workshop[],
|
||||||
meta: data.meta
|
meta: data.meta,
|
||||||
|
userId: data.userId,
|
||||||
|
role: data.role,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
return (data as ApiErrorResponse);
|
return (data as ApiErrorResponse);
|
||||||
|
|||||||
@@ -1,67 +1,43 @@
|
|||||||
import { Outlet, useNavigate } from "react-router";
|
import { Outlet, useNavigate } from "react-router";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import type { ApiErrorResponse, User } from "../../../types";
|
import { useGetCurrentUser } from "../../../hooks/useGetCurrentUser";
|
||||||
|
import { CgSpinner } from "react-icons/cg";
|
||||||
|
import styles from "./styles.module.css";
|
||||||
|
|
||||||
export default function AdminLayout() {
|
export default function AdminLayout() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const userRaw = localStorage.getItem("user");
|
const [loading, setLoading] = useState(true);
|
||||||
const [user, setUser] = useState<User | null>(userRaw ? JSON.parse(userRaw) : null);
|
|
||||||
|
|
||||||
/* Verificação do role_id no frontend das páginas do admin */
|
/* Verificação do role_id no frontend das páginas do admin */
|
||||||
const [checkingRole, setCheckingRole] = useState<boolean>(true);
|
const { getCurrentUser } = useGetCurrentUser();
|
||||||
const [error, setError] = useState<ApiErrorResponse | null>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getVerifyRole();
|
const fetchCurrentUser = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const currentUser = await getCurrentUser();
|
||||||
|
|
||||||
|
if (currentUser.data.role_id !== 1) {
|
||||||
|
navigate("/dashboard", { replace: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(false);
|
||||||
|
} catch (error) {
|
||||||
|
navigate("/dashboard", { replace: true });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
fetchCurrentUser();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
async function getVerifyRole() {
|
if (loading) {
|
||||||
try {
|
return <div className="text-center mt-5 d-flex flex-column gap-2 align-items-center">
|
||||||
const response = await fetch("http://127.0.0.1:8000/api/me", {
|
<CgSpinner className={`${styles.animateSpin} text-2xl fs-3`} />
|
||||||
method: "GET",
|
</div>;
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${localStorage.getItem("token")}`,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
setCheckingRole(false);
|
|
||||||
navigate("/dashboard");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
console.log("role_id:", data?.data?.role_id);
|
|
||||||
|
|
||||||
if (data?.data?.role_id !== 1) {
|
|
||||||
setError({
|
|
||||||
message: "Acesso negado. Apenas administradores podem aceder a esta página",
|
|
||||||
data: null,
|
|
||||||
errors: {},
|
|
||||||
});
|
|
||||||
navigate("/dashboard");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
setCheckingRole(false);
|
|
||||||
navigate("/dashboard");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
return <Outlet />;
|
||||||
if (!user || user.role_id !== 1) {
|
|
||||||
navigate("/dashboard", { replace: true });
|
|
||||||
}
|
|
||||||
}, [user, navigate]);
|
|
||||||
|
|
||||||
if (!user || user.role_id !== 1) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Outlet />
|
|
||||||
);
|
|
||||||
|
|
||||||
/* Criado o layout para o admin, o proximo passo é aninhar este layout em routes.tsx no frontend */
|
/* Criado o layout para o admin, o proximo passo é aninhar este layout em routes.tsx no frontend */
|
||||||
}
|
}
|
||||||
@@ -12,13 +12,8 @@ export default function CreateUser() {
|
|||||||
const [password, setPassword] = useState<string>("");
|
const [password, setPassword] = useState<string>("");
|
||||||
const [password_confirmation, setPasswordConfirmation] = useState<string>("");
|
const [password_confirmation, setPasswordConfirmation] = useState<string>("");
|
||||||
const [role_id, setRoleId] = useState("2");
|
const [role_id, setRoleId] = useState("2");
|
||||||
const [checkingRole, setCheckingRole] = useState<boolean>(true);
|
|
||||||
const [creating, setCreating] = useState(false);
|
const [creating, setCreating] = useState(false);
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
setCheckingRole(false);
|
|
||||||
}, 2000);
|
|
||||||
|
|
||||||
async function create() {
|
async function create() {
|
||||||
//validação dos campos
|
//validação dos campos
|
||||||
|
|
||||||
@@ -115,14 +110,6 @@ export default function CreateUser() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (checkingRole) {
|
|
||||||
return (
|
|
||||||
<div className="text-center">
|
|
||||||
<CgSpinner className={`${styles.animateSpin} text-2xl fs-3`} />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`${styles.container} p-3 p-xl-0`}>
|
<div className={`${styles.container} p-3 p-xl-0`}>
|
||||||
<div className={"text-start"}>
|
<div className={"text-start"}>
|
||||||
|
|||||||
@@ -16,15 +16,10 @@ export default function CreateVideo() {
|
|||||||
const [isUploading, setIsUploading] = useState(false);
|
const [isUploading, setIsUploading] = useState(false);
|
||||||
const [uploadProgress, setUploadProgress] = useState(0);
|
const [uploadProgress, setUploadProgress] = useState(0);
|
||||||
const [category_ids, setCategoryIds] = useState<string[]>([]);
|
const [category_ids, setCategoryIds] = useState<string[]>([]);
|
||||||
const [checkingRole, setCheckingRole] = useState(true);
|
|
||||||
const [creating, setCreating] = useState(false);
|
const [creating, setCreating] = useState(false);
|
||||||
const [categories, setCategories] = useState<Category[]>([]);
|
const [categories, setCategories] = useState<Category[]>([]);
|
||||||
const [order, setOrder] = useState<number>(0);
|
const [order, setOrder] = useState<number>(0);
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
setCheckingRole(false);
|
|
||||||
}, 2000);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getCategories();
|
getCategories();
|
||||||
}, []);
|
}, []);
|
||||||
@@ -210,14 +205,6 @@ export default function CreateVideo() {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (checkingRole) {
|
|
||||||
return (
|
|
||||||
<div className="text-center">
|
|
||||||
<CgSpinner className={`${styles.animateSpin} text-2xl fs-3`} />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
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"}>
|
<div className={"text-start"}>
|
||||||
|
|||||||
@@ -17,13 +17,8 @@ export default function CreateWorkshop() {
|
|||||||
const [time_end, setTimeEnd] = useState<Date | null>(null);
|
const [time_end, setTimeEnd] = useState<Date | null>(null);
|
||||||
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [checkingRole, setCheckingRole] = useState(true);
|
|
||||||
const [creating, setCreating] = useState(false);
|
const [creating, setCreating] = useState(false);
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
setCheckingRole(false);
|
|
||||||
}, 3000);
|
|
||||||
|
|
||||||
//função para formatar a data para o formato Local
|
//função para formatar a data para o formato Local
|
||||||
function formatDateLocalISO(d: Date) {
|
function formatDateLocalISO(d: Date) {
|
||||||
const yyyy = d.getFullYear();
|
const yyyy = d.getFullYear();
|
||||||
@@ -94,14 +89,6 @@ export default function CreateWorkshop() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (checkingRole) {
|
|
||||||
return (
|
|
||||||
<div className="text-center mt-5">
|
|
||||||
<CgSpinner className={`${styles.animateSpin} text-2xl fs-3`} />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
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"}>
|
<div className={"text-start"}>
|
||||||
|
|||||||
@@ -117,6 +117,8 @@ export default function editVideo() {
|
|||||||
category_ids.forEach((categoryId) => {
|
category_ids.forEach((categoryId) => {
|
||||||
formData.append("category_ids[]", categoryId);
|
formData.append("category_ids[]", categoryId);
|
||||||
});
|
});
|
||||||
|
// Mesmo que fique vazio, força o backend a sincronizar (sync([]) remove pivots antigas)
|
||||||
|
formData.append("sync_categories", "1");
|
||||||
|
|
||||||
// Mantém compatibilidade de upload de ficheiro com Laravel
|
// Mantém compatibilidade de upload de ficheiro com Laravel
|
||||||
// quando a rota aceita PATCH (method spoofing)
|
// quando a rota aceita PATCH (method spoofing)
|
||||||
@@ -241,7 +243,6 @@ export default function editVideo() {
|
|||||||
{loading ? (
|
{loading ? (
|
||||||
<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">
|
||||||
<CgSpinner className={`${styles.animateSpin} text-2xl fs-3`} />
|
<CgSpinner className={`${styles.animateSpin} text-2xl fs-3`} />
|
||||||
<span>A carregar dados do vídeo...</span>
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className={`${styles.container} p-3 p-xl-0`}>
|
<div className={`${styles.container} p-3 p-xl-0`}>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Link, useParams } from "react-router";
|
import { Link, useParams } from "react-router";
|
||||||
import { CgSpinner } from "react-icons/cg";
|
import { CgSpinner } from "react-icons/cg";
|
||||||
import type { ApiErrorResponse, User, Workshop } from "../../../../types";
|
import type { ApiErrorResponse, Workshop } from "../../../../types";
|
||||||
import { LuArrowLeft, LuCalendar, LuCheck, LuClock3, LuPencil, LuTrash2, LuUpload, LuUsers, LuX} from "react-icons/lu";
|
import { LuArrowLeft, LuCalendar, LuCheck, LuClock3, LuPencil, LuTrash2, LuUpload, LuUsers, LuX} from "react-icons/lu";
|
||||||
import { pt } from "date-fns/locale";
|
import { pt } from "date-fns/locale";
|
||||||
import DatePicker from "react-datepicker";
|
import DatePicker from "react-datepicker";
|
||||||
@@ -11,10 +11,6 @@ import styles from "./styles.module.css";
|
|||||||
import { imageSkeletonFadeStyle, onImageSkeletonLoad } from "../../../../utils/imageSkeleton";
|
import { imageSkeletonFadeStyle, onImageSkeletonLoad } from "../../../../utils/imageSkeleton";
|
||||||
|
|
||||||
export default function Workshop() {
|
export default function Workshop() {
|
||||||
const user = JSON.parse(localStorage.getItem("user") as unknown as string) as User;
|
|
||||||
const isAdmin = user.role_id === 1;
|
|
||||||
|
|
||||||
|
|
||||||
const { id } = useParams();
|
const { id } = useParams();
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [workshop, setWorkshop] = useState<Workshop | null>(null);
|
const [workshop, setWorkshop] = useState<Workshop | null>(null);
|
||||||
@@ -28,6 +24,7 @@ export default function Workshop() {
|
|||||||
const [time_end, setTimeEnd] = useState<Date | null>(null);
|
const [time_end, setTimeEnd] = useState<Date | null>(null);
|
||||||
const [status, setStatus] = useState<string>("");
|
const [status, setStatus] = useState<string>("");
|
||||||
const [listagemInscritos, setListagemInscritos] = useState<boolean>(false);
|
const [listagemInscritos, setListagemInscritos] = useState<boolean>(false);
|
||||||
|
const [role, setRole] = useState<number>(0);
|
||||||
|
|
||||||
/* Para enviar a data no formato que o backend espera */
|
/* Para enviar a data no formato que o backend espera */
|
||||||
function formatDateToYmd(value: Date): string {
|
function formatDateToYmd(value: Date): string {
|
||||||
@@ -64,6 +61,7 @@ export default function Workshop() {
|
|||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
setWorkshop(data.data);
|
setWorkshop(data.data);
|
||||||
setStatus(data.data.status);
|
setStatus(data.data.status);
|
||||||
|
setRole(data.role);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
} else {
|
} else {
|
||||||
setWorkshop(null);
|
setWorkshop(null);
|
||||||
@@ -187,7 +185,6 @@ export default function Workshop() {
|
|||||||
return (
|
return (
|
||||||
<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">
|
||||||
<CgSpinner className={`${styles.animateSpin} text-2xl fs-3`} />
|
<CgSpinner className={`${styles.animateSpin} text-2xl fs-3`} />
|
||||||
<span>A carregar dados do workshop...</span>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -217,7 +214,7 @@ export default function Workshop() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="col-12 col-lg-9 d-flex flex-column justify-content-between gap-2">
|
<div 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">
|
||||||
{isAdmin ? (
|
{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" }}>
|
||||||
<span className="fw-bold">{workshop.status === "pending" ? (<><LuClock3 className="mb-1" /> Agendado</>) : workshop.status === "realized" ? (<><LuCheck className="mb-1" /> Realizado</>) : (<><LuX className="mb-1" /> Cancelado</>)}</span>
|
<span className="fw-bold">{workshop.status === "pending" ? (<><LuClock3 className="mb-1" /> Agendado</>) : workshop.status === "realized" ? (<><LuCheck className="mb-1" /> Realizado</>) : (<><LuX className="mb-1" /> Cancelado</>)}</span>
|
||||||
</span>
|
</span>
|
||||||
@@ -239,12 +236,12 @@ export default function Workshop() {
|
|||||||
{workshop.time_start.slice(0, 5).split(":").join("h")} - {workshop.time_end.slice(0, 5).split(":").join("h")}
|
{workshop.time_start.slice(0, 5).split(":").join("h")} - {workshop.time_end.slice(0, 5).split(":").join("h")}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isAdmin && workshop.users.length === 0 ? (
|
{role === 1 && workshop.users.length === 0 ? (
|
||||||
<div className={`${styles.timeWorkshop} text-start align-content-center d-inline-block px-3 py-2`}>
|
<div className={`${styles.timeWorkshop} text-start align-content-center d-inline-block px-3 py-2`}>
|
||||||
<small className="d-block text-muted me-2"><LuUsers className={`${styles.iconClock} mb-1 me-2`} /> Inscritos</small>
|
<small className="d-block text-muted me-2"><LuUsers className={`${styles.iconClock} mb-1 me-2`} /> Inscritos</small>
|
||||||
<span className={`${styles.contagemUtilizadoresInscritos} fs-6`}>0 utilizadores</span>
|
<span className={`${styles.contagemUtilizadoresInscritos} fs-6`}>0 utilizadores</span>
|
||||||
</div>
|
</div>
|
||||||
) : isAdmin && workshop.users.length > 0 ? (
|
) : role === 1 && workshop.users.length > 0 ? (
|
||||||
<div className={`${styles.timeWorkshop} text-start align-content-center d-inline-block px-3 py-2`}>
|
<div className={`${styles.timeWorkshop} text-start align-content-center d-inline-block px-3 py-2`}>
|
||||||
<small className="d-block text-muted mb-1 me-2"><LuUsers className={`${styles.iconClock} mb-1 me-2`} /> Inscritos</small>
|
<small className="d-block text-muted mb-1 me-2"><LuUsers className={`${styles.iconClock} mb-1 me-2`} /> Inscritos</small>
|
||||||
<span className={`${styles.contagemUtilizadoresInscritos} fs-6 px-0`}>{workshop.users.length === 1 ? "1 utilizador" : `${workshop.users.length} utilizadores`} </span>
|
<span className={`${styles.contagemUtilizadoresInscritos} fs-6 px-0`}>{workshop.users.length === 1 ? "1 utilizador" : `${workshop.users.length} utilizadores`} </span>
|
||||||
@@ -298,7 +295,7 @@ export default function Workshop() {
|
|||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!formEdit && isAdmin ? (
|
{!formEdit && role === 1 ? (
|
||||||
<div className={`${styles.buttonsContainer} d-flex flex-wrap gap-2 justify-content-center mt-5`}>
|
<div className={`${styles.buttonsContainer} d-flex flex-wrap gap-2 justify-content-center mt-5`}>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -1,31 +1,26 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Link, useNavigate, useParams } from "react-router";
|
import { Link, useNavigate, useParams } from "react-router";
|
||||||
import type { ApiErrorResponse, User, Video, Workshop } from "../../../../types";
|
import type { ApiErrorResponse, User, Workshop } from "../../../../types";
|
||||||
import styles from "./styles.module.css";
|
import styles from "./styles.module.css";
|
||||||
import { LuCheck, LuTrash2, LuX, LuPencil, LuArrowLeft } from "react-icons/lu";
|
import { LuCheck, LuTrash2, LuX, LuPencil, LuArrowLeft, LuCalendar, LuClock3 } from "react-icons/lu";
|
||||||
|
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 { useGetWorkshops } from "../../../../hooks/useGetWorkshops";
|
|
||||||
|
|
||||||
export default function User() {
|
export default function User() {
|
||||||
const { id } = useParams();
|
const { id } = useParams();
|
||||||
const [user, setUser] = useState<User | null>(null);
|
const [user, setUser] = useState<User | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [formEdit, setFormEdit] = useState<boolean>(false);
|
const [formEdit, setFormEdit] = useState<boolean>(false);
|
||||||
const [workshops, setWorkshops] = useState<Workshop[]>([]);
|
|
||||||
const [error, setError] = useState<ApiErrorResponse | null>(null);
|
const [error, setError] = useState<ApiErrorResponse | null>(null);
|
||||||
const { getWorkshops } = useGetWorkshops();
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const [videosWatched, setVideosWatched] = useState(0);
|
||||||
|
const [videosCount, setVideosCount] = useState(0);
|
||||||
|
const [workshopsInscribed, setWorkshopsInscribed] = useState(0);
|
||||||
|
const [workshopsParticipated, setWorkshopsParticipated] = useState<Workshop[]>([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchAll = async () => {
|
getUser();
|
||||||
const [workshopsData] = await Promise.all([
|
|
||||||
getWorkshops(),
|
|
||||||
]);
|
|
||||||
setWorkshops(workshopsData as Workshop[]);
|
|
||||||
getUser();
|
|
||||||
};
|
|
||||||
fetchAll();
|
|
||||||
}, [id]);
|
}, [id]);
|
||||||
|
|
||||||
async function getUser() {
|
async function getUser() {
|
||||||
@@ -44,6 +39,10 @@ export default function User() {
|
|||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
setUser(data.data);
|
setUser(data.data);
|
||||||
|
setVideosWatched(data.videosWatched);
|
||||||
|
setVideosCount(data.videosCount);
|
||||||
|
setWorkshopsInscribed(data.workshopsInscribed);
|
||||||
|
setWorkshopsParticipated(data.workshopsParticipated as Workshop[]);
|
||||||
} else {
|
} else {
|
||||||
setError(data as ApiErrorResponse);
|
setError(data as ApiErrorResponse);
|
||||||
setUser(null);
|
setUser(null);
|
||||||
@@ -166,11 +165,11 @@ export default function User() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
{ user ? (
|
{user ? (
|
||||||
<div className="my-3">
|
<div className="my-3">
|
||||||
<span className={styles.title}>Dados do Utilizador </span>
|
<span className={styles.title}>Dados do Utilizador </span>
|
||||||
|
|
||||||
<div className="row mt-5 justify-content-between">
|
<div className="row mt-5 justify-content-between px-2">
|
||||||
<div className="col-12 col-lg-8 p-2 rounded-3">
|
<div 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">
|
||||||
@@ -197,16 +196,60 @@ export default function User() {
|
|||||||
<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>
|
||||||
<span className="fs-2 fw-bold text-white">0/{JSON.parse(localStorage.getItem("videos") || "[]").filter((video: Video) => video.is_active === true).length}</span>
|
<span className="fs-2 fw-bold text-white">{videosWatched}/{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">{workshops.filter((workshop: Workshop) => workshop.status === "pending" && workshop.users.some((user: User) => user.id === user?.id)).length}</span>
|
<span className="fs-2 fw-bold text-white">{workshopsInscribed}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="col-12 p-2 mt-4 mb-3">
|
||||||
|
<span className={`${styles.subtitle} text-start`}>Workshops inscrito</span>
|
||||||
|
</div>
|
||||||
|
{workshopsParticipated.length > 0 ? (
|
||||||
|
workshopsParticipated.map((workshop) => (
|
||||||
|
<div key={workshop.id} className="col-12 col-sm-6 col-lg-4 p-2">
|
||||||
|
<div className={`${styles.boxWorkshop} text-start pb-3`}>
|
||||||
|
<div className={`${styles.thumbnailWorkshop} position-relative`}>
|
||||||
|
<img
|
||||||
|
src={`http://127.0.0.1:8000/storage/${workshop.image}`}
|
||||||
|
alt={workshop.title}
|
||||||
|
className={styles.thumbnailWorkshop}
|
||||||
|
style={imageSkeletonFadeStyle}
|
||||||
|
onLoad={onImageSkeletonLoad}
|
||||||
|
/>
|
||||||
|
<span className={`${styles.workshopStatus} position-absolute top-0 end-0 m-2 badge ${workshop.status === "pending" ? "bg-warning text-dark" : workshop.status === "realized" ? "bg-success" : "bg-danger"}`}>
|
||||||
|
{workshop.status === "pending" ? "Pendente" : workshop.status === "realized" ? "Realizado" : "Cancelado"}
|
||||||
|
</span>
|
||||||
|
<div className="position-absolute w-100 bottom-0 justify-content-evenly d-none d-md-flex mb-2 px-2">
|
||||||
|
<p className={`${styles.dateWorkshop} mb-0`}><LuCalendar className={`${styles.icon} mb-1 me-2`} />{workshop.date.split("-").reverse().join("-")}</p>
|
||||||
|
<p className={`${styles.timeWorkshop} mb-0`}><LuClock3 className={`${styles.icon} mb-1 me-2`} />{workshop.time_start.slice(0, 5)} - {workshop.time_end.slice(0, 5)}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="px-3">
|
||||||
|
<h2 className={`${styles.titleWorkshop} d-block text-start mt-3`}>{workshop.title}</h2>
|
||||||
|
<div className="d-flex flex-column d-md-none mt-2">
|
||||||
|
<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)} - {workshop.time_end.slice(0, 5)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="px-3 mt-3">
|
||||||
|
<Link to={`/admin/edit-workshop/${workshop.id}`} className={`${styles.linkWorkshop} d-block text-center py-2 px-4 text-decoration-none`}>
|
||||||
|
Ver detalhes
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<div className="col-12 text-center ps-1">
|
||||||
|
<span className="text-muted fs-5">Este utilizador ainda não participou em nenhum workshop</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-5">
|
<div className="mt-5">
|
||||||
|
|||||||
@@ -9,6 +9,66 @@
|
|||||||
font-size: var(--size-font-title);
|
font-size: var(--size-font-title);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
color: var(--primary-contrast-color);
|
||||||
|
font-size: var(--size-font-subtitle);
|
||||||
|
}
|
||||||
|
|
||||||
|
.boxWorkshop {
|
||||||
|
height: fit-content;
|
||||||
|
background-color: var(--bg-white);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
.titleWorkshop {
|
||||||
|
color: var(--text-primary-color);
|
||||||
|
font-size: var(--size-font-text);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thumbnailWorkshop {
|
||||||
|
width: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
border-top-right-radius: var(--border-radius);
|
||||||
|
border-top-left-radius: var(--border-radius);
|
||||||
|
height: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dateWorkshop,
|
||||||
|
.timeWorkshop {
|
||||||
|
background-color: var(--bg-white);
|
||||||
|
font-size: var(--size-font-small);
|
||||||
|
font-weight: 700;
|
||||||
|
border-radius: var(--border-radius-button);
|
||||||
|
padding: 5px 10px;
|
||||||
|
width: fit-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dateWorkshopMobile,
|
||||||
|
.timeWorkshopMobile {
|
||||||
|
font-size: var(--size-font-small);
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.linkWorkshop {
|
||||||
|
display: block;
|
||||||
|
width: fit-content;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
font-weight: 600;
|
||||||
|
background-color: var(--bg-grey);
|
||||||
|
color: var(--text-black);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--bg-primary-color-opacity);
|
||||||
|
color: var(--text-primary-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.workshopStatus {
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
.animateSpin{
|
.animateSpin{
|
||||||
animation: spin 1s linear infinite;
|
animation: spin 1s linear infinite;
|
||||||
}
|
}
|
||||||
@@ -71,7 +131,7 @@
|
|||||||
|
|
||||||
.userVideosWatched{
|
.userVideosWatched{
|
||||||
border-radius: var(--border-radius);
|
border-radius: var(--border-radius);
|
||||||
background-color: var(--bg-primary-color);
|
background-color: var(--bg-neutral-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.closeFormButton{
|
.closeFormButton{
|
||||||
|
|||||||
@@ -1,26 +1,21 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useDebounce } from "../../../../hooks/useDebounce";
|
import { useDebounce } from "../../../../hooks/useDebounce";
|
||||||
import type { ApiErrorResponse, User } from "../../../../types";
|
import type { ApiErrorResponse, User } from "../../../../types";
|
||||||
import { Link, Navigate } from "react-router";
|
import { Link } from "react-router";
|
||||||
import styles from "./styles.module.css";
|
import styles from "./styles.module.css";
|
||||||
import { LuPencil, LuSettings2 } from "react-icons/lu";
|
import { LuPencil, LuSettings2, LuPlus } from "react-icons/lu";
|
||||||
import { CgSpinner } from "react-icons/cg";
|
import { CgSpinner } from "react-icons/cg";
|
||||||
import { LuPlus } from "react-icons/lu";
|
import { Form, Pagination, Table } from "react-bootstrap";
|
||||||
import { Dropdown, Form, Pagination, Table } from "react-bootstrap";
|
import { AnimatePresence, motion } from "framer-motion";
|
||||||
|
|
||||||
|
|
||||||
export default function Users() {
|
export default function Users() {
|
||||||
const user = JSON.parse(localStorage.getItem("user") as unknown as string);
|
|
||||||
|
|
||||||
if (user.role_id !== 1) {
|
|
||||||
return <Navigate to="/dashboard" />;
|
|
||||||
}
|
|
||||||
|
|
||||||
const [users, setUsers] = useState<User[]>([]);
|
const [users, setUsers] = useState<User[]>([]);
|
||||||
const [error, setError] = useState<ApiErrorResponse | null>(null);
|
const [error, setError] = useState<ApiErrorResponse | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [search, setSearch] = useState<string>("");
|
const [search, setSearch] = useState<string>("");
|
||||||
const [selectedUsers, setSelectedUsers] = useState<string>("all");
|
const [selectedUsers, setSelectedUsers] = useState<string>("all");
|
||||||
|
const [showFilterDropdown, setShowFilterDropdown] = useState(false);
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
const [lastPage, setLastPage] = useState(1);
|
const [lastPage, setLastPage] = useState(1);
|
||||||
const [listTotal, setListTotal] = useState(0);
|
const [listTotal, setListTotal] = useState(0);
|
||||||
@@ -68,8 +63,8 @@ export default function Users() {
|
|||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="text-center mt-5">
|
<div className="d-flex flex-column text-center mt-5">
|
||||||
<CgSpinner className={`${styles.animateSpin} text-2xl fs-3`} />
|
<CgSpinner className={`${styles.animateSpin} mx-auto text-2xl fs-3`} />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -96,22 +91,56 @@ export default function Users() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div 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">
|
||||||
<Dropdown className="flex-grow-1" onSelect={(value) => {
|
<div
|
||||||
if (value) {
|
className="btn-group flex-grow-1 position-relative"
|
||||||
setSelectedUsers(value);
|
onMouseEnter={() => setShowFilterDropdown(true)}
|
||||||
setCurrentPage(1);
|
onMouseLeave={() => setShowFilterDropdown(false)}
|
||||||
setLoadingUsers(true);
|
>
|
||||||
}
|
<button type="button" className="btn btn-outline-secondary w-100 dropdown-toggle">
|
||||||
}}>
|
|
||||||
<Dropdown.Toggle variant="outline-secondary" className="w-100">
|
|
||||||
<LuSettings2 /> {selectedUsers === 'all' ? 'Todos' : selectedUsers === 'admin' ? 'Administradores' : 'Utilizadores'}
|
<LuSettings2 /> {selectedUsers === 'all' ? 'Todos' : selectedUsers === 'admin' ? 'Administradores' : 'Utilizadores'}
|
||||||
</Dropdown.Toggle>
|
</button>
|
||||||
<Dropdown.Menu className="text-center w-100">
|
|
||||||
<Dropdown.Item eventKey="all" active={selectedUsers === 'all'}>Todos</Dropdown.Item>
|
<AnimatePresence>
|
||||||
<Dropdown.Item eventKey="admin" active={selectedUsers === 'admin'}>Administradores</Dropdown.Item>
|
{showFilterDropdown && (
|
||||||
<Dropdown.Item eventKey="user" active={selectedUsers === 'user'}>Utilizadores</Dropdown.Item>
|
<motion.ul
|
||||||
</Dropdown.Menu>
|
initial={{ opacity: 0, y: -10, scale: 0.98 }}
|
||||||
</Dropdown>
|
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||||
|
exit={{ opacity: 0, y: -10, scale: 0.98 }}
|
||||||
|
transition={{ duration: 0.2, ease: "easeOut" }}
|
||||||
|
className="dropdown-menu text-center w-100"
|
||||||
|
style={{ display: "block", zIndex: 4000, top: "100%", marginTop: "0.25rem" }}
|
||||||
|
>
|
||||||
|
<li>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`dropdown-item ${selectedUsers === "all" ? "active" : ""}`}
|
||||||
|
onClick={() => { setSelectedUsers("all"); setCurrentPage(1); setLoadingUsers(true); setShowFilterDropdown(false); }}
|
||||||
|
>
|
||||||
|
Todos
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`dropdown-item ${selectedUsers === "admin" ? "active" : ""}`}
|
||||||
|
onClick={() => { setSelectedUsers("admin"); setCurrentPage(1); setLoadingUsers(true); setShowFilterDropdown(false); }}
|
||||||
|
>
|
||||||
|
Administradores
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`dropdown-item ${selectedUsers === "user" ? "active" : ""}`}
|
||||||
|
onClick={() => { setSelectedUsers("user"); setCurrentPage(1); setLoadingUsers(true); setShowFilterDropdown(false); }}
|
||||||
|
>
|
||||||
|
Utilizadores
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</motion.ul>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="col-12 col-sm col-md-12 col-lg-3 text-end align-content-center mt-4 mt-lg-4">
|
<div className="col-12 col-sm col-md-12 col-lg-3 text-end align-content-center mt-4 mt-lg-4">
|
||||||
@@ -199,8 +228,6 @@ export default function Users() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -7,9 +7,9 @@ import { Navigate } from "react-router";
|
|||||||
import AccordionFAQS from "../../../components/accordion FAQ´s/accordionFAQS";
|
import AccordionFAQS from "../../../components/accordion FAQ´s/accordionFAQS";
|
||||||
|
|
||||||
export default function Contactos() {
|
export default function Contactos() {
|
||||||
const user = JSON.parse(localStorage.getItem("user") as unknown as string);
|
const token = localStorage.getItem("token");
|
||||||
|
|
||||||
if (!user) {
|
if (!token) {
|
||||||
return <Navigate to="/login" />;
|
return <Navigate to="/login" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,86 +1,72 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import type { ApiErrorResponse, User, Video, Workshop } from "../../../types";
|
import type { ApiErrorResponse, Video, Workshop, DashboardResponse } from "../../../types";
|
||||||
import styles from "./styles.module.css";
|
import styles from "./styles.module.css";
|
||||||
import { LuArrowUpRight, LuCalendar, LuClock3, LuPencil } from "react-icons/lu";
|
import { LuArrowUpRight, LuCalendar, LuClock3, LuPencil } from "react-icons/lu";
|
||||||
import { Link } from "react-router";
|
import { Link } from "react-router";
|
||||||
import { CgSpinner } from "react-icons/cg";
|
import { CgSpinner } from "react-icons/cg";
|
||||||
import { ProgressBar } from "react-bootstrap";
|
import { ProgressBar } from "react-bootstrap";
|
||||||
import { useGetCurrentUser } from "../../../hooks/useGetCurrentUser"
|
/* import { useGetCurrentUser } from "../../../hooks/useGetCurrentUser"
|
||||||
|
|
||||||
import { useGetVideosLength } from "../../../hooks/useGetVideosLength";
|
import { useGetVideosLength } from "../../../hooks/useGetVideosLength";
|
||||||
import { useNextVideos } from "../../../hooks/useNextVideos";
|
import { useNextVideos } from "../../../hooks/useNextVideos";
|
||||||
import { useNextWorkshops } from "../../../hooks/useNextWorkshops";
|
import { useNextWorkshops } from "../../../hooks/useNextWorkshops";
|
||||||
import { useGetWorkshopsLength } from "../../../hooks/useGetWorkshopsLength";
|
import { useGetWorkshopsLength } from "../../../hooks/useGetWorkshopsLength"; */
|
||||||
import Swal from "sweetalert2";
|
import Swal from "sweetalert2";
|
||||||
import { PiCheckCircleFill } from "react-icons/pi";
|
import { PiCheckCircleFill } from "react-icons/pi";
|
||||||
import { imageSkeletonFadeStyle, onImageSkeletonLoad } from "../../../utils/imageSkeleton";
|
import { imageSkeletonFadeStyle, onImageSkeletonLoad } from "../../../utils/imageSkeleton";
|
||||||
|
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
const user = JSON.parse(localStorage.getItem("user") || "{}") as User;
|
|
||||||
const isAdmin = user.role_id === 1;
|
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState<ApiErrorResponse | null>(null);
|
const [error, setError] = useState<ApiErrorResponse | null>(null);
|
||||||
const [messageSuccess, setMessageSuccess] = useState<string | null>(null);
|
const [messageSuccess, setMessageSuccess] = useState<string | null>(null);
|
||||||
const [sending, setSending] = useState(false);
|
const [sending, setSending] = useState(false);
|
||||||
const { getCurrentUser } = useGetCurrentUser();
|
const [videos, setVideos] = useState<Video[]>([]);
|
||||||
const [currentUserData, setCurrentUserData] = useState<User | null>(null);
|
const [videosWatched, setVideosWatched] = useState(0);
|
||||||
const { getVideosLength } = useGetVideosLength();
|
const [videosCount, setVideosCount] = useState(0);
|
||||||
const { getNextVideos } = useNextVideos();
|
const [workshops, setWorkshops] = useState<Workshop[]>([]);
|
||||||
const [nextVideos, setNextVideos] = useState<Video[]>([]);
|
const [workshopsInscribed, setWorkshopsInscribed] = useState(0);
|
||||||
const { getNextWorkshops } = useNextWorkshops();
|
const [workshopsCount, setWorkshopsCount] = useState(0);
|
||||||
const [nextWorkshops, setNextWorkshops] = useState<Workshop[]>([]);
|
const [role, setRole] = useState(0);
|
||||||
const { getWorkshopsLength } = useGetWorkshopsLength();
|
const [userId, setUserId] = useState<number | null>(null);
|
||||||
const [videosStats, setVideosStats] = useState({
|
|
||||||
videos: 0,
|
|
||||||
videosWatched: 0
|
|
||||||
});
|
|
||||||
const [workshopsStats, setWorkshopsStats] = useState({
|
|
||||||
workshops: 0,
|
|
||||||
workshopsInscribed: 0
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchAll = async () => {
|
getDashboard();
|
||||||
setLoading(true);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const [
|
|
||||||
currentUserData,
|
|
||||||
videosLengthData,
|
|
||||||
workshopsLengthData,
|
|
||||||
nextVideosData,
|
|
||||||
nextWorkshopsData
|
|
||||||
] = await Promise.all([
|
|
||||||
getCurrentUser(),
|
|
||||||
getVideosLength(),
|
|
||||||
getWorkshopsLength(),
|
|
||||||
getNextVideos(),
|
|
||||||
getNextWorkshops(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
setCurrentUserData((currentUserData as { data: User }).data);
|
|
||||||
setVideosStats(videosLengthData as { videos: number, videosWatched: number });
|
|
||||||
setWorkshopsStats(workshopsLengthData as { workshops: number, workshopsInscribed: number });
|
|
||||||
|
|
||||||
if ("videos" in nextVideosData) {
|
|
||||||
setNextVideos(nextVideosData.videos);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ("workshops" in nextWorkshopsData) {
|
|
||||||
setNextWorkshops(nextWorkshopsData.workshops as unknown as Workshop[]);
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
fetchAll();
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
async function getDashboard() {
|
||||||
|
setLoading(true);
|
||||||
|
const response = await fetch("http://127.0.0.1:8000/api/dashboard", {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
Accept: "application/json",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Bearer ${localStorage.getItem("token")}`
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
setVideos(data.videos);
|
||||||
|
setVideosWatched(data.videosWatched);
|
||||||
|
setVideosCount(data.videosCount);
|
||||||
|
setWorkshops(data.workshops);
|
||||||
|
setWorkshopsInscribed(data.workshopsInscribed);
|
||||||
|
setWorkshopsCount(data.workshopsCount);
|
||||||
|
setRole(data.role);
|
||||||
|
setUserId(data.userId);
|
||||||
|
|
||||||
|
localStorage.setItem("videosWatched", data.videosWatched.toString());
|
||||||
|
localStorage.setItem("videosCount", data.videosCount.toString());
|
||||||
|
setLoading(false);
|
||||||
|
} else {
|
||||||
|
setError(data as ApiErrorResponse);
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
/* Vídeos */
|
/* Vídeos */
|
||||||
let progressoVideos = Math.round(videosStats.videosWatched / videosStats.videos * 100);
|
let progressoVideos = Math.round(videosWatched / videosCount * 100);
|
||||||
|
|
||||||
/* Inscrever num workshop */
|
/* Inscrever num workshop */
|
||||||
async function inscrever(workshopId: number) {
|
async function inscrever(workshopId: number) {
|
||||||
@@ -96,17 +82,14 @@ export default function Home() {
|
|||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
setMessageSuccess(data.message);
|
|
||||||
Swal.fire({
|
Swal.fire({
|
||||||
title: data.message,
|
title: data.message,
|
||||||
icon: 'success',
|
icon: 'success',
|
||||||
showConfirmButton: false,
|
showConfirmButton: false,
|
||||||
showCloseButton: true,
|
showCloseButton: true,
|
||||||
});
|
});
|
||||||
const result = await getNextWorkshops();
|
getDashboard();
|
||||||
if ("workshops" in result) {
|
setWorkshops(data.workshops as DashboardResponse['workshops']);
|
||||||
setNextWorkshops(result.workshops as unknown as Workshop[]);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
Swal.fire({
|
Swal.fire({
|
||||||
title: data.message,
|
title: data.message,
|
||||||
@@ -137,10 +120,8 @@ export default function Home() {
|
|||||||
showConfirmButton: false,
|
showConfirmButton: false,
|
||||||
showCloseButton: true,
|
showCloseButton: true,
|
||||||
});
|
});
|
||||||
const result = await getNextWorkshops();
|
getDashboard();
|
||||||
if ("workshops" in result) {
|
setWorkshops(data.workshops as DashboardResponse['workshops']);
|
||||||
setNextWorkshops(result.workshops as unknown as Workshop[]);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
Swal.fire({
|
Swal.fire({
|
||||||
title: data.message,
|
title: data.message,
|
||||||
@@ -200,7 +181,6 @@ export default function Home() {
|
|||||||
return (
|
return (
|
||||||
<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">
|
||||||
<CgSpinner className={`${styles.animateSpin} text-2xl fs-3`} />
|
<CgSpinner className={`${styles.animateSpin} text-2xl fs-3`} />
|
||||||
<span>A carregar a página...</span>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -210,11 +190,11 @@ export default function Home() {
|
|||||||
|
|
||||||
<div className={`${styles.containerVideos} px-2 p-sm-4`}>
|
<div className={`${styles.containerVideos} px-2 p-sm-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 px-0 `}>{isAdmin ? "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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!isAdmin && (
|
{role !== 1 && (
|
||||||
<>
|
<>
|
||||||
<ProgressBar className={`${styles.progressBar}`} now={progressoVideos} label={`${progressoVideos}%`} />
|
<ProgressBar className={`${styles.progressBar}`} now={progressoVideos} label={`${progressoVideos}%`} />
|
||||||
|
|
||||||
@@ -227,11 +207,11 @@ export default function Home() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="row mt-4 px-2">
|
<div className="row mt-4 px-2">
|
||||||
{nextVideos.length > 0 ? nextVideos.map((video: Video) => (
|
{videos.length > 0 ? videos.map((video: Video) => (
|
||||||
<div className="col-12 col-sm-6 col-lg-4 p-2">
|
<div 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 && (
|
{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>
|
||||||
)}
|
)}
|
||||||
<img
|
<img
|
||||||
@@ -249,7 +229,7 @@ export default function Home() {
|
|||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
)) : nextVideos.length === 0 ? (
|
)) : videos.length === 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>
|
||||||
@@ -263,7 +243,7 @@ export default function Home() {
|
|||||||
<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>
|
</div>
|
||||||
<div className="row mt-4 mt-sm-1 px-2">
|
<div className="row mt-4 mt-sm-1 px-2">
|
||||||
{nextWorkshops.length > 0 ? nextWorkshops.map((workshop: Workshop) => (
|
{workshops.length > 0 ? workshops.map((workshop: Workshop) => (
|
||||||
<div className="col-12 col-sm-6 col-lg-4 p-2 position-relative">
|
<div className="col-12 col-sm-6 col-lg-4 p-2 position-relative">
|
||||||
<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`}>
|
||||||
@@ -287,10 +267,10 @@ export default function Home() {
|
|||||||
</div>
|
</div>
|
||||||
</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={`${isAdmin ? `/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>
|
||||||
|
|
||||||
{!isAdmin ? (
|
{role !== 1 ? (
|
||||||
currentUserData && (workshop.users as unknown as number[]).includes(currentUserData.id) ? (
|
(workshop.users as unknown as number[]).includes(userId as number) ? (
|
||||||
<button type="button" className={`${styles.btncancelarInscricao} btn text-center py-2 text-decoration-none`} onClick={() => cancelarInscricao(workshop.id)} key={workshop.id} style={{ width: '180px' }}>
|
<button type="button" className={`${styles.btncancelarInscricao} btn text-center py-2 text-decoration-none`} onClick={() => cancelarInscricao(workshop.id)} key={workshop.id} style={{ width: '180px' }}>
|
||||||
Anular inscrição
|
Anular inscrição
|
||||||
</button>
|
</button>
|
||||||
@@ -304,7 +284,7 @@ export default function Home() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
) : nextWorkshops.length === 0 ? (
|
) : workshops.length === 0 ? (
|
||||||
<div className="col-12 text-center text-sm-start px-0">
|
<div className="col-12 text-center text-sm-start px-0">
|
||||||
<span className="text-black text-muted fs-5 ms-sm-2 ps-sm-1">Sem workshops agendados</span>
|
<span className="text-black text-muted fs-5 ms-sm-2 ps-sm-1">Sem workshops agendados</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -368,13 +348,13 @@ export default function Home() {
|
|||||||
<div className="h-100 pt-4">
|
<div className="h-100 pt-4">
|
||||||
<div className={`${styles.userVideosWatched} text-start justify-content-evenly d-flex flex-column h-100 p-4`}>
|
<div className={`${styles.userVideosWatched} text-start justify-content-evenly d-flex flex-column h-100 p-4`}>
|
||||||
<div className="d-flex flex-column">
|
<div className="d-flex flex-column">
|
||||||
<span className="fw-normal fs-3 text-white" > {isAdmin ? "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" }}>{isAdmin ? videosStats.videos : `${videosStats.videosWatched}/${videosStats.videos}`}</span>
|
<span className=" fw-bold text-white" style={{ fontSize: "4rem" }}>{role === 1 ? videosCount : `${videosWatched}/${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)" }}>{isAdmin ? "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" }}>{isAdmin ? workshopsStats.workshops : `${workshopsStats.workshopsInscribed}/${workshopsStats.workshops}`}</span>
|
<span className="fw-bold text-white" style={{ fontSize: "4rem" }}>{role === 1 ? workshopsCount : `${workshopsInscribed}/${workshopsCount}`}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,45 +1,74 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import type { ApiErrorResponse, User, Video, Workshop } from "../../../types";
|
import type { User, Video, Workshop } from "../../../types";
|
||||||
import styles from "./styles.module.css";
|
import styles from "./styles.module.css";
|
||||||
import { LuCheck, LuKeyRound, LuPencil, LuX } from "react-icons/lu";
|
import { LuCheck, LuKeyRound, LuPencil, LuX, LuArrowUpRight, LuClock3, LuCalendar } from "react-icons/lu";
|
||||||
import { useGetWorkshops } from "../../../hooks/useGetWorkshops";
|
import { CgSpinner } from "react-icons/cg";
|
||||||
|
import { Link } from "react-router";
|
||||||
|
import { ProgressBar } from "react-bootstrap";
|
||||||
|
import { imageSkeletonFadeStyle, onImageSkeletonLoad } from "../../../utils/imageSkeleton";
|
||||||
|
import { PiCheckCircleFill } from "react-icons/pi";
|
||||||
|
import Swal from "sweetalert2";
|
||||||
|
|
||||||
export default function Profile() {
|
export default function Profile() {
|
||||||
const [user, setUser] = useState<User | null>(JSON.parse(localStorage.getItem("user") as unknown as string));
|
const [role, setRole] = useState(0);
|
||||||
const [error, setError] = useState<ApiErrorResponse | null>(null);
|
const [userId, setUserId] = useState(0);
|
||||||
const isAdmin = user?.role_id === 1;
|
const [loading, setLoading] = useState(true);
|
||||||
/* const [loading, setLoading] = useState(true); */
|
|
||||||
|
|
||||||
const [formEdit, setFormEdit] = useState<boolean>(false);
|
const [formEdit, setFormEdit] = useState<boolean>(false);
|
||||||
const [formEditPassword, setFormEditPassword] = useState<boolean>(false);
|
const [formEditPassword, setFormEditPassword] = useState<boolean>(false);
|
||||||
const [messageUpdateUser, setMessageUpdateUser] = useState<string>("");
|
const [userData, setUserData] = useState<User | null>(null);
|
||||||
const [messageUpdatePassword, setMessageUpdatePassword] = useState<string>("");
|
|
||||||
const { getWorkshops } = useGetWorkshops();
|
|
||||||
const [workshops, setWorkshops] = useState<Workshop[]>([]);
|
const [workshops, setWorkshops] = useState<Workshop[]>([]);
|
||||||
|
const [workshopsCount, setWorkshopsCount] = useState(0);
|
||||||
|
const [nextWorkshops, setNextWorkshops] = useState<Workshop[]>([]);
|
||||||
|
const [videos, setVideos] = useState<Video[]>([]);
|
||||||
|
const [videosCount, setVideosCount] = useState(0);
|
||||||
|
const [videosWatched, setVideosWatched] = useState(0);
|
||||||
|
const [progressoVideos, setProgressoVideos] = useState(0);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchAll = async () => {
|
getProfile();
|
||||||
const [workshopsData] = await Promise.all([
|
|
||||||
getWorkshops(),
|
|
||||||
]);
|
|
||||||
setWorkshops(workshopsData as Workshop[]);
|
|
||||||
};
|
|
||||||
fetchAll();
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
async function getProfile() {
|
||||||
|
setLoading(true);
|
||||||
|
const response = await fetch("http://127.0.0.1:8000/api/profile", {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
Accept: "application/json",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Bearer ${localStorage.getItem("token")}`
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
setUserData(data.data as User);
|
||||||
|
setWorkshops(data.workshopsInscribed as Workshop[]);
|
||||||
|
setWorkshopsCount(data.workshopsCount);
|
||||||
|
setVideos(data.videos as Video[]);
|
||||||
|
setVideosCount(data.videosCount);
|
||||||
|
setNextWorkshops(data.nextWorkshops as Workshop[]);
|
||||||
|
setVideosWatched(data.videosWatched);
|
||||||
|
setUserId(data.userId);
|
||||||
|
setRole(data.role);
|
||||||
|
setProgressoVideos(Math.round(data.videosWatched / data.videosCount * 100));
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
async function updatePassword(event: React.FormEvent<HTMLFormElement>) {
|
async function updatePassword(event: React.FormEvent<HTMLFormElement>) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const formData = new FormData(event.currentTarget);
|
const formData = new FormData(event.currentTarget);
|
||||||
const user_id = user?.id;
|
const user_id = userData?.id;
|
||||||
const passwordAtual = formData.get("passwordAtual");
|
const passwordAtual = formData.get("passwordAtual");
|
||||||
const novaPassword = formData.get("novaPassword");
|
const novaPassword = formData.get("novaPassword");
|
||||||
const confirmarPassword = formData.get("confirmarPassword");
|
const confirmarPassword = formData.get("confirmarPassword");
|
||||||
|
|
||||||
if (novaPassword !== confirmarPassword) {
|
if (novaPassword !== confirmarPassword) {
|
||||||
setError({
|
Swal.fire({
|
||||||
message: "As passwords não coincidem",
|
title: "As passwords não coincidem",
|
||||||
data: null,
|
icon: 'error',
|
||||||
errors: {},
|
showConfirmButton: false,
|
||||||
|
showCloseButton: true,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -64,24 +93,29 @@ export default function Profile() {
|
|||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
setMessageUpdatePassword("Palavra-passe atualizada com sucesso");
|
Swal.fire({
|
||||||
setTimeout(() => {
|
title: data.message as string,
|
||||||
setMessageUpdatePassword("");
|
icon: 'success',
|
||||||
}, 3000)
|
showConfirmButton: false,
|
||||||
|
showCloseButton: true,
|
||||||
|
});
|
||||||
setFormEditPassword(false);
|
setFormEditPassword(false);
|
||||||
setError(null);
|
|
||||||
} else {
|
} else {
|
||||||
setMessageUpdatePassword("");
|
Swal.fire({
|
||||||
setError(data as ApiErrorResponse);
|
title: data.message as string,
|
||||||
|
icon: 'error',
|
||||||
|
showConfirmButton: false,
|
||||||
|
showCloseButton: true,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function update(event: React.FormEvent<HTMLFormElement>) {
|
async function update(event: React.FormEvent<HTMLFormElement>) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const formData = new FormData(event.currentTarget);
|
const formData = new FormData(event.currentTarget);
|
||||||
const user_id = user?.id;
|
const user_id = userData?.id;
|
||||||
const name = formData.get("name");
|
const name = formData.get("name") || userData?.name;
|
||||||
const email = formData.get("email");
|
const email = formData.get("email") || userData?.email;
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
user_id: user_id,
|
user_id: user_id,
|
||||||
@@ -102,32 +136,63 @@ export default function Profile() {
|
|||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
setUser(data.data);
|
|
||||||
setFormEdit(false);
|
setFormEdit(false);
|
||||||
setMessageUpdateUser(data.message as string);
|
Swal.fire({
|
||||||
setTimeout(() => {
|
title: data.message,
|
||||||
setMessageUpdateUser("");
|
icon: 'success',
|
||||||
}, 3000)
|
showConfirmButton: false,
|
||||||
setError(null);
|
showCloseButton: true,
|
||||||
|
});
|
||||||
|
getProfile();
|
||||||
} else {
|
} else {
|
||||||
setMessageUpdateUser("");
|
Swal.fire({
|
||||||
setError(data as ApiErrorResponse);
|
title: data.message,
|
||||||
|
icon: 'error',
|
||||||
|
showConfirmButton: false,
|
||||||
|
showCloseButton: true,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Cancelar inscrição num workshop */
|
||||||
|
async function cancelarInscricao(workshopId: number) {
|
||||||
|
const response = await fetch(`http://127.0.0.1:8000/api/cancelar-inscricao/${workshopId}`, {
|
||||||
|
method: "DELETE",
|
||||||
|
headers: {
|
||||||
|
Accept: "application/json",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Bearer ${localStorage.getItem("token")}`
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
/* { user ? {
|
const data = await response.json();
|
||||||
|
|
||||||
} : {
|
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
Swal.fire({
|
||||||
|
title: data.message,
|
||||||
|
icon: 'error',
|
||||||
|
showConfirmButton: false,
|
||||||
|
showCloseButton: true,
|
||||||
|
});
|
||||||
|
getProfile();
|
||||||
|
setWorkshops(data.workshops as Workshop[]);
|
||||||
|
} else {
|
||||||
|
Swal.fire({
|
||||||
|
title: data.message,
|
||||||
|
icon: 'error',
|
||||||
|
showConfirmButton: false,
|
||||||
|
showCloseButton: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="text-center">
|
<div className="text-center mt-5 d-flex flex-column gap-2 align-items-center">
|
||||||
<CgSpinner className={`${styles.animateSpin} text-2xl fs-3`} />
|
<CgSpinner className={`${styles.animateSpin} text-2xl fs-3`} />
|
||||||
<span>A carregar os seus dados...</span>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
} */
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container">
|
<div className="container">
|
||||||
@@ -140,43 +205,43 @@ export default function Profile() {
|
|||||||
<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">
|
||||||
<div className="d-flex flex-column">
|
<div className="d-flex flex-column">
|
||||||
<span className="fw-bold fs-4 mt-1">{user?.name}</span>
|
<span className="fw-bold fs-4 mt-1">{userData?.name}</span>
|
||||||
<span className="fs-5 flex-grow-1">{user?.email}</span>
|
<span className="fs-5 flex-grow-1">{userData?.email}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<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(user?.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-column flex-sm-row flex-grow-1 justify-content-evenly justify-content-md-end flex-wrap mt-3 mx-auto">
|
<div className="d-flex flex-column flex-sm-row 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); setMessageUpdateUser(""); }}> 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); }}> 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); setMessageUpdateUser(""); }}> Alterar password <LuKeyRound className={`${styles.icon} mb-1 text-danger`} /></a>
|
`} onClick={() => { setFormEditPassword(true); }}> Alterar password <LuKeyRound className={`${styles.icon} mb-1 text-danger`} /></a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-12 col-lg-4 p-3">
|
<div className="col-12 col-lg-4 p-3">
|
||||||
{isAdmin ? (
|
{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-primary-color-opacity)" }}>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">{JSON.parse(localStorage.getItem("videos") || "[]").filter((video: Video) => video.is_active === true).length}</span>
|
<span className="fs-2 fw-bold text-white">{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">{workshops.filter((workshop: Workshop) => workshop.status === "pending").length}</span>
|
<span className="fs-2 fw-bold text-white">{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-primary-color-opacity)" }}>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">0/{JSON.parse(localStorage.getItem("videos") || "[]").filter((video: Video) => video.is_active === true).length}</span>
|
<span className="fs-2 fw-bold text-white">{videosWatched}/{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">{workshops.filter((workshop: Workshop) => workshop.status === "pending" && workshop.users.some((user: User) => user.id === user?.id)).length}</span>
|
<span className="fs-2 fw-bold text-white">{workshopsCount}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -185,39 +250,23 @@ export default function Profile() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{messageUpdateUser || messageUpdatePassword ? (
|
|
||||||
<div className="">
|
|
||||||
<div className="alert alert-success mt-4">
|
|
||||||
{messageUpdateUser || messageUpdatePassword}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{formEdit ? (
|
{formEdit ? (
|
||||||
<div className="my-3">
|
<div 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">
|
||||||
<div className="row g-3 mt-2">
|
<div className="row g-3 mt-2">
|
||||||
<input type="hidden" name="user_id" value={user?.id} />
|
<input type="hidden" name="user_id" value={userId} />
|
||||||
<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>
|
||||||
<input type="text" className="form-control py-2" id="name" name="name" defaultValue={user?.name} />
|
<input type="text" className="form-control py-2" id="name" name="name" defaultValue={userData?.name} />
|
||||||
</div>
|
</div>
|
||||||
<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="email">Email</label>
|
<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} />
|
<input type="email" className="form-control py-2" id="email" name="email" defaultValue={userData?.email} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{error?.errors ? (
|
|
||||||
<div className="">
|
|
||||||
<div className="alert alert-danger mt-4">
|
|
||||||
<p>{error.errors.message || error.message}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
<div className="buttonsContainer mt-5 d-flex gap-2 justify-content-center flex-wrap" >
|
<div className="buttonsContainer mt-5 d-flex gap-2 justify-content-center flex-wrap" >
|
||||||
<button type="button" className={`${styles.closeFormButton}`} onClick={() => setFormEdit(false)}> Cancelar <LuX className={`${styles.icon} mb-1`} /></button>
|
<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>
|
<button type="submit" className={`${styles.submitFormButton}`} >Submeter dados <LuCheck className="mb-1" /></button>
|
||||||
@@ -225,12 +274,12 @@ export default function Profile() {
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
) : formEditPassword ? (
|
) : formEditPassword ? (
|
||||||
<div className="my-3">
|
<div 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">
|
||||||
<div className="row g-3 mt-2">
|
<div className="row g-3 mt-2">
|
||||||
<input type="hidden" name="user_id" value={user?.id} />
|
<input type="hidden" name="user_id" value={userId} />
|
||||||
<div className="col-12 col-lg-4 text-start">
|
<div className="col-12 col-lg-4 text-start">
|
||||||
<label className="form-label fw-bold" htmlFor="passwordAtual">Password atual</label>
|
<label className="form-label fw-bold" htmlFor="passwordAtual">Password atual</label>
|
||||||
<input type="password" className="form-control py-2" id="passwordAtual" name="passwordAtual" />
|
<input type="password" className="form-control py-2" id="passwordAtual" name="passwordAtual" />
|
||||||
@@ -245,14 +294,6 @@ export default function Profile() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{error?.errors ? (
|
|
||||||
<div className="">
|
|
||||||
<div className="alert alert-danger mt-4">
|
|
||||||
<p>{error.errors.message || error.message}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
<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="button" className={`${styles.closeFormButton}`} onClick={() => setFormEditPassword(false)}> Cancelar <LuX className={`${styles.icon} mb-1`} /></button>
|
<button type="button" className={`${styles.closeFormButton}`} onClick={() => setFormEditPassword(false)}> Cancelar <LuX className={`${styles.icon} mb-1`} /></button>
|
||||||
<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>
|
||||||
@@ -260,6 +301,141 @@ export default function Profile() {
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
<div 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" >
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{role !== 1 && (
|
||||||
|
<>
|
||||||
|
<ProgressBar className={`${styles.progressBar}`} now={progressoVideos} label={`${progressoVideos}%`} />
|
||||||
|
|
||||||
|
{progressoVideos === 100 && (
|
||||||
|
<div className="text-center mt-3">
|
||||||
|
<span className="text-black fw-bold fs-6">Parabéns! A sua formação está completa!</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="row mt-4 px-2">
|
||||||
|
{videosCount > 0 ? videos.map((video: Video) => (
|
||||||
|
<div 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}>
|
||||||
|
<div className={`${styles.boxVideo} position-relative`} data-category={video.categories?.map((category) => category.id).join(', ')} >
|
||||||
|
{role === 1 && (
|
||||||
|
<Link to={`/admin/edit-video/${video.id}`}> <LuPencil className={`${styles.iconEdit} text-decoration-none`} /> </Link>
|
||||||
|
)}
|
||||||
|
<img
|
||||||
|
src={`http://127.0.0.1:8000/storage/${video.thumbnail}`}
|
||||||
|
className={`${styles.videoThumbnail} position-absolute top-0 start-0 bottom-0 h-100`}
|
||||||
|
alt={video.title}
|
||||||
|
style={imageSkeletonFadeStyle}
|
||||||
|
onLoad={onImageSkeletonLoad}
|
||||||
|
/>
|
||||||
|
<div className={`${styles.boxVideoInfo} px-3 py-2 text-start text-truncate position-absolute bottom-0`}>
|
||||||
|
<h2 className={`${styles.titleVideo} d-flex text-wrap mb-1`}>{video.title}</h2>
|
||||||
|
</div>
|
||||||
|
{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>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
)) : videosCount === 0 ? (
|
||||||
|
<div className="col-12 text-start ps-1">
|
||||||
|
<span className="text-muted fs-5">Nenhum vídeo encontrado</span>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="ms-0 px-4 mt-4">
|
||||||
|
<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`}>{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>
|
||||||
|
</div>
|
||||||
|
<div className="row mt-4 mt-sm-1 px-2 mb-5">
|
||||||
|
{role !== 1 && workshopsCount > 0 ? workshops.map((workshop: Workshop) => (
|
||||||
|
<div className="col-12 col-sm-6 col-lg-4 p-2 position-relative">
|
||||||
|
<div className={`${styles.boxWorkshop} text-start pb-3`}>
|
||||||
|
<div className={`${styles.thumbnailWorkshop} position-relative`}>
|
||||||
|
<img
|
||||||
|
src={`http://127.0.0.1:8000/storage/${workshop.image}`}
|
||||||
|
alt={workshop.title}
|
||||||
|
className={styles.thumbnailWorkshop}
|
||||||
|
style={imageSkeletonFadeStyle}
|
||||||
|
onLoad={onImageSkeletonLoad}
|
||||||
|
/>
|
||||||
|
<div className="position-absolute w-100 bottom-0 justify-content-evenly d-none d-md-flex d-xl-none d-xxl-flex mb-2">
|
||||||
|
<p className={`${styles.dateWorkshop}`}><LuCalendar className={`${styles.icon} mb-1 me-2`} />{workshop.date.split('-').reverse().join('-')}</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 className="px-3">
|
||||||
|
<h2 className={`${styles.titleWorkshop} d-block text-start mt-3`}>{workshop.title}</h2>
|
||||||
|
<div className="position-relative d-flex flex-column d-md-none d-xl-flex d-xxl-none mt-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.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">
|
||||||
|
<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 ? (
|
||||||
|
<button type="button" className={`${styles.btncancelarInscricao} btn text-center py-2 text-decoration-none`} onClick={() => cancelarInscricao(workshop.id)} key={workshop.id} style={{ width: '180px' }}>
|
||||||
|
Anular inscrição
|
||||||
|
</button>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
) : workshopsCount === 0 ? (
|
||||||
|
<div className="col-12 text-center px-0 mt-3">
|
||||||
|
<span className="text-black text-muted fs-5 mb-5">Não está inscrito em nenhum workshop</span>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{role === 1 && workshopsCount > 0 ? nextWorkshops.map((workshop: Workshop) => (
|
||||||
|
<div className="col-12 col-sm-6 col-lg-4 p-2 position-relative">
|
||||||
|
<div className={`${styles.boxWorkshop} text-start pb-3`}>
|
||||||
|
<div className={`${styles.thumbnailWorkshop} position-relative`}>
|
||||||
|
<img
|
||||||
|
src={`http://127.0.0.1:8000/storage/${workshop.image}`}
|
||||||
|
alt={workshop.title}
|
||||||
|
className={styles.thumbnailWorkshop}
|
||||||
|
style={imageSkeletonFadeStyle}
|
||||||
|
onLoad={onImageSkeletonLoad}
|
||||||
|
/>
|
||||||
|
<div className="position-absolute w-100 bottom-0 justify-content-evenly d-none d-md-flex d-xl-none d-xxl-flex mb-2">
|
||||||
|
<p className={`${styles.dateWorkshop}`}><LuCalendar className={`${styles.icon} mb-1 me-2`} />{workshop.date.split('-').reverse().join('-')}</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 className="px-3">
|
||||||
|
<h2 className={`${styles.titleWorkshop} d-block text-start mt-3`}>{workshop.title}</h2>
|
||||||
|
<div className="position-relative d-flex flex-column d-md-none d-xl-flex d-xxl-none mt-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.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">
|
||||||
|
<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>
|
||||||
|
)
|
||||||
|
) : workshopsCount === 0 ? (
|
||||||
|
<div className="col-12 text-center px-0 mt-3">
|
||||||
|
<span className="text-black text-muted fs-5 mb-5">Não há workshops agendados</span>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -9,6 +9,11 @@
|
|||||||
font-size: var(--size-font-title);
|
font-size: var(--size-font-title);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
color: var(--primary-contrast-color);
|
||||||
|
font-size: var(--size-font-subtitle);
|
||||||
|
}
|
||||||
|
|
||||||
.userCard{
|
.userCard{
|
||||||
border-radius: var(--border-radius);
|
border-radius: var(--border-radius);
|
||||||
background-color: var(--bg-white);
|
background-color: var(--bg-white);
|
||||||
@@ -91,6 +96,189 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.containerVideos, .containerForm {
|
||||||
|
background-color: var(--bg-white);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
.link {
|
||||||
|
display: block;
|
||||||
|
width: fit-content;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary-color);
|
||||||
|
transition: all .3s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--primary-color-contrast);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.progressBar :global(.progress-bar) {
|
||||||
|
position: relative;
|
||||||
|
overflow: visible;
|
||||||
|
color: var(--text-primary-color);
|
||||||
|
font-weight: 800;
|
||||||
|
background-color: var(--bg-primary-color-opacity);
|
||||||
|
}
|
||||||
|
|
||||||
|
.linkVideo {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
background-color: var(--bg-white);
|
||||||
|
}
|
||||||
|
|
||||||
|
.boxVideo {
|
||||||
|
background: #e0e0e0; /* cinzento enquanto a imagem não carrega */
|
||||||
|
background: linear-gradient(90deg, #e0e0e0 25%, #f0f0f0 50%, #e0e0e0 75%);
|
||||||
|
background-size: 200% 100%;
|
||||||
|
animation: skeleton 1.5s infinite;
|
||||||
|
height: 200px;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
.boxVideo::after {
|
||||||
|
content: '\F4F4';
|
||||||
|
color: var(--bg-grey);
|
||||||
|
font-size: 4rem;
|
||||||
|
align-content: center;
|
||||||
|
font-family: 'bootstrap-icons';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(180deg, rgba(0, 0, 0, 0) 40%, rgba(0, 0, 0, 0.8) 100%);
|
||||||
|
z-index: 1;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.boxVideo:hover::after {
|
||||||
|
color: var(--bg-primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.boxVideoInfo {
|
||||||
|
z-index: 1000;
|
||||||
|
position: relative;
|
||||||
|
max-width: 100%;
|
||||||
|
color: var(--text-white);
|
||||||
|
}
|
||||||
|
|
||||||
|
.titleVideo {
|
||||||
|
color: var(--text-white);
|
||||||
|
font-size: var(--size-font-text);
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.descriptionVideo {
|
||||||
|
color: var(--text-white);
|
||||||
|
font-size: var(--size-font-small);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.videoThumbnail {
|
||||||
|
width: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
border-top-right-radius: var(--border-radius);
|
||||||
|
border-top-left-radius: var(--border-radius);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dateWorkshop,
|
||||||
|
.timeWorkshop {
|
||||||
|
background-color: var(--bg-white);
|
||||||
|
font-size: var(--size-font-small);
|
||||||
|
font-weight: 700;
|
||||||
|
border-radius: var(--border-radius-button);
|
||||||
|
padding: 5px 10px;
|
||||||
|
width: fit-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dateWorkshopMobile,
|
||||||
|
.timeWorkshopMobile {
|
||||||
|
font-size: var(--size-font-small);
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.boxWorkshop {
|
||||||
|
height: fit-content;
|
||||||
|
/* max-height: 400px; */
|
||||||
|
background-color: var(--bg-white);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
.titleWorkshop {
|
||||||
|
color: var(--text-primary-color);
|
||||||
|
font-size: var(--size-font-text);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thumbnailWorkshop {
|
||||||
|
width: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
border-top-right-radius: var(--border-radius);
|
||||||
|
border-top-left-radius: var(--border-radius);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
height: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.linkWorkshop {
|
||||||
|
display: block;
|
||||||
|
width: fit-content;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
font-weight: 600;
|
||||||
|
|
||||||
|
background-color: var(--bg-grey);
|
||||||
|
color: var(--text-black);
|
||||||
|
transition: all .3s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--bg-primary-color-opacity);
|
||||||
|
color: var(--text-primary-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.btncancelarInscricao {
|
||||||
|
display: block;
|
||||||
|
width: fit-content;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
font-weight: 600;
|
||||||
|
|
||||||
|
background-color: var(--bg-primary-color-opacity);
|
||||||
|
color: var(--text-primary-color);
|
||||||
|
transition: all .3s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--bg-primary-color);
|
||||||
|
color: var(--text-white);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.btnInscrever {
|
||||||
|
display: block;
|
||||||
|
width: fit-content;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
font-weight: 600;
|
||||||
|
|
||||||
|
background-color: var(--tertiary-color);
|
||||||
|
color: var(--text-white);
|
||||||
|
transition: all .3s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--bg-tertiary-color-opacity);
|
||||||
|
color: var(--text-tertiary-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
.animateSpin{
|
.animateSpin{
|
||||||
animation: spin 1s linear infinite;
|
animation: spin 1s linear infinite;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,37 +0,0 @@
|
|||||||
import { Link, Outlet, useNavigate } from "react-router";
|
|
||||||
import styles from "./styles.module.css";
|
|
||||||
import Header from "../../components/header";
|
|
||||||
import Sidebar from "../../components/sidebar";
|
|
||||||
import Footer from "../../components/footer";
|
|
||||||
import { useEffect } from "react";
|
|
||||||
|
|
||||||
export default function ProtectedLayout() {
|
|
||||||
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const token = localStorage.getItem("token");
|
|
||||||
if (!token) {
|
|
||||||
navigate("/login");
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!token) {
|
|
||||||
navigate("/login");
|
|
||||||
}
|
|
||||||
}, [token]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className={styles.mainContainer}>
|
|
||||||
<Sidebar />
|
|
||||||
|
|
||||||
<section className={styles.contentArea}>
|
|
||||||
<Header />
|
|
||||||
<main className={styles.main}>
|
|
||||||
<Outlet />
|
|
||||||
</main>
|
|
||||||
{/* <Footer /> */}
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
import { useNavigate } from "react-router";
|
|
||||||
import { useEffect } from "react";
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export default function Home() {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<h1>Home</h1>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
.mainContainer{
|
|
||||||
display: flex;
|
|
||||||
width: 100%;
|
|
||||||
height: 100vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
.contentArea {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
height: 100vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
.main{
|
|
||||||
flex: 1;
|
|
||||||
background: var(--bg-grey);
|
|
||||||
overflow: auto;
|
|
||||||
padding: 20px;
|
|
||||||
justify-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
input:focus, input:active{
|
|
||||||
border: none;
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
@@ -1,169 +0,0 @@
|
|||||||
import { useEffect, useState } from "react";
|
|
||||||
import type { ApiErrorResponse, Category, Video } from "../../../types";
|
|
||||||
import { Link } from "react-router";
|
|
||||||
import styles from "./styles.module.css";
|
|
||||||
import { CgSpinner } from "react-icons/cg";
|
|
||||||
import { LuPlus } from "react-icons/lu";
|
|
||||||
import { LuPencil } from "react-icons/lu";
|
|
||||||
import { imageSkeletonFadeStyle, onImageSkeletonLoad } from "../../../../utils/imageSkeleton";
|
|
||||||
|
|
||||||
export default function Videos() {
|
|
||||||
const [loadingTimeout, setLoadingTimeout] = useState(false);
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
const [videos, setVideos] = useState<Video[]>([]);
|
|
||||||
const [error, setError] = useState<ApiErrorResponse | null>(null);
|
|
||||||
const [categories, setCategories] = useState<Category[]>([]);
|
|
||||||
const [selectedCategoryId, setSelectedCategoryId] = useState<string>("all");
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
getCategories();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
async function getCategories() {
|
|
||||||
const response = await fetch("http://127.0.0.1:8000/api/categories", {
|
|
||||||
method: "GET",
|
|
||||||
headers: {
|
|
||||||
Accept: "application/json",
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
Authorization: `Bearer ${localStorage.getItem("token")}`
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
setCategories(data.data);
|
|
||||||
setError(null);
|
|
||||||
} else {
|
|
||||||
setCategories([]);
|
|
||||||
setError(data as ApiErrorResponse);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if(!loading) return;
|
|
||||||
|
|
||||||
const timer = setTimeout(() => {
|
|
||||||
setLoadingTimeout(true);
|
|
||||||
}, 20000);
|
|
||||||
|
|
||||||
return () => clearTimeout(timer);
|
|
||||||
}, [loading]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
index();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
async function index() {
|
|
||||||
setLoading(true);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch("http://127.0.0.1:8000/api/videos", {
|
|
||||||
method: "GET",
|
|
||||||
headers: {
|
|
||||||
Accept: "application/json",
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
Authorization: `Bearer ${localStorage.getItem("token")}`
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
setVideos(data.data as Video[]);
|
|
||||||
setError(null);
|
|
||||||
} else {
|
|
||||||
setVideos([]);
|
|
||||||
setError(data as ApiErrorResponse);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (error) {
|
|
||||||
setVideos([]);
|
|
||||||
setError(error as ApiErrorResponse);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const filteredVideos = videos.filter((video) => {
|
|
||||||
if (selectedCategoryId === "all") return true;
|
|
||||||
if (selectedCategoryId === "active") return Boolean(video.is_active);
|
|
||||||
if (selectedCategoryId === "inactive") return !video.is_active;
|
|
||||||
|
|
||||||
return (video.categories ?? []).some(
|
|
||||||
(category) => String(category.id) === selectedCategoryId
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
if(loading) {
|
|
||||||
return(
|
|
||||||
<div className="text-center mt-5 d-flex flex-column gap-2 align-items-center">
|
|
||||||
<CgSpinner className={`${styles.animateSpin} text-2xl fs-3`} />
|
|
||||||
<span>A carregar os vídeos...</span>
|
|
||||||
</div>)
|
|
||||||
}
|
|
||||||
|
|
||||||
if(videos.length === 0) {
|
|
||||||
return (
|
|
||||||
<div className="text-center mt-5 d-flex flex-column gap-2 align-items-center">
|
|
||||||
<span>Nenhum vídeo encontrado</span>
|
|
||||||
<Link to="/create-video" className={`${styles.btnAdicionarVideo} text-decoration-none`}><LuPlus className="mb-1" />Adicionar vídeo</Link>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
return (
|
|
||||||
<div className={`${styles.container} p-4 p-lg-0`}>
|
|
||||||
<h1 className={styles.title}>Videos</h1>
|
|
||||||
|
|
||||||
<div className="row py-3 g-4">
|
|
||||||
<div className="col-12 col-sm-6 text-start">
|
|
||||||
<label htmlFor="filter" className="form-label fw-bold text-start">Filtrar vídeos</label>
|
|
||||||
<select className="form-control select-filter" name="filter" id="filter" value={selectedCategoryId} onChange={(e) => setSelectedCategoryId(e.target.value)}>
|
|
||||||
<option key="all" value="all">Todos</option>
|
|
||||||
<option key="active" value="active">Ativos</option>
|
|
||||||
<option key="inactive" value="inactive">Inativos</option>
|
|
||||||
{categories.map((category) => (
|
|
||||||
<option key={category.id} value={String(category.id)}>{category.name}</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div className="col-12 col-sm-6 text-center text-sm-end align-content-center">
|
|
||||||
<Link to="/create-video" className={`${styles.btnAdicionarVideo} text-decoration-none`}><LuPlus className="mb-1" />Adicionar vídeo</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<div className={`${styles.containerVideos} mt-3`}>
|
|
||||||
<div className="row g-3 p-0">
|
|
||||||
{filteredVideos.map((video) => (
|
|
||||||
<div className="col-12 col-sm-6 col-lg-4 col-xl-3 p-2 position-relative">
|
|
||||||
<div className={`${styles.boxVideo}`} data-category={video.categories?.map((category) => category.id).join(', ')}>
|
|
||||||
<Link to={`/edit-video/${video.id}`}> <LuPencil className={`${styles.iconEdit} text-decoration-none`} /> </Link>
|
|
||||||
<a className={`${styles.linkVideo} text-decoration-none pb-2`} href={`/video/${video.id}`} key={video.id} data-category={video.categories?.map((category) => category.name).join(', ')}>
|
|
||||||
<img
|
|
||||||
className={`${styles.thumbnail}`}
|
|
||||||
src={`http://127.0.0.1:8000/storage/${video.thumbnail}`}
|
|
||||||
alt={video.title}
|
|
||||||
style={imageSkeletonFadeStyle}
|
|
||||||
onLoad={onImageSkeletonLoad}
|
|
||||||
/>
|
|
||||||
<div className="px-3 text-start text-truncate">
|
|
||||||
<h2 className={`${styles.titleVideo} mt-2`}>{video.title}</h2>
|
|
||||||
<span className="text-muted text-black mt-0
|
|
||||||
">{video.description}</span>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
{filteredVideos.length === 0 && (
|
|
||||||
<div className="col-12 text-center mt-5">
|
|
||||||
<span className="text-muted fs-5">Nenhum vídeo encontrado com o filtro selecionado</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
.container{
|
|
||||||
width: 100%;
|
|
||||||
max-width: 1400px;
|
|
||||||
align-self: start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title{
|
|
||||||
color: var(--primary-contrast-color);
|
|
||||||
font-size: var(--size-font-title);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btnAdicionarVideo{
|
|
||||||
background-color: var(--primary-color);
|
|
||||||
color: var(--text-white);
|
|
||||||
border: none;
|
|
||||||
border-radius: var(--border-radius-button);
|
|
||||||
padding: 10px 20px;
|
|
||||||
font-size: var(--size-font-small);
|
|
||||||
|
|
||||||
font-weight: 500;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
&:hover{
|
|
||||||
background-color: var(--neutral-color);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.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: 30px;
|
|
||||||
right: 30px;
|
|
||||||
border-radius: var(--border-radius);
|
|
||||||
padding: 10px;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
&:hover{
|
|
||||||
background-color: var(--neutral-color);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.linkVideo{
|
|
||||||
display: block;
|
|
||||||
width: 100%;
|
|
||||||
border-radius: var(--border-radius);
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
background-color: var(--bg-white);
|
|
||||||
}
|
|
||||||
|
|
||||||
.boxVideo{
|
|
||||||
background: linear-gradient(90deg, #e0e0e0 25%, #f0f0f0 50%, #e0e0e0 75%);
|
|
||||||
background-size: 200% 100%;
|
|
||||||
animation: skeleton 1.5s infinite;
|
|
||||||
height: 200px;
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
border-radius: var(--border-radius);
|
|
||||||
}
|
|
||||||
|
|
||||||
.titleVideo{
|
|
||||||
color: var(--text-black);
|
|
||||||
font-size: var(--size-font-text);
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.thumbnail{
|
|
||||||
width: 100%;
|
|
||||||
object-fit: cover;
|
|
||||||
border-top-right-radius: var(--border-radius);
|
|
||||||
border-top-left-radius: var(--border-radius);
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
height: 150px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.animateSpin{
|
|
||||||
animation: spin 1s linear infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes skeleton {
|
|
||||||
0% { background-position: 200% 0; }
|
|
||||||
100% { background-position: -200% 0; }
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spin{
|
|
||||||
from{
|
|
||||||
transform: rotate(0deg);
|
|
||||||
}
|
|
||||||
to{
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,126 +0,0 @@
|
|||||||
import { useEffect, useState } from "react";
|
|
||||||
import type { ApiErrorResponse, Workshop } from "../../../../types";
|
|
||||||
import { data, Link } from "react-router";
|
|
||||||
import { CgSpinner } from "react-icons/cg";
|
|
||||||
import styles from "./styles.module.css";
|
|
||||||
import { LuPlus, LuCalendar, LuClock3 } from "react-icons/lu";
|
|
||||||
import { imageSkeletonFadeStyle, onImageSkeletonLoad } from "../../../../utils/imageSkeleton";
|
|
||||||
|
|
||||||
export default function Workshops() {
|
|
||||||
const [workshops, setWorkshops] = useState<Workshop[]>([]);
|
|
||||||
const [error, setError] = useState<ApiErrorResponse | null>(null);
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
const [selectedWorkshopStatus, setSelectedWorkshopStatus] = useState<string>("all");
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
getWorkshops();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
async function getWorkshops() {
|
|
||||||
setLoading(true);
|
|
||||||
|
|
||||||
const response = await fetch("http://127.0.0.1:8000/api/workshops", {
|
|
||||||
method: "GET",
|
|
||||||
headers: {
|
|
||||||
Accept: "application/json",
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
Authorization: `Bearer ${localStorage.getItem("token")}`
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if(response.ok) {
|
|
||||||
response.json().then((data) => {
|
|
||||||
setWorkshops(data.data as Workshop[]);
|
|
||||||
setError(null);
|
|
||||||
setLoading(false);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
response.json().then((data) => {
|
|
||||||
setError(data as ApiErrorResponse);
|
|
||||||
setLoading(false);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const filteredWorkshops = workshops.filter((workshop) => {
|
|
||||||
if (selectedWorkshopStatus === "all") return true;
|
|
||||||
if (selectedWorkshopStatus === "active") return true;
|
|
||||||
if (selectedWorkshopStatus === "inactive") return false;
|
|
||||||
});
|
|
||||||
|
|
||||||
if(loading) {
|
|
||||||
return (
|
|
||||||
<div className="text-center mt-5 d-flex flex-column gap-2 align-items-center">
|
|
||||||
<CgSpinner className={`${styles.animateSpin} text-2xl fs-3`} />
|
|
||||||
<span>A carregar os workshops...</span>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if(workshops.length === 0) {
|
|
||||||
return (
|
|
||||||
<div className="text-center mt-5 d-flex flex-column gap-2 align-items-center">
|
|
||||||
<span>Nenhum workshop encontrado</span>
|
|
||||||
<Link to="/create-workshop" className={`${styles.btnAdicionarWorkshop} text-decoration-none`}><LuPlus className="mb-1" />Adicionar workshop</Link>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
return (
|
|
||||||
<div className={`${styles.container} p-4 p-lg-0`}>
|
|
||||||
<h1 className={styles.title}>Workshops</h1>
|
|
||||||
|
|
||||||
<div className="row py-3 g-4">
|
|
||||||
<div className="col-12 col-sm-6 text-start px-2 ">
|
|
||||||
<label htmlFor="filter" className="form-label fw-bold text-start">Filtrar workshops</label>
|
|
||||||
<select className="form-control select-filter" name="filter" id="filter" value={selectedWorkshopStatus} onChange={(e) => setSelectedWorkshopStatus(e.target.value)}>
|
|
||||||
<option key="all" value="all">Todos</option>
|
|
||||||
<option key="active" value="active">Ativos</option>
|
|
||||||
<option key="inactive" value="inactive">Inativos</option>
|
|
||||||
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div className="col-12 col-sm-6 text-center text-sm-end align-content-center px-0">
|
|
||||||
<Link to="/create-workshop" className={`${styles.btnAdicionarWorkshop} text-decoration-none`}><LuPlus className="mb-1" />Adicionar workshop</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="row py-3 g-4">
|
|
||||||
{filteredWorkshops.map((workshop) => (
|
|
||||||
<div className="col-12 col-sm-6 col-lg-4 p-2 position-relative">
|
|
||||||
<div className={`${styles.boxWorkshop} text-start pb-3`}>
|
|
||||||
<div className={`${styles.thumbnailWorkshop} position-relative`}>
|
|
||||||
<img
|
|
||||||
src={`http://127.0.0.1:8000/storage/${workshop.image}`}
|
|
||||||
alt={workshop.title}
|
|
||||||
className={styles.thumbnailWorkshop}
|
|
||||||
style={imageSkeletonFadeStyle}
|
|
||||||
onLoad={onImageSkeletonLoad}
|
|
||||||
/>
|
|
||||||
<div className="position-absolute w-100 bottom-0 justify-content-evenly d-none d-md-flex d-xl-none d-xxl-flex mb-2">
|
|
||||||
<p className={`${styles.dateWorkshop}`}><LuCalendar className={`${styles.icon} mb-1 me-2`} />{workshop.date.split('-').reverse().join('-')}</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 className="px-3">
|
|
||||||
<h2 className={`${styles.titleWorkshop} d-block text-start mt-3`}>{workshop.title}</h2>
|
|
||||||
<p className={`${styles.descriptionWorkshop} text-truncate text-muted mt-0 mb-2`}>{workshop.description}</p>
|
|
||||||
<div className="position-relative d-flex flex-column d-md-none d-xl-flex d-xxl-none mt-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.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>
|
|
||||||
<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>
|
|
||||||
|
|
||||||
))}
|
|
||||||
{filteredWorkshops.length === 0 && (
|
|
||||||
<div className="col-12 text-center mt-5">
|
|
||||||
<span className="text-muted fs-5">Nenhum workshop encontrado com o filtro selecionado</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,123 +0,0 @@
|
|||||||
.container{
|
|
||||||
width: 100%;
|
|
||||||
max-width: 1400px;
|
|
||||||
align-self: start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title{
|
|
||||||
color: var(--primary-contrast-color);
|
|
||||||
font-size: var(--size-font-title);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btnAdicionarWorkshop{
|
|
||||||
background-color: var(--primary-color);
|
|
||||||
color: var(--text-white);
|
|
||||||
border: none;
|
|
||||||
border-radius: var(--border-radius-button);
|
|
||||||
padding: 10px 20px;
|
|
||||||
font-size: var(--size-font-small);
|
|
||||||
|
|
||||||
font-weight: 500;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
&:hover{
|
|
||||||
background-color: var(--neutral-color);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.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: 30px;
|
|
||||||
right: 30px;
|
|
||||||
border-radius: var(--border-radius);
|
|
||||||
padding: 10px;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
&:hover{
|
|
||||||
background-color: var(--neutral-color);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.linkWorkshop{
|
|
||||||
display: block;
|
|
||||||
width: fit-content;
|
|
||||||
border-radius: var(--border-radius);
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
font-weight: 600;
|
|
||||||
|
|
||||||
background-color: var(--bg-primary-color);
|
|
||||||
color: var(--text-white);
|
|
||||||
transition: all .3s ease;
|
|
||||||
&:hover{
|
|
||||||
background-color: var(--neutral-color);
|
|
||||||
color: var(--text-white);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.dateWorkshop, .timeWorkshop{
|
|
||||||
background-color: var(--bg-white);
|
|
||||||
font-size: var(--size-font-small);
|
|
||||||
font-weight: 700;
|
|
||||||
border-radius: var(--border-radius-button);
|
|
||||||
padding: 5px 10px;
|
|
||||||
width: fit-content;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dateWorkshopMobile, .timeWorkshopMobile{
|
|
||||||
font-size: var(--size-font-small);
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
.boxWorkshop{
|
|
||||||
height: fit-content;
|
|
||||||
max-height: 400px;
|
|
||||||
background-color: var(--bg-white);
|
|
||||||
border-radius: var(--border-radius);
|
|
||||||
}
|
|
||||||
|
|
||||||
.titleWorkshop{
|
|
||||||
color: var(--text-primary-color);
|
|
||||||
font-size: var(--size-font-text);
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.thumbnailWorkshop{
|
|
||||||
background: linear-gradient(90deg, #e0e0e0 25%, #f0f0f0 50%, #e0e0e0 75%);
|
|
||||||
background-size: 200% 100%;
|
|
||||||
animation: skeleton 1.5s infinite;
|
|
||||||
width: 100%;
|
|
||||||
object-fit: cover;
|
|
||||||
border-top-right-radius: var(--border-radius);
|
|
||||||
border-top-left-radius: var(--border-radius);
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
height: 150px;
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon{
|
|
||||||
color: var(--text-primary-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.animateSpin{
|
|
||||||
animation: spin 1s linear infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes skeleton {
|
|
||||||
0% { background-position: 200% 0; }
|
|
||||||
100% { background-position: -200% 0; }
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spin{
|
|
||||||
from{
|
|
||||||
transform: rotate(0deg);
|
|
||||||
}
|
|
||||||
to{
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -5,18 +5,19 @@ import styles from "./styles.module.css";
|
|||||||
import { CgSpinner } from "react-icons/cg";
|
import { CgSpinner } from "react-icons/cg";
|
||||||
import { LuPlus, LuSettings2 } from "react-icons/lu";
|
import { LuPlus, LuSettings2 } from "react-icons/lu";
|
||||||
import { LuPencil } from "react-icons/lu";
|
import { LuPencil } from "react-icons/lu";
|
||||||
import { Dropdown, Form, Pagination } from "react-bootstrap";
|
import { Form, Pagination } from "react-bootstrap";
|
||||||
import { useGetVideos } from "../../../hooks/useGetVideos";
|
import { useGetVideos } from "../../../hooks/useGetVideos";
|
||||||
import { useDebounce } from "../../../hooks/useDebounce";
|
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";
|
||||||
|
|
||||||
export default function Videos() {
|
export default function Videos() {
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState<ApiErrorResponse | null>(null);
|
const [error, setError] = useState<ApiErrorResponse | null>(null);
|
||||||
const [categories, setCategories] = useState<Category[]>([]);
|
const [categories, setCategories] = useState<Category[]>([]);
|
||||||
const [selectedCategoryId, setSelectedCategoryId] = useState<string>("all");
|
const [selectedCategoryId, setSelectedCategoryId] = useState<string>("all");
|
||||||
const [isAdmin, setIsAdmin] = useState(false);
|
const [showFilterDropdown, setShowFilterDropdown] = useState(false);
|
||||||
const { getVideos } = useGetVideos();
|
const { getVideos } = useGetVideos();
|
||||||
const [videos, setVideos] = useState<Video[]>([]);
|
const [videos, setVideos] = useState<Video[]>([]);
|
||||||
const [search, setSearch] = useState<string>("");
|
const [search, setSearch] = useState<string>("");
|
||||||
@@ -25,49 +26,7 @@ export default function Videos() {
|
|||||||
const [lastPage, setLastPage] = useState(1);
|
const [lastPage, setLastPage] = useState(1);
|
||||||
const [loadingVideos, setLoadingVideos] = useState(false);
|
const [loadingVideos, setLoadingVideos] = useState(false);
|
||||||
const videosToShow = videos;
|
const videosToShow = videos;
|
||||||
|
const [role, setRole] = useState(0);
|
||||||
useEffect(() => {
|
|
||||||
getRole();
|
|
||||||
getCategories();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
async function getRole() {
|
|
||||||
const response = await fetch("http://127.0.0.1:8000/api/me", {
|
|
||||||
method: "GET",
|
|
||||||
headers: {
|
|
||||||
Accept: "application/json",
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
Authorization: `Bearer ${localStorage.getItem("token")}`
|
|
||||||
},
|
|
||||||
|
|
||||||
});
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
setIsAdmin(data.data.role_id === 1);
|
|
||||||
} else {
|
|
||||||
setIsAdmin(false);
|
|
||||||
setError(data as ApiErrorResponse);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getCategories() {
|
|
||||||
const response = await fetch("http://127.0.0.1:8000/api/categories", {
|
|
||||||
method: "GET",
|
|
||||||
headers: {
|
|
||||||
Accept: "application/json",
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
Authorization: `Bearer ${localStorage.getItem("token")}`
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
setCategories(data.data);
|
|
||||||
} else {
|
|
||||||
setCategories([]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchVideos = async () => {
|
const fetchVideos = async () => {
|
||||||
@@ -103,6 +62,8 @@ export default function Videos() {
|
|||||||
setVideos(videosData.videos);
|
setVideos(videosData.videos);
|
||||||
setLastPage(videosData.meta.last_page);
|
setLastPage(videosData.meta.last_page);
|
||||||
setCurrentPage(videosData.meta.current_page);
|
setCurrentPage(videosData.meta.current_page);
|
||||||
|
setRole(videosData.role);
|
||||||
|
setCategories(videosData.categories);
|
||||||
} else {
|
} else {
|
||||||
setVideos([]);
|
setVideos([]);
|
||||||
}
|
}
|
||||||
@@ -145,42 +106,104 @@ export default function Videos() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div 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">
|
||||||
<Dropdown className="flex-grow-1" onSelect={(value) => {
|
<div
|
||||||
setCurrentPage(1);
|
className="btn-group flex-grow-1 position-relative"
|
||||||
if (value) {
|
onMouseEnter={() => setShowFilterDropdown(true)}
|
||||||
setSelectedCategoryId(value);
|
onMouseLeave={() => setShowFilterDropdown(false)}
|
||||||
}
|
>
|
||||||
}}>
|
<button type="button" className="btn btn-outline-secondary w-100 dropdown-toggle">
|
||||||
<Dropdown.Toggle variant="outline-secondary" className="w-100">
|
|
||||||
<LuSettings2 /> {selectedCategoryId === 'all' ? 'Todos' :
|
<LuSettings2 /> {selectedCategoryId === 'all' ? 'Todos' :
|
||||||
selectedCategoryId === 'active' ? 'Ativos' :
|
selectedCategoryId === 'active' ? 'Ativos' :
|
||||||
selectedCategoryId === 'inactive' ? 'Inativos' :
|
selectedCategoryId === 'inactive' ? 'Inativos' :
|
||||||
selectedCategoryId === 'watched' ? 'Vistos' :
|
selectedCategoryId === 'watched' ? 'Vistos' :
|
||||||
selectedCategoryId === 'unwatched' ? 'Não vistos' :
|
selectedCategoryId === 'unwatched' ? 'Não vistos' :
|
||||||
categories.find(c => String(c.id) === selectedCategoryId)?.name ?? 'Todos'}
|
categories.find(c => String(c.id) === selectedCategoryId)?.name ?? 'Todos'}
|
||||||
</Dropdown.Toggle>
|
</button>
|
||||||
<Dropdown.Menu style={{ zIndex: 4000 }} className="text-center w-100">
|
|
||||||
<Dropdown.Item eventKey="all" active={selectedCategoryId === 'all'}>Todos</Dropdown.Item>
|
<AnimatePresence>
|
||||||
{!isAdmin && (
|
{showFilterDropdown && (
|
||||||
<>
|
<motion.ul
|
||||||
<Dropdown.Item eventKey="watched" active={selectedCategoryId === 'watched'}>Vistos</Dropdown.Item>
|
initial={{ opacity: 0, y: -10, scale: 0.98 }}
|
||||||
<Dropdown.Item eventKey="unwatched" active={selectedCategoryId === 'unwatched'}>Não vistos</Dropdown.Item>
|
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||||
</>
|
exit={{ opacity: 0, y: -10, scale: 0.98 }}
|
||||||
|
transition={{ duration: 0.2, ease: "easeOut" }}
|
||||||
|
className="dropdown-menu text-center w-100"
|
||||||
|
style={{ display: "block", zIndex: 4000, top: "100%", marginTop: "0.25rem" }}
|
||||||
|
>
|
||||||
|
<li>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`dropdown-item ${selectedCategoryId === "all" ? "active" : ""}`}
|
||||||
|
onClick={() => { setSelectedCategoryId("all"); setCurrentPage(1); setShowFilterDropdown(false); }}
|
||||||
|
>
|
||||||
|
Todos
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
{role !== 1 && (
|
||||||
|
<>
|
||||||
|
<li>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`dropdown-item ${selectedCategoryId === "watched" ? "active" : ""}`}
|
||||||
|
onClick={() => { setSelectedCategoryId("watched"); setCurrentPage(1); setShowFilterDropdown(false); }}
|
||||||
|
>
|
||||||
|
Vistos
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`dropdown-item ${selectedCategoryId === "unwatched" ? "active" : ""}`}
|
||||||
|
onClick={() => { setSelectedCategoryId("unwatched"); setCurrentPage(1); setShowFilterDropdown(false); }}
|
||||||
|
>
|
||||||
|
Não vistos
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{role === 1 && (
|
||||||
|
<>
|
||||||
|
<li>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`dropdown-item ${selectedCategoryId === "active" ? "active" : ""}`}
|
||||||
|
onClick={() => { setSelectedCategoryId("active"); setCurrentPage(1); setShowFilterDropdown(false); }}
|
||||||
|
>
|
||||||
|
Ativos
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`dropdown-item ${selectedCategoryId === "inactive" ? "active" : ""}`}
|
||||||
|
onClick={() => { setSelectedCategoryId("inactive"); setCurrentPage(1); setShowFilterDropdown(false); }}
|
||||||
|
>
|
||||||
|
Inativos
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{categories.map((category) => (
|
||||||
|
<li key={category.id}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`dropdown-item ${selectedCategoryId === String(category.id) ? "active" : ""}`}
|
||||||
|
onClick={() => { setSelectedCategoryId(String(category.id)); setCurrentPage(1); setShowFilterDropdown(false); }}
|
||||||
|
>
|
||||||
|
{category.name}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</motion.ul>
|
||||||
)}
|
)}
|
||||||
{isAdmin === true && (
|
</AnimatePresence>
|
||||||
<>
|
</div>
|
||||||
<Dropdown.Item eventKey="active" active={selectedCategoryId === 'active'}>Ativos</Dropdown.Item>
|
|
||||||
<Dropdown.Item eventKey="inactive" active={selectedCategoryId === 'inactive'}>Inativos</Dropdown.Item>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{categories.map((category) => (
|
|
||||||
<Dropdown.Item key={category.id} eventKey={String(category.id)} active={selectedCategoryId === String(category.id)}>{category.name}</Dropdown.Item>
|
|
||||||
))}
|
|
||||||
</Dropdown.Menu>
|
|
||||||
</Dropdown>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isAdmin && (
|
{role === 1 && (
|
||||||
<div className="col-12 col-sm col-md-12 col-lg-3 text-end align-content-center mt-4 mt-lg-4">
|
<div 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>
|
</div>
|
||||||
@@ -200,7 +223,7 @@ export default function Videos() {
|
|||||||
<div className="col-12 col-sm-6 col-lg-4 p-2">
|
<div 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 && (
|
{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>
|
||||||
)}
|
)}
|
||||||
<img
|
<img
|
||||||
|
|||||||
@@ -132,7 +132,6 @@ export default function Workshop() {
|
|||||||
return (
|
return (
|
||||||
<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">
|
||||||
<CgSpinner className={`${styles.animateSpin} text-2xl fs-3`} />
|
<CgSpinner className={`${styles.animateSpin} text-2xl fs-3`} />
|
||||||
<span>A carregar dados do workshop...</span>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,27 +5,25 @@ import { CgSpinner } from "react-icons/cg";
|
|||||||
import styles from "./styles.module.css";
|
import styles from "./styles.module.css";
|
||||||
import { LuPlus, LuCalendar, LuClock3, LuSettings2 } from "react-icons/lu";
|
import { LuPlus, LuCalendar, LuClock3, LuSettings2 } from "react-icons/lu";
|
||||||
import { useGetWorkshops } from "../../../hooks/useGetWorkshops";
|
import { useGetWorkshops } from "../../../hooks/useGetWorkshops";
|
||||||
import { useGetCurrentUser } from "../../../hooks/useGetCurrentUser";
|
|
||||||
import { useDebounce } from "../../../hooks/useDebounce";
|
import { useDebounce } from "../../../hooks/useDebounce";
|
||||||
import Swal from "sweetalert2";
|
import Swal from "sweetalert2";
|
||||||
import { Dropdown, 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";
|
||||||
|
|
||||||
export default function Workshops() {
|
export default function Workshops() {
|
||||||
const user = JSON.parse(localStorage.getItem("user") || "{}") as User;
|
|
||||||
const isAdmin = user.role_id === 1;
|
|
||||||
|
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [selectedWorkshopStatus, setSelectedWorkshopStatus] = useState<string>("pending");
|
const [selectedWorkshopStatus, setSelectedWorkshopStatus] = useState<string>("pending");
|
||||||
|
const [showFilterDropdown, setShowFilterDropdown] = useState(false);
|
||||||
const { getWorkshops } = useGetWorkshops();
|
const { getWorkshops } = useGetWorkshops();
|
||||||
const [workshops, setWorkshops] = useState<Workshop[]>([]);
|
const [workshops, setWorkshops] = useState<Workshop[]>([]);
|
||||||
const { getCurrentUser } = useGetCurrentUser();
|
|
||||||
const [currentUserData, setCurrentUserData] = useState<User | null>(null);
|
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
const [search, setSearch] = useState<string>("");
|
const [search, setSearch] = useState<string>("");
|
||||||
const debouncedSearch = useDebounce(search, 500);
|
const debouncedSearch = useDebounce(search, 500);
|
||||||
const [lastPage, setLastPage] = useState(1);
|
const [lastPage, setLastPage] = useState(1);
|
||||||
const [loadingWorkshops, setLoadingWorkshops] = useState(false);
|
const [loadingWorkshops, setLoadingWorkshops] = useState(false);
|
||||||
|
const [role, setRole] = useState(0);
|
||||||
|
const [userId, setUserId] = useState(0);
|
||||||
/* const [searchLoading, setSearchLoading] = useState(false); */
|
/* const [searchLoading, setSearchLoading] = useState(false); */
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -33,9 +31,6 @@ export default function Workshops() {
|
|||||||
setLoadingWorkshops(true);
|
setLoadingWorkshops(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const currentUserData = await getCurrentUser();
|
|
||||||
setCurrentUserData(currentUserData.data);
|
|
||||||
|
|
||||||
const workshopsData = await getWorkshops({
|
const workshopsData = await getWorkshops({
|
||||||
page: currentPage,
|
page: currentPage,
|
||||||
search: debouncedSearch,
|
search: debouncedSearch,
|
||||||
@@ -44,6 +39,8 @@ export default function Workshops() {
|
|||||||
|
|
||||||
if ("workshops" in workshopsData) {
|
if ("workshops" in workshopsData) {
|
||||||
setWorkshops(workshopsData.workshops);
|
setWorkshops(workshopsData.workshops);
|
||||||
|
setUserId(workshopsData.userId);
|
||||||
|
setRole(workshopsData.role);
|
||||||
setLastPage(workshopsData.meta.last_page);
|
setLastPage(workshopsData.meta.last_page);
|
||||||
setCurrentPage(workshopsData.meta.current_page);
|
setCurrentPage(workshopsData.meta.current_page);
|
||||||
setLoadingWorkshops(false);
|
setLoadingWorkshops(false);
|
||||||
@@ -149,7 +146,6 @@ export default function Workshops() {
|
|||||||
return (
|
return (
|
||||||
<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">
|
||||||
<CgSpinner className={`${styles.animateSpin} text-2xl fs-3`} />
|
<CgSpinner className={`${styles.animateSpin} text-2xl fs-3`} />
|
||||||
<span>A carregar workshops...</span>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -167,32 +163,72 @@ export default function Workshops() {
|
|||||||
</div>
|
</div>
|
||||||
<div 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">
|
||||||
{/* <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> */}
|
||||||
<Dropdown className="flex-grow-1" onSelect={(value) => {
|
<div
|
||||||
setCurrentPage(1);
|
className="btn-group flex-grow-1 position-relative"
|
||||||
if (value) setSelectedWorkshopStatus(value);
|
onMouseEnter={() => setShowFilterDropdown(true)}
|
||||||
}}>
|
onMouseLeave={() => setShowFilterDropdown(false)}
|
||||||
<Dropdown.Toggle variant="outline-secondary" className="w-100 text-center" >
|
>
|
||||||
|
<button type="button" className="btn btn-outline-secondary w-100 dropdown-toggle text-center">
|
||||||
<LuSettings2 /> {selectedWorkshopStatus === 'pending' ? 'Agendados' : selectedWorkshopStatus === 'inscrito' ? 'Inscrito' : selectedWorkshopStatus === 'realized' ? 'Realizados' : 'Cancelados'}
|
<LuSettings2 /> {selectedWorkshopStatus === 'pending' ? 'Agendados' : selectedWorkshopStatus === 'inscrito' ? 'Inscrito' : selectedWorkshopStatus === 'realized' ? 'Realizados' : 'Cancelados'}
|
||||||
</Dropdown.Toggle>
|
</button>
|
||||||
<Dropdown.Menu className="text-center w-100" >
|
|
||||||
<Dropdown.Item eventKey="pending" active={selectedWorkshopStatus === 'pending'}>
|
<AnimatePresence>
|
||||||
Agendados
|
{showFilterDropdown && (
|
||||||
</Dropdown.Item>
|
<motion.ul
|
||||||
{!isAdmin && (
|
initial={{ opacity: 0, y: -10, scale: 0.98 }}
|
||||||
<Dropdown.Item eventKey="inscrito" active={selectedWorkshopStatus === 'inscrito'}>
|
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||||
Inscrito
|
exit={{ opacity: 0, y: -10, scale: 0.98 }}
|
||||||
</Dropdown.Item>
|
transition={{ duration: 0.2, ease: "easeOut" }}
|
||||||
|
className="dropdown-menu text-center w-100"
|
||||||
|
style={{ display: "block", zIndex: 4000, top: "100%", marginTop: "0.25rem" }}
|
||||||
|
>
|
||||||
|
<li>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`dropdown-item ${selectedWorkshopStatus === "pending" ? "active" : ""}`}
|
||||||
|
onClick={() => { setSelectedWorkshopStatus("pending"); setCurrentPage(1); setShowFilterDropdown(false); }}
|
||||||
|
>
|
||||||
|
Agendados
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
{role !== 1 && (
|
||||||
|
<li>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`dropdown-item ${selectedWorkshopStatus === "inscrito" ? "active" : ""}`}
|
||||||
|
onClick={() => { setSelectedWorkshopStatus("inscrito"); setCurrentPage(1); setShowFilterDropdown(false); }}
|
||||||
|
>
|
||||||
|
Inscrito
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<li>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`dropdown-item ${selectedWorkshopStatus === "realized" ? "active" : ""}`}
|
||||||
|
onClick={() => { setSelectedWorkshopStatus("realized"); setCurrentPage(1); setShowFilterDropdown(false); }}
|
||||||
|
>
|
||||||
|
Realizados
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`dropdown-item ${selectedWorkshopStatus === "canceled" ? "active" : ""}`}
|
||||||
|
onClick={() => { setSelectedWorkshopStatus("canceled"); setCurrentPage(1); setShowFilterDropdown(false); }}
|
||||||
|
>
|
||||||
|
Cancelados
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</motion.ul>
|
||||||
)}
|
)}
|
||||||
<Dropdown.Item eventKey="realized" active={selectedWorkshopStatus === 'realized'}>
|
</AnimatePresence>
|
||||||
Realizados
|
</div>
|
||||||
</Dropdown.Item>
|
|
||||||
<Dropdown.Item eventKey="canceled" active={selectedWorkshopStatus === 'canceled'}>
|
|
||||||
Cancelados
|
|
||||||
</Dropdown.Item>
|
|
||||||
</Dropdown.Menu>
|
|
||||||
</Dropdown>
|
|
||||||
</div>
|
</div>
|
||||||
{isAdmin ? (
|
{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' }}>
|
<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' }}>
|
||||||
<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>
|
</div>
|
||||||
@@ -217,6 +253,11 @@ export default function Workshops() {
|
|||||||
style={imageSkeletonFadeStyle}
|
style={imageSkeletonFadeStyle}
|
||||||
onLoad={onImageSkeletonLoad}
|
onLoad={onImageSkeletonLoad}
|
||||||
/>
|
/>
|
||||||
|
{(workshop.status === "realized" || workshop.status === "canceled") && (
|
||||||
|
<span className={`${styles.workshopStatus} position-absolute top-0 end-0 m-2 badge ${workshop.status === "realized" ? "bg-success" : "bg-danger"}`}>
|
||||||
|
{workshop.status === "realized" ? "Realizado" : "Cancelado"}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
<div className="position-absolute w-100 bottom-0 justify-content-evenly d-none d-md-flex d-xl-none d-xxl-flex mb-2">
|
<div className="position-absolute w-100 bottom-0 justify-content-evenly d-none d-md-flex d-xl-none d-xxl-flex mb-2">
|
||||||
<p className={`${styles.dateWorkshop}`}><LuCalendar className={`${styles.icon} mb-1 me-2`} />{workshop.date.split('-').reverse().join('-')}</p>
|
<p className={`${styles.dateWorkshop}`}><LuCalendar className={`${styles.icon} mb-1 me-2`} />{workshop.date.split('-').reverse().join('-')}</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>
|
<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>
|
||||||
@@ -231,21 +272,11 @@ export default function Workshops() {
|
|||||||
</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">
|
||||||
{workshop.status === "realized" ? (
|
{workshop.status === "realized" || workshop.status === "canceled" ? (
|
||||||
<>
|
role === 1 && (
|
||||||
<span className="text-success fw-bold text-center py-2 mb-0">Workshop realizado</span>
|
<Link to={`/admin/edit-workshop/${workshop.id}`} className={`${styles.linkWorkshop} d-block text-center py-2 px-5 text-decoration-none`}>Detalhes</Link>
|
||||||
{isAdmin && (
|
)
|
||||||
<Link to={`/admin/edit-workshop/${workshop.id}`} className={`${styles.linkWorkshop} d-block text-center py-2 px-5 text-decoration-none`}>Detalhes</Link>
|
) : workshop.status === "pending" && role !== 1 && workshop.users.some((u: User) => u.id === userId) ? (
|
||||||
)}
|
|
||||||
</>
|
|
||||||
) : workshop.status === "canceled" ? (
|
|
||||||
<>
|
|
||||||
<span className="text-danger fw-bold text-center py-2 mb-0">Workshop cancelado</span>
|
|
||||||
{isAdmin && (
|
|
||||||
<Link to={`/admin/edit-workshop/${workshop.id}`} className={`${styles.linkWorkshop} d-block text-center py-2 px-5 text-decoration-none`}>Detalhes</Link>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
) : workshop.status === "pending" && currentUserData && workshop.users.some((u: User) => u.id === currentUserData.id) ? (
|
|
||||||
<>
|
<>
|
||||||
<Link to={`/workshop/${workshop.id}`} className={`${styles.linkWorkshop} d-block text-center py-2 px-5 text-decoration-none`}>Detalhes</Link>
|
<Link to={`/workshop/${workshop.id}`} className={`${styles.linkWorkshop} d-block text-center py-2 px-5 text-decoration-none`}>Detalhes</Link>
|
||||||
<button type="button" className={`${styles.btncancelarInscricao} btn text-center py-2 text-decoration-none`} onClick={() => {
|
<button type="button" className={`${styles.btncancelarInscricao} btn text-center py-2 text-decoration-none`} onClick={() => {
|
||||||
@@ -258,9 +289,9 @@ export default function Workshops() {
|
|||||||
Anular inscrição
|
Anular inscrição
|
||||||
</button>
|
</button>
|
||||||
</>
|
</>
|
||||||
) : isAdmin && workshop.status === "pending" ? (
|
) : role === 1 && workshop.status === "pending" ? (
|
||||||
<Link to={`/admin/edit-workshop/${workshop.id}`} className={`${styles.linkWorkshop} d-block text-center py-2 px-5 text-decoration-none`}>Detalhes</Link>
|
<Link to={`/admin/edit-workshop/${workshop.id}`} className={`${styles.linkWorkshop} d-block text-center py-2 px-5 text-decoration-none`}>Detalhes</Link>
|
||||||
) : workshop.status === "pending" && !isAdmin ? (
|
) : workshop.status === "pending" && role !== 1 ? (
|
||||||
<>
|
<>
|
||||||
<Link to={`/workshop/${workshop.id}`} className={`${styles.linkWorkshop} d-block text-center py-2 px-5 text-decoration-none`}>Detalhes</Link>
|
<Link to={`/workshop/${workshop.id}`} className={`${styles.linkWorkshop} d-block text-center py-2 px-5 text-decoration-none`}>Detalhes</Link>
|
||||||
<button type="button" className={`${styles.btnInscrever} btn text-center py-2 px-4 text-decoration-none`} onClick={() => {
|
<button type="button" className={`${styles.btnInscrever} btn text-center py-2 px-4 text-decoration-none`} onClick={() => {
|
||||||
|
|||||||
@@ -135,6 +135,10 @@
|
|||||||
color: var(--text-primary-color);
|
color: var(--text-primary-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.workshopStatus {
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
.animateSpin{
|
.animateSpin{
|
||||||
animation: spin 1s linear infinite;
|
animation: spin 1s linear infinite;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useEffect, useState, type FormEvent } from "react";
|
import { useEffect, useState, type FormEvent } from "react";
|
||||||
import { useNavigate } from "react-router";
|
import { useNavigate } from "react-router";
|
||||||
import type { ApiErrorResponse, LoginResponse, User } from "../../../types";
|
import type { ApiErrorResponse, LoginResponse } from "../../../types";
|
||||||
import styles from "./styles.module.css";
|
import styles from "./styles.module.css";
|
||||||
import { CgSpinner } from "react-icons/cg";
|
import { CgSpinner } from "react-icons/cg";
|
||||||
|
|
||||||
@@ -48,7 +48,6 @@ export default function Login() {
|
|||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
localStorage.setItem("token", data.access_token as string);
|
localStorage.setItem("token", data.access_token as string);
|
||||||
localStorage.setItem("user", JSON.stringify(data.user as User));
|
|
||||||
navigate("/dashboard");
|
navigate("/dashboard");
|
||||||
} else {
|
} else {
|
||||||
setError(data as ApiErrorResponse);
|
setError(data as ApiErrorResponse);
|
||||||
|
|||||||
@@ -7,6 +7,9 @@ export default function Logout() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
localStorage.removeItem("token");
|
localStorage.removeItem("token");
|
||||||
|
localStorage.removeItem("user");
|
||||||
|
localStorage.removeItem("videosWatched");
|
||||||
|
localStorage.removeItem("videosCount");
|
||||||
navigate("/login", { replace: true }); /* Replace é para evitar que o usuário possa voltar para a página de logout */
|
navigate("/login", { replace: true }); /* Replace é para evitar que o usuário possa voltar para a página de logout */
|
||||||
}, [navigate]);
|
}, [navigate]);
|
||||||
|
|
||||||
|
|||||||
@@ -21,20 +21,20 @@ export type ApiErrorResponse = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type User = {
|
export type User = {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
email: string;
|
email: string;
|
||||||
role_id: number;
|
role_id: number;
|
||||||
password: string;
|
password: string;
|
||||||
created_at: string;
|
|
||||||
updated_at: string;
|
|
||||||
allowed_access?: boolean;
|
|
||||||
pivot?: {
|
|
||||||
workshop_id: number;
|
|
||||||
user_id: number;
|
|
||||||
created_at: string;
|
created_at: string;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
};
|
allowed_access?: boolean;
|
||||||
|
pivot?: {
|
||||||
|
workshop_id: number;
|
||||||
|
user_id: number;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UpdateUserResponse = {
|
export type UpdateUserResponse = {
|
||||||
@@ -73,6 +73,8 @@ export type Video = {
|
|||||||
is_active: boolean;
|
is_active: boolean;
|
||||||
order: number;
|
order: number;
|
||||||
watched: boolean;
|
watched: boolean;
|
||||||
|
users: User[];
|
||||||
|
role: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type NextVideosResponse = {
|
export type NextVideosResponse = {
|
||||||
@@ -84,6 +86,16 @@ export type UpdateVideoResponse = {
|
|||||||
data?: Video;
|
data?: Video;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type DashboardResponse = {
|
||||||
|
videos: Video[];
|
||||||
|
videosWatched: number;
|
||||||
|
videosCount: number;
|
||||||
|
workshops: Workshop[];
|
||||||
|
workshopsInscribed: number;
|
||||||
|
workshopsCount: number;
|
||||||
|
role: number;
|
||||||
|
}
|
||||||
|
|
||||||
export type GetVideosResponse = {
|
export type GetVideosResponse = {
|
||||||
message?: string;
|
message?: string;
|
||||||
data?: Video[];
|
data?: Video[];
|
||||||
@@ -112,6 +124,7 @@ export type Workshop = {
|
|||||||
time_end: string;
|
time_end: string;
|
||||||
is_active: boolean;
|
is_active: boolean;
|
||||||
users: User[];
|
users: User[];
|
||||||
|
role: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type NextWorkshopsResponse = {
|
export type NextWorkshopsResponse = {
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use App\Models\Video;
|
||||||
|
use App\Models\Workshop;
|
||||||
|
|
||||||
|
class DashboardController extends Controller
|
||||||
|
{
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
$user = auth()->user();
|
||||||
|
|
||||||
|
if (!$user) {
|
||||||
|
return response()->json([
|
||||||
|
'message' => 'Utilizador não autenticado',
|
||||||
|
'data' => null,
|
||||||
|
'errors' => null,
|
||||||
|
], 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$userId = $user->id;
|
||||||
|
$role = $user->role_id;
|
||||||
|
|
||||||
|
|
||||||
|
$videos = Video::select('id', 'title', 'thumbnail', 'is_active', 'order')
|
||||||
|
->where('is_active', true)
|
||||||
|
->whereDoesntHave('views', function ($q) use ($user) {
|
||||||
|
$q->where('user_id', $user->id);
|
||||||
|
})
|
||||||
|
->orderBy('order', 'asc')
|
||||||
|
->limit(3)
|
||||||
|
->get()
|
||||||
|
->map(function ($video) {
|
||||||
|
return [
|
||||||
|
'id' => $video->id,
|
||||||
|
'title' => $video->title,
|
||||||
|
'thumbnail' => $video->thumbnail,
|
||||||
|
'is_active' => $video->is_active,
|
||||||
|
'watched' => false,
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
$videosCount = Video::select('id')->where('is_active', true)->count();
|
||||||
|
|
||||||
|
$videosWatched = Video::select('id')
|
||||||
|
->where('is_active', true)
|
||||||
|
->whereHas('views', function ($query) use ($user) {
|
||||||
|
$query->where('user_id', $user->id);
|
||||||
|
})->count();
|
||||||
|
|
||||||
|
$workshops = Workshop::select(['id', 'title', 'image', 'date', 'time_start', 'time_end', 'status'])
|
||||||
|
->with(['users:id'])
|
||||||
|
->where('status', 'pending')
|
||||||
|
->orderBy('date', 'asc')
|
||||||
|
->orderBy('time_start', 'asc')
|
||||||
|
->limit(3)
|
||||||
|
->get()
|
||||||
|
->map(function ($workshop) {
|
||||||
|
return [
|
||||||
|
'id' => $workshop->id,
|
||||||
|
'title' => $workshop->title,
|
||||||
|
'image' => $workshop->image,
|
||||||
|
'date' => $workshop->date,
|
||||||
|
'time_start' => $workshop->time_start,
|
||||||
|
'time_end' => $workshop->time_end,
|
||||||
|
'status' => $workshop->status,
|
||||||
|
'users' => $workshop->users->pluck('id'),
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
$workshopsInscribed = Workshop::select('id')
|
||||||
|
->where('status', 'pending')
|
||||||
|
->whereHas('users', function ($query) use ($user) {
|
||||||
|
$query->where('user_id', $user->id);
|
||||||
|
})->count();
|
||||||
|
|
||||||
|
$workshopsCount = Workshop::select('id')->where('status', 'pending')->count();
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'videos' => $videos,
|
||||||
|
'videosWatched' => $videosWatched,
|
||||||
|
'videosCount' => $videosCount,
|
||||||
|
'workshops' => $workshops,
|
||||||
|
'workshopsCount' => $workshopsCount,
|
||||||
|
'workshopsInscribed' => $workshopsInscribed,
|
||||||
|
'userId' => $userId,
|
||||||
|
'role' => $role,
|
||||||
|
], 200);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,6 +7,8 @@ use Illuminate\Http\Request;
|
|||||||
use Illuminate\Support\Facades\Hash;
|
use Illuminate\Support\Facades\Hash;
|
||||||
use App\Http\Requests\CreateUserRequest;
|
use App\Http\Requests\CreateUserRequest;
|
||||||
use App\Http\Requests\UpdateUserRequest;
|
use App\Http\Requests\UpdateUserRequest;
|
||||||
|
use App\Models\Workshop;
|
||||||
|
use App\Models\Video;
|
||||||
|
|
||||||
class UserController extends Controller
|
class UserController extends Controller
|
||||||
{
|
{
|
||||||
@@ -22,7 +24,9 @@ class UserController extends Controller
|
|||||||
], 404);
|
], 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($user->role_id === 1) {
|
$role = $user->role_id;
|
||||||
|
|
||||||
|
if ($role === 1) {
|
||||||
$search = trim((string) $request->query('search', ''));
|
$search = trim((string) $request->query('search', ''));
|
||||||
$filter = $request->query('filter', 'all');
|
$filter = $request->query('filter', 'all');
|
||||||
$usersQuery = User::query();
|
$usersQuery = User::query();
|
||||||
@@ -30,7 +34,7 @@ class UserController extends Controller
|
|||||||
if ($search !== '') {
|
if ($search !== '') {
|
||||||
$usersQuery->where(function ($q) use ($search) {
|
$usersQuery->where(function ($q) use ($search) {
|
||||||
$q->where('name', 'like', "%{$search}%")
|
$q->where('name', 'like', "%{$search}%")
|
||||||
->orWhere('email', 'like', "%{$search}%");
|
->orWhere('email', 'like', "%{$search}%");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,6 +49,7 @@ class UserController extends Controller
|
|||||||
return response()->json([
|
return response()->json([
|
||||||
'message' => 'Utilizadores obtidos com sucesso',
|
'message' => 'Utilizadores obtidos com sucesso',
|
||||||
'data' => $users,
|
'data' => $users,
|
||||||
|
'role' => $role,
|
||||||
'errors' => null,
|
'errors' => null,
|
||||||
], 200);
|
], 200);
|
||||||
} else {
|
} else {
|
||||||
@@ -58,6 +63,7 @@ class UserController extends Controller
|
|||||||
|
|
||||||
public function getUser($id)
|
public function getUser($id)
|
||||||
{
|
{
|
||||||
|
|
||||||
$user = User::find($id);
|
$user = User::find($id);
|
||||||
|
|
||||||
if (!$user) {
|
if (!$user) {
|
||||||
@@ -68,10 +74,116 @@ class UserController extends Controller
|
|||||||
], 404);
|
], 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$videosWatched = Video::select('id')
|
||||||
|
->whereHas('views', function ($query) use ($user) {
|
||||||
|
$query->where('user_id', $user->id);
|
||||||
|
})->count();
|
||||||
|
|
||||||
|
$videosCount = Video::select('id')->where('is_active', true)->count();
|
||||||
|
|
||||||
|
$workshopsCount = Workshop::select('id')->where('status', 'pending')->count();
|
||||||
|
|
||||||
|
$workshopsInscribed = Workshop::select('id')
|
||||||
|
->where('status', 'pending')
|
||||||
|
->whereHas('users', function ($query) use ($user) {
|
||||||
|
$query->where('users.id', $user->id);
|
||||||
|
})->count();
|
||||||
|
|
||||||
|
$nextWorkshops = Workshop::select('id', 'title', 'image', 'date', 'time_start', 'time_end', 'status')->with('users:id')->where('status', 'pending')->orderBy('date', 'asc')->orderBy('time_start', 'asc')->limit(3)->get();
|
||||||
|
|
||||||
|
$workshopsParticipated = Workshop::select('id', 'title', 'image', 'date', 'time_start', 'time_end', 'status')->with('users:id')->whereHas('users', function ($query) use ($user) {
|
||||||
|
$query->where('users.id', $user->id);
|
||||||
|
})->get();
|
||||||
|
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'message' => 'Utilizador obtido com sucesso',
|
'message' => 'Utilizador obtido com sucesso',
|
||||||
'data' => $user,
|
'data' => $user,
|
||||||
'errors' => null,
|
'errors' => null,
|
||||||
|
'videosWatched' => $videosWatched,
|
||||||
|
'videosCount' => $videosCount,
|
||||||
|
'workshopsInscribed' => $workshopsInscribed,
|
||||||
|
'nextWorkshops' => $nextWorkshops,
|
||||||
|
'workshopsCount' => $workshopsCount,
|
||||||
|
'workshopsParticipated' => $workshopsParticipated,
|
||||||
|
], 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function profile()
|
||||||
|
{
|
||||||
|
$user = auth()->user();
|
||||||
|
|
||||||
|
if (!$user) {
|
||||||
|
return response()->json([
|
||||||
|
'message' => 'Utilizador não autenticado',
|
||||||
|
'data' => null,
|
||||||
|
'errors' => null,
|
||||||
|
], 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$role = $user->role_id;
|
||||||
|
$userId = $user->id;
|
||||||
|
|
||||||
|
if ($role === 1) {
|
||||||
|
$nextWorkshops = Workshop::select('id', 'title', 'image', 'date', 'time_start', 'time_end', 'status')->where('status', 'pending')->orderBy('date', 'asc')->orderBy('time_start', 'asc')->limit(3)->get();
|
||||||
|
$workshopsCount = Workshop::select('id')->where('status', 'pending')->count();
|
||||||
|
|
||||||
|
$videos = Video::select('id', 'title', 'thumbnail', 'is_active', 'order')->where('is_active', true)->orderBy('order', 'asc')->limit(3)->get()->map(function ($video) {
|
||||||
|
return [
|
||||||
|
'id' => $video->id,
|
||||||
|
'title' => $video->title,
|
||||||
|
'thumbnail' => $video->thumbnail,
|
||||||
|
'is_active' => $video->is_active,
|
||||||
|
'order' => $video->order,
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
$videosCount = Video::select('id')->where('is_active', true)->count();
|
||||||
|
} else {
|
||||||
|
$videos = Video::select('id', 'title', 'thumbnail', 'is_active', 'order')
|
||||||
|
->where('is_active', true)
|
||||||
|
->whereDoesntHave('views', function ($q) use ($user) {
|
||||||
|
$q->where('user_id', $user->id);
|
||||||
|
})
|
||||||
|
->orderBy('order', 'asc')
|
||||||
|
->limit(3)
|
||||||
|
->get()
|
||||||
|
->map(function ($video) {
|
||||||
|
return [
|
||||||
|
'id' => $video->id,
|
||||||
|
'title' => $video->title,
|
||||||
|
'thumbnail' => $video->thumbnail,
|
||||||
|
'is_active' => $video->is_active,
|
||||||
|
'watched' => false,
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
$videosCount = Video::select('id')->where('is_active', true)->count();
|
||||||
|
|
||||||
|
$videosWatched = Video::select('id')
|
||||||
|
->where('is_active', true)
|
||||||
|
->whereHas('views', function ($query) use ($user) {
|
||||||
|
$query->where('user_id', $user->id);
|
||||||
|
})->count();
|
||||||
|
|
||||||
|
$workshopsInscribed = Workshop::select('id', 'title', 'image', 'date', 'time_start', 'time_end', 'status')->with('users:id')->where('status', 'pending')->whereHas('users', function ($query) use ($user) {
|
||||||
|
$query->where('users.id', $user->id);
|
||||||
|
})->get();
|
||||||
|
|
||||||
|
$workshopsCount = Workshop::select('id')->where('status', 'pending')->count();
|
||||||
|
}
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'message' => 'Utilizador obtido com sucesso',
|
||||||
|
'data' => $user,
|
||||||
|
'videos' => $videos,
|
||||||
|
'videosCount' => $videosCount ?? 0,
|
||||||
|
'videosWatched' => $videosWatched ?? 0,
|
||||||
|
'workshopsInscribed' => $workshopsInscribed ?? [],
|
||||||
|
'workshopsCount' => $workshopsCount ?? 0,
|
||||||
|
'nextWorkshops' => $nextWorkshops ?? [],
|
||||||
|
'userId' => $userId,
|
||||||
|
'role' => $role,
|
||||||
|
'errors' => null,
|
||||||
], 200);
|
], 200);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,7 +281,7 @@ class UserController extends Controller
|
|||||||
$userUpdated = User::find($id);
|
$userUpdated = User::find($id);
|
||||||
|
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'message' => 'Utilizador atualizado com sucesso',
|
'message' => 'Dados atualizados com sucesso',
|
||||||
'data' => $userUpdated,
|
'data' => $userUpdated,
|
||||||
'errors' => null,
|
'errors' => null,
|
||||||
], 201);
|
], 201);
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ use Illuminate\Http\Request;
|
|||||||
use App\Http\Requests\CreateVideoRequest;
|
use App\Http\Requests\CreateVideoRequest;
|
||||||
use App\Http\Requests\UpdateVideoRequest;
|
use App\Http\Requests\UpdateVideoRequest;
|
||||||
use Illuminate\Support\Facades\Storage;
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
use App\Models\Category;
|
||||||
|
|
||||||
class VideosController extends Controller
|
class VideosController extends Controller
|
||||||
{
|
{
|
||||||
@@ -33,6 +34,9 @@ class VideosController extends Controller
|
|||||||
], 401);
|
], 401);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$userId = $user->id;
|
||||||
|
$role = $user->role_id;
|
||||||
|
|
||||||
$search = trim((string) $request->query('search', ''));
|
$search = trim((string) $request->query('search', ''));
|
||||||
$categoryId = $request->query('category');
|
$categoryId = $request->query('category');
|
||||||
$watched = $request->query('watched');
|
$watched = $request->query('watched');
|
||||||
@@ -43,7 +47,7 @@ class VideosController extends Controller
|
|||||||
'categories:id,name',
|
'categories:id,name',
|
||||||
'views' => function ($q) use ($user) {
|
'views' => function ($q) use ($user) {
|
||||||
$q->select('id', 'video_id', 'user_id')
|
$q->select('id', 'video_id', 'user_id')
|
||||||
->where('user_id', $user->id);
|
->where('user_id', $user->id);
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
|
||||||
@@ -100,6 +104,12 @@ class VideosController extends Controller
|
|||||||
];
|
];
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$categories = Category::select('id', 'name')
|
||||||
|
->where('is_active', true)
|
||||||
|
->whereHas('videos')
|
||||||
|
->orderBy('name', 'asc')
|
||||||
|
->get();
|
||||||
|
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'message' => 'Vídeos obtidos com sucesso',
|
'message' => 'Vídeos obtidos com sucesso',
|
||||||
'data' => $query->items(),
|
'data' => $query->items(),
|
||||||
@@ -109,6 +119,9 @@ class VideosController extends Controller
|
|||||||
'per_page' => $query->perPage(),
|
'per_page' => $query->perPage(),
|
||||||
'total' => $query->total(),
|
'total' => $query->total(),
|
||||||
],
|
],
|
||||||
|
'userId' => $userId,
|
||||||
|
'role' => $role,
|
||||||
|
'categories' => $categories,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,6 +168,7 @@ class VideosController extends Controller
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public function search(Request $request)
|
public function search(Request $request)
|
||||||
{
|
{
|
||||||
$user = auth()->user();
|
$user = auth()->user();
|
||||||
@@ -174,7 +188,7 @@ class VideosController extends Controller
|
|||||||
})
|
})
|
||||||
->where(function ($query) use ($search) {
|
->where(function ($query) use ($search) {
|
||||||
$query->where('title', 'like', "%{$search}%")
|
$query->where('title', 'like', "%{$search}%")
|
||||||
->orWhere('tags', 'like', "%{$search}%");
|
->orWhere('tags', 'like', "%{$search}%");
|
||||||
})
|
})
|
||||||
->limit(20)
|
->limit(20)
|
||||||
->get()
|
->get()
|
||||||
@@ -319,9 +333,7 @@ class VideosController extends Controller
|
|||||||
'order' => $validated['order'],
|
'order' => $validated['order'],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if ($request->filled('category_ids')) {
|
$video->categories()->sync($request->input('category_ids', []));
|
||||||
$video->categories()->sync($request->input('category_ids'));
|
|
||||||
}
|
|
||||||
|
|
||||||
$baseUrl = $request->getSchemeAndHttpHost();
|
$baseUrl = $request->getSchemeAndHttpHost();
|
||||||
|
|
||||||
@@ -386,9 +398,7 @@ class VideosController extends Controller
|
|||||||
'order' => $validated['order'] ?? $videoToUpdate->order,
|
'order' => $validated['order'] ?? $videoToUpdate->order,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if ($request->has('category_ids')) {
|
$videoToUpdate->categories()->sync($request->input('category_ids', []));
|
||||||
$videoToUpdate->categories()->sync($request->input('category_ids'));
|
|
||||||
}
|
|
||||||
|
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'message' => 'Dados do vídeo atualizados com sucesso',
|
'message' => 'Dados do vídeo atualizados com sucesso',
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ class WorkshopsController extends Controller
|
|||||||
], 404);
|
], 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$role = $user->role_id;
|
||||||
|
|
||||||
$search = trim((string) $request->query('search', ''));
|
$search = trim((string) $request->query('search', ''));
|
||||||
$status = trim((string) $request->query('status', ''));
|
$status = trim((string) $request->query('status', ''));
|
||||||
$perPage = $request->query('per_page', 9);
|
$perPage = $request->query('per_page', 9);
|
||||||
@@ -73,6 +75,8 @@ class WorkshopsController extends Controller
|
|||||||
'per_page' => $query->perPage(),
|
'per_page' => $query->perPage(),
|
||||||
'total' => $query->total(),
|
'total' => $query->total(),
|
||||||
],
|
],
|
||||||
|
'userId' => $user->id,
|
||||||
|
'role' => $role,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,6 +149,8 @@ class WorkshopsController extends Controller
|
|||||||
], 401);
|
], 401);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$role = $user->role_id;
|
||||||
|
|
||||||
$workshops = Workshop::select(['id', 'title', 'image', 'date', 'time_start', 'time_end', 'status'])
|
$workshops = Workshop::select(['id', 'title', 'image', 'date', 'time_start', 'time_end', 'status'])
|
||||||
->with(['users:id'])
|
->with(['users:id'])
|
||||||
->where('status', 'pending')
|
->where('status', 'pending')
|
||||||
@@ -168,6 +174,7 @@ class WorkshopsController extends Controller
|
|||||||
return response()->json([
|
return response()->json([
|
||||||
'message' => 'Dashboard workshops',
|
'message' => 'Dashboard workshops',
|
||||||
'data' => $workshops,
|
'data' => $workshops,
|
||||||
|
'role' => $role,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -175,6 +182,9 @@ class WorkshopsController extends Controller
|
|||||||
{
|
{
|
||||||
$workshop = Workshop::with('users')->find($id);
|
$workshop = Workshop::with('users')->find($id);
|
||||||
|
|
||||||
|
$user = auth()->user();
|
||||||
|
$role = $user->role_id;
|
||||||
|
|
||||||
if (!$workshop) {
|
if (!$workshop) {
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'message' => 'Workshop não encontrado',
|
'message' => 'Workshop não encontrado',
|
||||||
@@ -186,6 +196,7 @@ class WorkshopsController extends Controller
|
|||||||
'message' => 'Workshop obtido com sucesso',
|
'message' => 'Workshop obtido com sucesso',
|
||||||
'data' => $workshop,
|
'data' => $workshop,
|
||||||
'errors' => null,
|
'errors' => null,
|
||||||
|
'role' => $role,
|
||||||
], 200);
|
], 200);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ use App\Http\Controllers\VideosController;
|
|||||||
use App\Http\Controllers\WorkshopsController;
|
use App\Http\Controllers\WorkshopsController;
|
||||||
use App\Http\Middleware\JwtMiddleware;
|
use App\Http\Middleware\JwtMiddleware;
|
||||||
use App\Http\Controllers\VideoViewController;
|
use App\Http\Controllers\VideoViewController;
|
||||||
|
use App\Http\Controllers\DashboardController;
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
| API Routes
|
| API Routes
|
||||||
@@ -29,7 +30,7 @@ Route::post('/contact', [ContactController::class, 'send']); //proteção da rot
|
|||||||
Route::middleware([JwtMiddleware::class])->group(function () {
|
Route::middleware([JwtMiddleware::class])->group(function () {
|
||||||
/* Rota protegida por middleware JwtMiddleware - Só os utilizadores autenticados podem aceder a esta rota */
|
/* Rota protegida por middleware JwtMiddleware - Só os utilizadores autenticados podem aceder a esta rota */
|
||||||
Route::patch('/profile/{id}', [UserController::class, 'update']);
|
Route::patch('/profile/{id}', [UserController::class, 'update']);
|
||||||
Route::get('/profile/{id}', [UserController::class, 'getUser']);
|
Route::get('/profile', [UserController::class, 'profile']);
|
||||||
|
|
||||||
Route::get('/videos', [VideosController::class, 'index']);
|
Route::get('/videos', [VideosController::class, 'index']);
|
||||||
Route::get('/video/{id}', [VideosController::class, 'getVideo']);
|
Route::get('/video/{id}', [VideosController::class, 'getVideo']);
|
||||||
@@ -51,6 +52,8 @@ Route::middleware([JwtMiddleware::class])->group(function () {
|
|||||||
Route::post('/inscrever/{id}', [WorkshopsController::class, 'inscrever']);
|
Route::post('/inscrever/{id}', [WorkshopsController::class, 'inscrever']);
|
||||||
Route::delete('/cancelar-inscricao/{id}', [WorkshopsController::class, 'cancelarInscricao']);
|
Route::delete('/cancelar-inscricao/{id}', [WorkshopsController::class, 'cancelarInscricao']);
|
||||||
|
|
||||||
|
Route::get('/dashboard', [DashboardController::class, 'index']);
|
||||||
|
|
||||||
/* Para fazer a verificação do role_id no frontend das páginas do admin */
|
/* Para fazer a verificação do role_id no frontend das páginas do admin */
|
||||||
Route::get('/me', [AuthController::class, 'me']);
|
Route::get('/me', [AuthController::class, 'me']);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user