fix: offcava

This commit is contained in:
Xavier Oliveira
2026-05-28 17:51:13 +01:00
parent 21f7f444dc
commit ca79de6cc1
7 changed files with 66 additions and 68 deletions

View File

@@ -14,7 +14,7 @@ import { PiCheckCircleFill } from "react-icons/pi";
import { useGetWorkshopsSearch } from "../../hooks/useGetWorkshopsSearch"; import { useGetWorkshopsSearch } from "../../hooks/useGetWorkshopsSearch";
import { imageSkeletonFadeStyle, onImageSkeletonLoad } from "../../utils/imageSkeleton"; import { imageSkeletonFadeStyle, onImageSkeletonLoad } from "../../utils/imageSkeleton";
import { motion, AnimatePresence } from "framer-motion"; import { motion, AnimatePresence } from "framer-motion";
/* import { useGetCurrentUser } from "../../hooks/useGetCurrentUser"; */ import { useGetCurrentUser } from "../../hooks/useGetCurrentUser";
export default function Header() { export default function Header() {
@@ -30,23 +30,21 @@ export default function Header() {
const { getVideos } = useGetVideos(); const { getVideos } = useGetVideos();
const { getVideosSearch } = useGetVideosSearch(); const { getVideosSearch } = useGetVideosSearch();
const { getWorkshopsSearch } = useGetWorkshopsSearch(); const { getWorkshopsSearch } = useGetWorkshopsSearch();
/* const { getCurrentUser } = useGetCurrentUser(); */ const { getCurrentUser } = useGetCurrentUser();
const [role, _setRole] = useState(0);
const [videosWatched, setVideosWatched] = useState(0); const [videosWatched, setVideosWatched] = useState(0);
const [videosCount, setVideosCount] = useState(0); const [videosCount, setVideosCount] = useState(0);
const [role, setRole] = useState(0);
const navigate = useNavigate(); const navigate = useNavigate();
useEffect(() => { useEffect(() => {
const fetchAll = async () => { const fetchCurrentUser = async () => {
const videosWatched = localStorage.getItem("videosWatched"); const currentUser = await getCurrentUser();
setVideosWatched(videosWatched ? parseInt(videosWatched) : 0); setRole(currentUser.data.role_id);
const videosCount = localStorage.getItem("videosCount"); setVideosWatched(currentUser.data.videosWatched);
setVideosCount(videosCount ? parseInt(videosCount) : 0); setVideosCount(currentUser.data.videosCount);
/* const userData = await getCurrentUser();
setRole(userData.data.role_id); */
}; };
fetchAll(); fetchCurrentUser();
}, []); }, []);
const handleCloseMenu = () => setShowMenu(false); const handleCloseMenu = () => setShowMenu(false);

View File

@@ -34,7 +34,6 @@ export default function Sidebar() {
</div> </div>
<nav className={`${styles.nav}`}> <nav className={`${styles.nav}`}>
{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>
@@ -45,29 +44,15 @@ export default function Sidebar() {
<li className={`${styles.navItem} text-start`}> <li className={`${styles.navItem} text-start`}>
<NavLink className={({ isActive }) => isActive ? `${styles.navLink} ${styles.navLinkActive}` : styles.navLink} to="/workshops"> <LuGraduationCap className="me-2" size={24} /> {sideMenu ? "Workshops" : ""} </NavLink> <NavLink className={({ isActive }) => isActive ? `${styles.navLink} ${styles.navLinkActive}` : styles.navLink} to="/workshops"> <LuGraduationCap className="me-2" size={24} /> {sideMenu ? "Workshops" : ""} </NavLink>
</li> </li>
{role === 1 ? (
<li className={`${styles.navItem} text-start`}> <li className={`${styles.navItem} text-start`}>
<NavLink className={({ isActive }) => isActive ? `${styles.navLink} ${styles.navLinkActive}` : styles.navLink} to="/admin/users"> <LuUsers className="me-2" size={24} /> {sideMenu ? "Utilizadores" : ""} </NavLink> <NavLink className={({ isActive }) => isActive ? `${styles.navLink} ${styles.navLinkActive}` : styles.navLink} to="/admin/users"> <LuUsers className="me-2" size={24} /> {sideMenu ? "Utilizadores" : ""} </NavLink>
</li> </li>
) : null}
<li className={`${styles.navItem} text-start`}> <li className={`${styles.navItem} text-start`}>
<NavLink className={({ isActive }) => isActive ? `${styles.navLink} ${styles.navLinkActive}` : styles.navLink} to="/contactos"> <LuMail className="me-2" size={24} /> {sideMenu ? "Contactos" : ""} </NavLink> <NavLink className={({ isActive }) => isActive ? `${styles.navLink} ${styles.navLinkActive}` : styles.navLink} to="/contactos"> <LuMail className="me-2" size={24} /> {sideMenu ? "Contactos" : ""} </NavLink>
</li> </li>
</ul> </ul>
) : (
<ul className={styles.navList}>
<li className={`${styles.navItem} text-start`}>
<NavLink className={({ isActive }) => isActive ? `${styles.navLink} ${styles.navLinkActive}` : styles.navLink} to="/dashboard"> <LuLayoutDashboard className="me-2" size={24} /> {sideMenu ? "Dashboard" : ""} </NavLink>
</li>
<li className={`${styles.navItem} text-start`}>
<NavLink className={({ isActive }) => isActive ? `${styles.navLink} ${styles.navLinkActive}` : styles.navLink} to="/videos"> <LuTvMinimalPlay className="me-2" size={24} />Videos</NavLink>
</li>
<li className={`${styles.navItem} text-start`}>
<NavLink className={({ isActive }) => isActive ? `${styles.navLink} ${styles.navLinkActive}` : styles.navLink} to="/workshops"> <LuGraduationCap className="me-2" size={24} /> {sideMenu ? "Workshops" : ""} </NavLink>
</li>
<li className={`${styles.navItem} text-start`}>
<NavLink className={({ isActive }) => isActive ? `${styles.navLink} ${styles.navLinkActive}` : styles.navLink} to="/contactos"> <LuMail className="me-2" size={24} /> {sideMenu ? "Contactos" : ""} </NavLink>
</li>
</ul>
)}
</nav> </nav>
<div className={styles.logoutWrapper}> <div className={styles.logoutWrapper}>

View File

@@ -37,6 +37,8 @@ export function useGetVideos() {
meta: data.meta, meta: data.meta,
role: data.role, role: data.role,
categories: data.categories as Category[], categories: data.categories as Category[],
videosActive: data.videosActive,
videosWatched: data.videosWatched,
}; };
} else { } else {
return data as ApiErrorResponse; return data as ApiErrorResponse;

View File

@@ -65,6 +65,8 @@ export default function Videos() {
setCurrentPage(videosData.meta.current_page); setCurrentPage(videosData.meta.current_page);
setRole(videosData.role); setRole(videosData.role);
setCategories(videosData.categories); setCategories(videosData.categories);
localStorage.setItem("videosActive", videosData.videosActive.toString());
localStorage.setItem("videosWatched", videosData.videosWatched.toString());
} else { } else {
setVideos([]); setVideos([]);
} }

View File

@@ -1,4 +1,3 @@
import API_URL from "../../../config/api";
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";
@@ -7,32 +6,27 @@ import { LuArrowLeft, LuCalendar, LuClock3, LuUsers } from "react-icons/lu";
import "react-datepicker/dist/react-datepicker.css"; import "react-datepicker/dist/react-datepicker.css";
import Swal from "sweetalert2"; import Swal from "sweetalert2";
import styles from "./styles.module.css"; import styles from "./styles.module.css";
import { useGetCurrentUser } from "../../../hooks/useGetCurrentUser";
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);
const [error, setError] = useState<ApiErrorResponse | null>(null); const [error, setError] = useState<ApiErrorResponse | null>(null);
const { getCurrentUser } = useGetCurrentUser(); const [role, setRole] = useState(0);
const [currentUserData, setCurrentUserData] = useState<User | null>(null); const [userId, setUserId] = useState(0);
useEffect(() => { useEffect(() => {
const fetchAll = async () => { const fetchAll = async () => {
setLoading(true); setLoading(true);
const [workshopData, currentUserData] = await Promise.all([ const [workshopData] = await Promise.all([
getWorkshop(), getWorkshop(),
getCurrentUser(),
]); ]);
setWorkshop(workshopData as Workshop); setWorkshop(workshopData?.workshop as Workshop);
setCurrentUserData(currentUserData.data); setRole(workshopData?.role as number);
setUserId(workshopData?.userId as number);
setLoading(false); setLoading(false);
}; };
@@ -42,7 +36,7 @@ export default function Workshop() {
async function getWorkshop() { async function getWorkshop() {
try { try {
const response = await fetch(`${API_URL}/api/workshop/${id}`, { const response = await fetch(`http://127.0.0.1:8000/api/workshop/${id}`, {
method: "GET", method: "GET",
headers: { headers: {
Accept: "application/json", Accept: "application/json",
@@ -54,7 +48,11 @@ export default function Workshop() {
const data = await response.json(); const data = await response.json();
if (response.ok) { if (response.ok) {
return data.data as Workshop; return {
workshop: data.data as Workshop,
role: data.role as number,
userId: data.userId as number,
};
} else { } else {
setError(data as ApiErrorResponse); setError(data as ApiErrorResponse);
return null; return null;
@@ -67,7 +65,7 @@ export default function Workshop() {
/* Inscrever num workshop */ /* Inscrever num workshop */
async function inscrever(workshopId: number) { async function inscrever(workshopId: number) {
const response = await fetch(`${API_URL}/api/inscrever/${workshopId}`, { const response = await fetch(`http://127.0.0.1:8000/api/inscrever/${workshopId}`, {
method: "POST", method: "POST",
headers: { headers: {
Accept: "application/json", Accept: "application/json",
@@ -86,7 +84,7 @@ export default function Workshop() {
showCloseButton: true, showCloseButton: true,
}); });
const workshop = await getWorkshop(); const workshop = await getWorkshop();
setWorkshop(workshop as Workshop); setWorkshop(workshop?.workshop as Workshop);
} else { } else {
Swal.fire({ Swal.fire({
title: data.message, title: data.message,
@@ -99,7 +97,7 @@ export default function Workshop() {
/* Cancelar inscrição num workshop */ /* Cancelar inscrição num workshop */
async function cancelarInscricao(workshopId: number) { async function cancelarInscricao(workshopId: number) {
const response = await fetch(`${API_URL}/api/cancelar-inscricao/${workshopId}`, { const response = await fetch(`http://127.0.0.1:8000/api/cancelar-inscricao/${workshopId}`, {
method: "DELETE", method: "DELETE",
headers: { headers: {
Accept: "application/json", Accept: "application/json",
@@ -118,7 +116,7 @@ export default function Workshop() {
showCloseButton: true, showCloseButton: true,
}); });
const workshop = await getWorkshop(); const workshop = await getWorkshop();
setWorkshop(workshop as Workshop); setWorkshop(workshop?.workshop as Workshop);
} else { } else {
Swal.fire({ Swal.fire({
title: data.message, title: data.message,
@@ -133,6 +131,7 @@ 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>
) )
} }
@@ -159,7 +158,7 @@ export default function Workshop() {
<div className="row mt-4 g-3 gx-md-4 gx-lg-5 ms-0"> <div className="row mt-4 g-3 gx-md-4 gx-lg-5 ms-0">
<div className={`${styles.thumbnailWrapper} col-12 col-lg-3 align-self-center align-self-sm-center px-0 mt-0`}> <div className={`${styles.thumbnailWrapper} col-12 col-lg-3 align-self-center align-self-sm-center px-0 mt-0`}>
<img <img
src={`${API_URL}/storage/${workshop.image}`} src={`http://127.0.0.1:8000/storage/${workshop.image}`}
alt={workshop.title} alt={workshop.title}
className={`${styles.thumbnail} w-100`} className={`${styles.thumbnail} w-100`}
style={imageSkeletonFadeStyle} style={imageSkeletonFadeStyle}
@@ -190,20 +189,20 @@ export default function Workshop() {
</div> </div>
</div> </div>
</div> </div>
{role !== 1 ? (
<div className="col-12 mx-auto ms-sm-0 mt-5 pt-3"> <div className="col-12 mx-auto ms-sm-0 mt-5 pt-3">
{!isAdmin ? ( {workshop.users.some((user: User) => user.id === userId) ? (
currentUserData && workshop.users.some((user: User) => user.id === currentUserData.id) ? ( <button type="button" className={`${styles.btncancelarInscricao} btn text-center mx-auto py-2 text-decoration-none`} onClick={() => cancelarInscricao(workshop.id)} key={workshop.id} style={{ width: '180px' }}>
<button type="button" className={`${styles.btncancelarInscricao} btn text-center mx-auto py-2 text-decoration-none`} onClick={() => { cancelarInscricao(workshop.id); getWorkshop(); }} key={workshop.id} style={{ width: '180px' }}>
Anular inscrição Anular inscrição
</button> </button>
) : ( ) : (
<button type="button" className={`${styles.btnInscrever} btn text-center mx-auto py-2 px-4 text-decoration-none`} onClick={() => { inscrever(workshop.id); getWorkshop(); }} key={workshop.id} style={{ width: '180px' }}> <button type="button" className={`${styles.btnInscrever} btn text-center mx-auto py-2 px-4 text-decoration-none`} onClick={() => inscrever(workshop.id)} key={workshop.id} style={{ width: '180px' }}>
Inscrever-me Inscrever-me
</button> </button>
) )}
</div>
) : null} ) : null}
</div>
</div> </div>
</div> </div>

View File

@@ -35,6 +35,8 @@ export type User = {
created_at: string; created_at: string;
updated_at: string; updated_at: string;
}; };
videosWatched: number;
videosCount: number;
} }
export type UpdateUserResponse = { export type UpdateUserResponse = {
@@ -125,6 +127,7 @@ export type Workshop = {
is_active: boolean; is_active: boolean;
users: User[]; users: User[];
role: number; role: number;
userId: number;
} }
export type NextWorkshopsResponse = { export type NextWorkshopsResponse = {

View File

@@ -8,6 +8,7 @@ use Illuminate\Support\Facades\Hash;
use Tymon\JWTAuth\Facades\JWTAuth; use Tymon\JWTAuth\Facades\JWTAuth;
use Tymon\JWTAuth\Exceptions\JWTException; use Tymon\JWTAuth\Exceptions\JWTException;
use App\Http\Requests\LoginRequest; use App\Http\Requests\LoginRequest;
use App\Models\Video;
class AuthController extends Controller class AuthController extends Controller
{ {
@@ -47,7 +48,7 @@ class AuthController extends Controller
"password" => $request->password "password" => $request->password
]); ]);
if(!$login) { if (!$login) {
return response()->json([ return response()->json([
'message' => 'Credenciais inválidas', 'message' => 'Credenciais inválidas',
'errors' => null, 'errors' => null,
@@ -103,12 +104,20 @@ class AuthController extends Controller
public function me() public function me()
{ {
$user = auth()->user(); $user = auth()->user();
$videosWatched = Video::select('id')
->with('views')
->whereHas('views', function ($q) use ($user) {
$q->where('user_id', $user->id);
})->count();
$videosCount = Video::select('id')->where('is_active', true)->count();
return response()->json([ return response()->json([
'message' => 'Utilizador obtido com sucesso', 'message' => 'Utilizador obtido com sucesso',
'data' => [ 'data' => [
'id' => $user->id, 'id' => $user->id,
'role_id' => $user->role_id, 'role_id' => $user->role_id,
'videosWatched' => $videosWatched,
'videosCount' => $videosCount,
], ],
'errors' => null, 'errors' => null,
], 200); ], 200);