feat- Organização da estrutura da app

This commit is contained in:
Xavier Oliveira
2026-05-15 16:56:20 +01:00
parent 41c5f87d5b
commit da0baaee15
15 changed files with 34 additions and 2377 deletions

View File

@@ -1,160 +0,0 @@
import { useState } from "react";
import { Link } from "react-router";
import type { ApiErrorResponse, User } from "../../../types";
import type { CreateUserResponse } from "../../../types";
import styles from "./styles.module.css";
import { LuArrowLeft } from "react-icons/lu";
import { LuPlus } from "react-icons/lu";
export default function CreateUser() {
const [name, setName] = useState<string>("");
const [email, setEmail] = useState<string>("");
const [password, setPassword] = useState<string>("");
const [password_confirmation, setPasswordConfirmation] = useState<string>("");
const [role_id, setRoleId] = useState("2");
const [createUserResponse, setCreateUserResponse] = useState<CreateUserResponse | null>(null);
const [error, setError] = useState<ApiErrorResponse | null>(null);
async function create() {
//validação dos campos
if (name.length < 3) {
setError({
message: "Nome deve ter pelo menos 3 caracteres",
data: null,
errors: {}
});
return;
}
if (!email.includes('@')) {
setError({
message: "Email inválido",
data: null,
errors: {}
});
return;
}
if (password !== password_confirmation) {
setError({
message: "As senhas não coincidem",
data: null,
errors: {}
});
return;
}
if (password.length < 6) {
setError({
message: "Password deve ter pelo menos 6 caracteres",
data: null,
errors: {}
});
return;
}
if (role_id === "") {
setError({
message: "Cargo é obrigatório",
data: null,
errors: {}
});
return;
}
const payload = {
name: name,
email: email,
password: password,
password_confirmation: password_confirmation,
role_id: role_id,
}
const response = await fetch("http://127.0.0.1:8000/api/users", {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
Authorization: `Bearer ${localStorage.getItem("token")}`
},
body: JSON.stringify(
payload
),
});
const data = await response.json();
if (response.ok){
setCreateUserResponse({
data: data.data as User,
message: data.message,
});
setName("");
setEmail("");
setPassword("");
setPasswordConfirmation("");
setRoleId("2");
} else {
setError({
message: data.message,
data: null,
errors: data.errors
});
}
}
return (
<div className={`${styles.container} p-3 p-xl-0`}>
<div className={"text-start"}>
<button className={`${styles.button} border-0 bg-transparent fs-5`}><Link className={`${styles.link} text-decoration-none`} to="/users"><LuArrowLeft className={`${styles.LinkIcon} mb-1 fs-4 align-items-center`} /> <span className={styles.linkText}>Utilizadores</span> </Link></button>
</div>
<h1 className={styles.title}>Criar Utilizador</h1>
<div>
<div>
<div className="row g-3">
<div className="col-12 col-md-6 text-start">
<label className="form-label fw-bold" htmlFor="name">Nome</label>
<input type="text" className="form-control py-2" id="name" name="name" value={name} onChange={(e) => setName(e.target.value)} />
</div>
<div className="col-12 col-md-6 text-start">
<label className="form-label fw-bold" htmlFor="email">Email</label>
<input type="email" className="form-control py-2" id="email" name="email" value={email} onChange={(e) => setEmail(e.target.value)} />
</div>
<div className="col-12 col-md-6 text-start">
<label className="form-label fw-bold" htmlFor="password">Password</label>
<input type="password" className="form-control py-2" value={password} onChange={(e) => setPassword(e.target.value)} />
</div>
<div className="col-12 col-md-6 text-start">
<label className="form-label fw-bold" htmlFor="password_confirmation">Confirme a Password</label>
<input type="password" className="form-control py-2" id="password_confirmation" name="password_confirmation" value={password_confirmation} onChange={(e) => setPasswordConfirmation(e.target.value)} />
</div>
<div className="col-12 col-md-6 text-start">
<label className="form-label fw-bold" htmlFor="role_id">Cargo</label>
<select className="form-select py-2" id="role_id" name="role_id" value={role_id} onChange={(e) => setRoleId(e.target.value)}>
<option value="2">Utilizador</option>
<option value="1">Administrador</option>
</select>
</div>
</div>
{createUserResponse?.message ? (
<div className="alert alert-success mt-4">
{createUserResponse.message}
</div>
) : error?.errors ? (
<div className="alert alert-danger mt-4">
<p>{error.message}</p>
</div>
): null}
<div className="buttonsContainer mt-4 d-flex gap-2 justify-content-center" >
<button type="submit" className={`${styles.createButton}`} onClick={create}><LuPlus className="mb-1 text-white" /> Adicionar</button>
</div>
</div>
</div>
</div>
)
}

View File

@@ -1,47 +0,0 @@
.container{
width: 100%;
max-width: 1400px;
align-self: start;
}
.title{
color: var(--primary-contrast-color);
font-size: var(--size-font-title);
}
.animateSpin{
animation: spin 1s linear infinite;
}
@keyframes spin{
from{
transform: rotate(0deg);
}
to{
transform: rotate(360deg);
}
}
.LinkIcon, .linkText{
color: var(--text-black);
}
.button:hover .LinkIcon, .button:hover .linkText{
color: var(--primary-color);
}
.createButton{
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);
}
}

View File

@@ -1,280 +0,0 @@
import { useEffect, useState } from "react";
import { Link } from "react-router";
import type { ApiErrorResponse, CreateCategoryResponse } from "../../../types";
import type { CreateVideoResponse } from "../../../types";
import type { Category } from "../../../types";
import styles from "./styles.module.css";
import { LuArrowLeft, LuPlus } from "react-icons/lu";
import { LuUpload } from "react-icons/lu";
import Swal from "sweetalert2";
export default function CreateVideo() {
const [title, setTitle] = useState<string>("");
const [description, setDescription] = useState<string>("");
const [videoFile, setVideoFile] = useState<File | null>(null);
const [thumbnailFile, setThumbnailFile] = useState<File | null>(null);
const [tags, setTags] = useState<string>("");
const [createVideoResponse, setCreateVideoResponse] = useState<CreateVideoResponse | null>(null);
const [error, setError] = useState<ApiErrorResponse | null>(null);
const [isUploading, setIsUploading] = useState(false);
const [uploadProgress, setUploadProgress] = useState(0);
const [category_ids, setCategoryIds] = useState<string[]>([]);
const [categories, setCategories] = useState<Category[]>([]);
const [createCategoryResponse, setCreateCategoryResponse] = useState<CreateCategoryResponse | null>(null);
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);
}
}
async function createVideo() {
setCreateVideoResponse(null);
setError(null);
if (!videoFile || !thumbnailFile) {
setError({
message: "Vídeo e thumbnail são obrigatórios.",
data: null,
errors: {},
});
return;
}
const formData = new FormData();
formData.append("title", title);
formData.append("description", description);
formData.append("url", videoFile);
formData.append("thumbnail", thumbnailFile);
formData.append("tags", tags);
category_ids.forEach(id => {
formData.append("category_ids[]", id);
});
setIsUploading(true);
setUploadProgress(0);
const xhr = new XMLHttpRequest();
xhr.open("POST", "http://127.0.0.1:8000/api/create-video");
xhr.setRequestHeader("Authorization", `Bearer ${localStorage.getItem("token")}`);
xhr.setRequestHeader("Accept", "application/json");
xhr.upload.onprogress = (event) => {
if (event.lengthComputable) {
setUploadProgress(Math.round((event.loaded / event.total) * 100));
}
};
xhr.onload = () => {
setIsUploading(false);
let data: any = null;
try {
data = xhr.responseText ? JSON.parse(xhr.responseText) : null;
} catch {
setError({ message: "Resposta inválida do servidor.", data: null, errors: {} });
return;
}
if (xhr.status >= 200 && xhr.status < 300) {
setCreateVideoResponse({ message: "Vídeo criado com sucesso.", data: data.data });
setError(null);
setUploadProgress(100);
setIsUploading(false);
setTitle("");
setDescription("");
setVideoFile(null);
setThumbnailFile(null);
setTags("");
setCategoryIds([]);
} else {
setCreateVideoResponse(null);
setIsUploading(false);
setError(
data ?? { message: `Erro ${xhr.status} no upload.`, data: null, errors: {} }
);
}
};
xhr.onerror = () => {
setIsUploading(false);
setError({ message: "Falha de rede durante upload.", data: null, errors: {} });
};
xhr.send(formData);
}
function handleCreateCategory() {
Swal.fire({
title: 'Criar nova categoria',
input: 'text',
inputPlaceholder: 'Categoria...',
showCancelButton: true,
confirmButtonText: 'Adicionar',
cancelButtonText: 'Cancelar',
confirmButtonColor: 'var(--primary-color)',
inputValidator: (value) => {
if (!value) {
return 'Indique o nome da categoria!';
}
}
}).then((result) => {
if (result.isConfirmed) {
const categoryName = result.value;
createCategory(categoryName);
}
});
}
async function createCategory(categoryName: string ) {
const response = await fetch("http://127.0.0.1:8000/api/categories", {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
Authorization: `Bearer ${localStorage.getItem("token")}`
},
body: JSON.stringify({
name: categoryName
})
});
const data = await response.json();
setCreateCategoryResponse(data);
setError(data as ApiErrorResponse);
if (response.ok) {
setCreateCategoryResponse(data);
getCategories();
setError(null);
} else {
setCreateCategoryResponse(null);
setError(data as ApiErrorResponse);
}
}
const handleCheckbox = (e: React.ChangeEvent<HTMLInputElement>) => {
const { value, checked } = e.target;
setCategoryIds(prev =>
checked ? [...prev, value] : prev.filter(id => id !== value)
);
};
return (
<div className={`${styles.container} p-3 p-xl-0 px-2 px-md-5`}>
<div className={"text-start"}>
<button className={`${styles.button} border-0 bg-transparent fs-5`}><Link className={`${styles.link} text-decoration-none`} to="/videos"><LuArrowLeft className={`${styles.LinkIcon} mb-1 fs-4 align-items-center`} /> <span className={styles.linkText}>Vídeos</span> </Link></button>
</div>
<h1 className={styles.title}>Adicionar Vídeo</h1>
<div className="row mx-auto g-4">
<div className="col-12 px-1 text-start">
<label className="form-label fw-bold" htmlFor="title">Título</label>
<input type="text" className="form-control py-2 text-truncate" id="title" name="title" placeholder="Insira o título do vídeo" value={title} onChange={(e) => setTitle(e.target.value)} required/>
</div>
<div className="col-12 px-1 text-start">
<label className="form-label fw-bold" htmlFor="description">Descrição</label>
<textarea rows={5} className="form-control py-2" id="description" name="description" placeholder="Insira uma descrição para o vídeo" value={description} onChange={(e) => setDescription(e.target.value)} />
</div>
<div className="col-12 px-1 text-start">
<label className="form-label fw-bold" htmlFor="tags">Palavras-chave <small className="text-muted">(separar por vírgulas)</small></label>
<input type="text" className="form-control py-2 text-truncate" id="tags" name="tags" placeholder="Insira as palavras-chave do vídeo" value={tags} onChange={(e) => setTags(e.target.value)} />
</div>
<div className="col-12 col-sm-6 px-1 text-start">
<label className="form-label fw-bold" htmlFor="url">Adicionar vídeo</label>
<div className="position-relative">
<span className={`${styles.inputFiles} form-control py-2 position-absolute top-0 start-0 text-white px-3 w-100 text-decoration-none text-start fw-light fs-6 text-truncate`}><LuUpload className="mb-1 me-2" /> {videoFile ? videoFile.name : "Carregar vídeo"} </span>
<input type="file" className="form-control py-2 text-truncate" id="url" name="url" accept="video/*" style={{ width: '100%', height: '100%', cursor: 'pointer', zIndex: 1000, opacity: 0 }} onChange={(e) => setVideoFile(e.target.files?.[0] ?? null)} />
</div>
</div>
<div className="col-12 col-sm-6 px-1 text-start">
<label className="form-label fw-bold" htmlFor="thumbnail">Adicionar thumbnail</label>
<div className="position-relative">
<span className={`${styles.inputFiles} form-control py-2 position-absolute top-0 start-0 text-white px-3 w-100 text-decoration-none text-start fw-light fs-6 text-truncate`}><LuUpload className="mb-1 me-2" /> {thumbnailFile ? thumbnailFile.name : "Carregar imagem"} </span>
<input type="file" className={`${styles.inputFiles} form-control py-2 text-truncate`} id="thumbnail" name="thumbnail" accept="image/*" style={{ width: '100%', height: '100%', cursor: 'pointer', zIndex: 1000, opacity: 0 }} onChange={(e) => setThumbnailFile(e.target.files?.[0] ?? null)} />
</div>
</div>
<div className="col-12 px-1 text-start">
<div className="d-flex">
<label className="form-label fw-bold align-content-center mb-0 me-2" htmlFor="category_id">Categorias</label>
<button className={`${styles.btnAdicionarCategoria}`} onClick={handleCreateCategory} title="Adicionar categoria"><LuPlus className="mb-1" /></button>
</div>
<div>
{categories && categories.length > 0 ? (
categories.map((category) => (
<div key={category.id}>
<input type="checkbox" name="category_id" id={`category_${category.id}`} value={category.id} className="me-2" checked={category_ids.includes(category.id.toString())} onChange={handleCheckbox}/>
<label htmlFor={`category_${category.id}`} key={category.id}>{category.name}</label>
</div>))
) : (
<div>
<span>Nenhuma categoria encontrada</span>
</div>
)}
{/* <button className={`${styles.btnAdicionarVideo} btn mt-5`}><LuPlus className="mb-1" /> Nova categoria</button> */}
</div>
</div>
</div>
{error?.message ? (
<div className="alert alert-danger mt-4">
<p className="mb-1">{error.message}</p>
{Object.keys(error.errors ?? {}).length > 0 ? (
<ul className="mb-0 small">
{Object.entries(error.errors).flatMap(([field, msgs]) =>
msgs.map((m) => (
<li key={`${field}-${m}`}>
<strong>{field}</strong>: {m}
</li>
))
)}
</ul>
) : null}
</div>
) : createVideoResponse?.message || createCategoryResponse?.message ? (
<div className="alert alert-success mt-4">
{createVideoResponse?.message || createCategoryResponse?.message}
</div>
) : null}
{isUploading && (
<div className="mt-3">
<div className="progress" role="progressbar" aria-valuenow={uploadProgress} aria-valuemin={0} aria-valuemax={100}>
<div
className="progress-bar"
style={{ width: `${uploadProgress}%` }}
>
{uploadProgress}%
</div>
</div>
<small className="text-muted">A carregar vídeo...</small>
</div>
)}
<button type="button" className={`${styles.btnAdicionarVideo} btn btn-primary mt-5`} onClick={createVideo} disabled={isUploading}>{isUploading ? "A carregar..." : "Adicionar vídeo"}</button>
</div>
)
}

View File

@@ -1,78 +0,0 @@
.container{
width: 100%;
max-width: 1000px;
align-self: start;
}
.LinkIcon, .linkText{
color: var(--text-black);
}
.button:hover .LinkIcon, .button:hover .linkText{
color: var(--primary-color);
}
.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);
}
}
.btnAdicionarCategoria{
background-color: transparent;
color: var(--primary-color);
border: none;
font-size: var(--size-font-subtitle);
padding: 0;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
&:hover{
color: var(--neutral-color);
}
}
.inputFiles{
display: block;
width: 100%;
padding: .375rem .75rem;
font-size: 1rem;
font-weight: 400;
line-height: 1.5;
color: #212529 !important;
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
border: var(--bs-border-width) solid var(--bs-border-color);
border-radius: var(--bs-border-radius);
transition: border-color .15s ease-in-out, box-shadow .15s ease-in-out;
}
.animateSpin{
animation: spin 1s linear infinite;
}
@keyframes spin{
from{
transform: rotate(0deg);
}
to{
transform: rotate(360deg);
}
}

View File

@@ -1,224 +0,0 @@
import { useEffect, useState } from "react";
import { Link, useNavigate } from "react-router";
import type { ApiErrorResponse, Workshop } from "../../../types";
import type { CreateWorkshopResponse } from "../../../types";
import styles from "./styles.module.css";
import { LuArrowLeft, LuPlus, LuUpload } from "react-icons/lu";
import DatePicker from "react-datepicker";
import "react-datepicker/dist/react-datepicker.css";
import { pt } from "date-fns/locale";
import { CgSpinner } from "react-icons/cg";
export default function CreateWorkshop() {
const [title, setTitle] = useState<string>("");
const [description, setDescription] = useState<string>("");
const [image, setImage] = useState<File | null>(null);
const [date, setDate] = useState<Date | null>(null);
const [time_start, setTimeStart] = useState<Date | null>(null);
const [time_end, setTimeEnd] = useState<Date | null>(null);
const [createWorkshopResponse, setCreateWorkshopResponse] = useState<CreateWorkshopResponse | null>(null);
const [error, setError] = useState<ApiErrorResponse | null>(null);
const [isLoading, setIsLoading] = useState(false);
const navigate = useNavigate();
/* Verificação do role_id no frontend das páginas do admin */
const [checkingRole, setCheckingRole] = useState<boolean>(true);
useEffect(() => {
getVerifyRole();
}, []);
async function getVerifyRole() {
try {
const response = await fetch("http://127.0.0.1:8000/api/me", {
method: "GET",
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;
}
}
//função para formatar a data para o formato Local
function formatDateLocalISO(d: Date) {
const yyyy = d.getFullYear();
const mm = String(d.getMonth() + 1).padStart(2, "0");
const dd = String(d.getDate()).padStart(2, "0");
return `${yyyy}-${mm}-${dd}`;
}
async function createWorkshop() {
setCreateWorkshopResponse(null);
setError(null);
if (!title || !description || !image || !date || !time_start || !time_end) {
setError({
message: "Todos os campos são obrigatórios",
data: null,
errors: {},
});
return;
}
const formData = new FormData();
formData.append("title", title);
formData.append("description", description);
formData.append("image", image as File);
formData.append("date", formatDateLocalISO(date));
formData.append("time_start", time_start.toTimeString().slice(0, 5));
formData.append("time_end", time_end.toTimeString().slice(0, 5));
const response = await fetch("http://127.0.0.1:8000/api/create-workshop", {
method: "POST",
headers: {
Authorization: `Bearer ${localStorage.getItem("token")}`,
},
body: formData,
});
setIsLoading(true);
const data = await response.json();
if (response.ok) {
setCreateWorkshopResponse({ message: "Workshop criado com sucesso.", data: data.data as Workshop });
setError(null);
setTitle("");
setDescription("");
setImage(null);
setDate(null);
setTimeStart(null);
setTimeEnd(null);
setIsLoading(false);
} else {
setCreateWorkshopResponse(null);
setError(data as ApiErrorResponse);
}
}
if (checkingRole) {
return (
<div className="text-center mt-5">
<CgSpinner className={`${styles.animateSpin} text-2xl fs-3`} />
</div>
)
}
return (
<div className={`${styles.container} p-3 p-xl-0 px-2 px-md-5`}>
<div className={"text-start"}>
<button className={`${styles.button} border-0 bg-transparent fs-5`}><Link className={`${styles.link} text-decoration-none`} to="/workshops"><LuArrowLeft className={`${styles.LinkIcon} mb-1 fs-4 align-items-center`} /> <span className={styles.linkText}>Workshops</span> </Link></button>
</div>
<h1 className={styles.title}>Adicionar Workshop</h1>
<div className="row mx-auto g-4">
<div className="col-12 px-1 text-start">
<label className="form-label fw-bold" htmlFor="title">Título</label>
<input type="text" className="form-control py-2 text-truncate" id="title" name="title" placeholder="Insira o título do workshop" value={title} onChange={(e) => setTitle(e.target.value)} required />
</div>
<div className="col-12 px-1 text-start">
<label className="form-label fw-bold" htmlFor="description">Descrição</label>
<textarea rows={5} className="form-control py-2" id="description" name="description" placeholder="Insira uma descrição para o workshop" value={description} onChange={(e) => setDescription(e.target.value)} />
</div>
<div className="col-12 px-1 text-start">
<label className="form-label fw-bold" htmlFor="image">Adicionar imagem</label>
<div className="position-relative">
<span className={`${styles.inputFiles} form-control py-2 position-absolute top-0 start-0 text-white px-3 w-100 text-decoration-none text-start fw-light fs-6 text-truncate`}><LuUpload className="mb-1" /> {image ? image.name : "Carregar imagem do workshop"} </span>
<input type="file" className={`${styles.inputFiles} form-control py-2 text-truncate`} id="image" name="image" accept="image/*" style={{ width: '100%', height: '100%', cursor: 'pointer', zIndex: 1000, opacity: 0 }} onChange={(e) => setImage(e.target.files?.[0] ?? null)} />
</div>
</div>
<div className="col-12 col-sm-4 px-1 text-start d-flex flex-column">
<label className="form-label fw-bold mb-2" htmlFor="date">Data</label>
<DatePicker
selected={date}
onChange={(d: Date | null) => setDate(d)}
dateFormat="dd/MM/yyyy"
locale={pt}
className="form-control py-2"
placeholderText="Seleciona a data"
id="date"
/>
</div>
<div className="col-12 col-sm-4 px-1 text-start d-flex flex-column">
<label className="form-label fw-bold" htmlFor="time_start">Hora de início</label>
<DatePicker
selected={time_start}
onChange={(t: Date | null) => setTimeStart(t)}
showTimeSelect
showTimeSelectOnly
timeIntervals={10}
timeFormat="HH:mm"
dateFormat="HH:mm"
locale={pt}
className="form-control py-2"
placeholderText="Hora de início"
id="time_start"
minTime={time_end
? new Date(new Date(time_end).getTime() - 30 * 60 * 1000)
: new Date(new Date().setHours(9, 0, 0, 0))
}
maxTime={new Date(new Date().setHours(23, 59, 59, 999))}
/>
</div>
<div className="col-12 col-sm-4 px-1 text-start d-flex flex-column">
<label className="form-label fw-bold" htmlFor="time_end">Hora de término</label>
<DatePicker
selected={time_end}
onChange={(t: Date | null) => setTimeEnd(t)}
showTimeSelect
showTimeSelectOnly
timeIntervals={10}
timeFormat="HH:mm"
dateFormat="HH:mm"
locale={pt}
className="form-control py-2"
placeholderText="Hora de término"
id="time_end"
minTime={time_start
? new Date(new Date(time_start).getTime() + 30 * 60 * 1000)
: new Date(new Date().setHours(23, 59, 59, 999))
}
maxTime={new Date(new Date().setHours(23, 59, 59, 999))}
/>
</div>
</div>
{createWorkshopResponse?.message ? (
<div className="alert alert-success mt-4">
{createWorkshopResponse.message}
</div>
) : error?.message ? (
<div className="alert alert-danger mt-4">
<p>{error.message}</p>
</div>
) : null}
<div className="buttonsContainer mt-4 d-flex gap-2 justify-content-center" >
<button type="submit" onClick={createWorkshop} className={`${styles.btnAdicionarWorkshop}`} disabled={isLoading}><LuPlus className="mb-1 text-white" /> Adicionar Workshop</button>
</div>
</div>
)
}

View File

@@ -1,64 +0,0 @@
.container{
width: 100%;
max-width: 1000px;
align-self: start;
}
.LinkIcon, .linkText{
color: var(--text-black);
}
.button:hover .LinkIcon, .button:hover .linkText{
color: var(--primary-color);
}
.title{
color: var(--primary-contrast-color);
font-size: var(--size-font-title);
}
.inputFiles{
display: block;
width: 100%;
padding: .375rem .75rem;
font-size: 1rem;
font-weight: 400;
line-height: 1.5;
color: #212529 !important;
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
border: var(--bs-border-width) solid var(--bs-border-color);
border-radius: var(--bs-border-radius);
transition: border-color .15s ease-in-out, box-shadow .15s ease-in-out;
}
.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);
}
}
.animateSpin{
animation: spin 1s linear infinite;
}
@keyframes spin{
from{
transform: rotate(0deg);
}
to{
transform: rotate(360deg);
}
}

View File

@@ -1,426 +0,0 @@
import { useEffect, useState } from "react";
import { Link, useParams } from "react-router";
import type { ApiErrorResponse, Category, Video } from "../../../types";
import styles from "./styles.module.css";
import { LuTrash2 } from "react-icons/lu";
import { LuPencil } from "react-icons/lu";
import { CgSpinner } from "react-icons/cg";
import { LuArrowLeft } from "react-icons/lu";
import Swal from "sweetalert2";
import { Plyr } from "plyr-react";
import "plyr-react/plyr.css";
import { LuUpload } from "react-icons/lu";
import { LuPlus } from "react-icons/lu";
import { LuX } from "react-icons/lu";
import { LuCheck } from "react-icons/lu";
export default function editVideo() {
const { id } = useParams();
const [video, setVideo] = useState<Video | null>(null);
const [messageDeleteVideo, setMessageDeleteVideo] = useState<string>("");
const [error, setError] = useState<ApiErrorResponse | null>(null);
const [messageUpdateVideo, setMessageUpdateVideo] = useState<string>("");
const [loading, setLoading] = useState(true);
const [categories, setCategories] = useState<Category[]>([]);
const [category_ids, setCategoryIds] = useState<string[]>([]);
const [thumbnailFile, setThumbnailFile] = useState<File | null>(null);
const [formEdit, setFormEdit] = useState<boolean>(false);
const [isActive, setIsActive] = useState<boolean>();
useEffect(() => {
getVideo();
}, [id]);
useEffect(() => {
if (!video) return;
setCategoryIds(video.categories?.map((category) => category.id.toString()) ?? []);
setIsActive(Boolean(video.is_active));
}, [video]);
async function getVideo() {
setLoading(true);
try {
const response = await fetch(`http://127.0.0.1:8000/api/video/${id}`, {
method: "GET",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
Authorization: `Bearer ${localStorage.getItem("token")}`
},
});
const data = await response.json();
if (response.ok) {
setVideo(data.data);
} else {
setVideo(null);
setError(data as ApiErrorResponse);
}
} catch {
console.error(error);
} finally {
setLoading(false);
}
}
async function destroy(id: number) {
const response = await fetch(`http://127.0.0.1:8000/api/video/${id}`, {
method: "DELETE",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
Authorization: `Bearer ${localStorage.getItem("token")}`
},
});
const data = await response.json();
if (response.ok) {
setMessageDeleteVideo(data.message as string);
setError(null);
} else {
setMessageDeleteVideo("");
setError(data as ApiErrorResponse);
}
}
function handleDeleteVideo() {
Swal.fire({
text: 'Tem a certeza que deseja apagar este vídeo?',
showCancelButton: true,
confirmButtonText: 'Sim, apagar',
confirmButtonColor: 'var(--primary-color)',
cancelButtonText: 'Cancelar',
}).then((result) => {
if (result.isConfirmed) {
destroy(video?.id as number);
}
});
}
async function update(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault();
const formData = new FormData(event.currentTarget);
// Garante categorias atualizadas via state (checkboxes controladas)
formData.delete("category_ids[]");
category_ids.forEach((categoryId) => {
formData.append("category_ids[]", categoryId);
});
// Mantém compatibilidade de upload de ficheiro com Laravel
// quando a rota aceita PATCH (method spoofing)
formData.append("_method", "PATCH");
const response = await fetch(`http://127.0.0.1:8000/api/video/${id}`, {
method: "POST",
headers: {
Accept: "application/json",
Authorization: `Bearer ${localStorage.getItem("token")}`
},
body: formData,
});
const data = await response.json();
if (response.ok) {
setVideo(data.data);
setMessageUpdateVideo(data.message as string);
setTimeout(() => {
setMessageUpdateVideo("");
}, 3000);
setError(null);
getVideo();
setFormEdit(false);
} else {
setMessageUpdateVideo("");
setError(data as ApiErrorResponse);
}
}
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);
}
}
const handleCheckbox = (e: React.ChangeEvent<HTMLInputElement>) => {
const { value, checked } = e.target;
setCategoryIds(prev =>
checked ? [...prev, value] : prev.filter(id => id !== value)
);
};
function handleCreateCategory(e: React.MouseEvent<HTMLButtonElement>) {
e.preventDefault();
Swal.fire({
title: 'Criar nova categoria',
input: 'text',
inputPlaceholder: 'Categoria...',
showCancelButton: true,
confirmButtonText: 'Adicionar',
cancelButtonText: 'Cancelar',
confirmButtonColor: 'var(--primary-color)',
inputValidator: (value) => {
if (!value) {
return 'Indique o nome da categoria!';
}
}
}).then((result) => {
if (result.isConfirmed) {
const categoryName = result.value;
createCategory(categoryName);
}
});
}
async function createCategory(categoryName: string) {
const response = await fetch("http://127.0.0.1:8000/api/categories", {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
Authorization: `Bearer ${localStorage.getItem("token")}`
},
body: JSON.stringify({
name: categoryName
})
});
const data = await response.json();
setError(data as ApiErrorResponse);
if (response.ok) {
getCategories();
setError(null);
} else {
setError(data as ApiErrorResponse);
}
}
if (loading) {
return (
<div className="text-center">
<CgSpinner className={`${styles.animateSpin} text-2xl fs-3`} />
</div>
)
}
return (
<div className={`${styles.container} p-3 p-xl-0`}>
<div className={"text-start"}>
<button className={`${styles.button} border-0 bg-transparent fs-5`}><Link className={`${styles.link} text-decoration-none`} to="/videos"><LuArrowLeft className={`${styles.LinkIcon} mb-1 fs-4 align-items-center`} /> <span className={styles.linkText}>Vídeos</span> </Link></button>
</div>
{messageDeleteVideo ? (
<div className="">
<div className="alert alert-success mt-4">
{messageDeleteVideo}
</div>
</div>
) : video ? (
<div className="my-3">
<span className={styles.title}>{video.title} </span>
<div className={`${styles.videoContainer} mt-4`} style={{ maxWidth: 1000, margin: "0 auto" }}>
<Plyr
source={{
type: "video",
sources: [
{
src: `http://127.0.0.1:8000${video.url}`,
type: "video/mp4",
},
],
}}
/>
</div>
{formEdit ? (
<div className="mt-5">
<span className={styles.title}>Alterar detalhes do vídeo</span>
<div className="row mx-auto g-4">
<form method="patch" onSubmit={update}>
<input type="hidden" name="video_id" value={video?.id} />
<div className="col-12 px-1 text-start mb-3">
<label className="form-label fw-bold" htmlFor="title">Título</label>
<input type="text" className="form-control py-2 text-truncate" id="title" name="title" defaultValue={video?.title} />
</div>
<div className="col-12 px-1 text-start mb-3">
<label className="form-label fw-bold" htmlFor="description">Descrição</label>
<textarea rows={5} className="form-control py-2" id="description" name="description" defaultValue={video?.description} />
</div>
<div className="col-12 px-1 text-start mb-3">
<label className="form-label fw-bold" htmlFor="tags">Tags <small className="text-muted">(separar por vírgulas)</small></label>
<input type="text" className="form-control py-2 text-truncate" id="tags" name="tags" defaultValue={video?.tags} />
</div>
<div className="col-12 px-1 text-start mb-3">
<label className="form-label fw-bold" htmlFor="thumbnail">Atualizar thumbnail</label>
<div className="position-relative">
<span className={`${styles.inputFiles} form-control py-2 position-absolute top-0 start-0 text-white px-3 w-100 text-decoration-none text-start fw-light fs-6 text-truncate`}><LuUpload className="mb-1 me-2" /> {thumbnailFile ? thumbnailFile.name : "Carregar imagem"} </span>
<input type="file" className={`${styles.inputFiles} form-control py-2 text-truncate`} id="thumbnail" name="thumbnail" accept="image/*" style={{ width: '100%', height: '100%', cursor: 'pointer', zIndex: 1000, opacity: 0 }} onChange={(e) => setThumbnailFile(e.target.files?.[0] ?? null)} />
</div>
</div>
<div className="col-12 px-1 text-start mb-3">
<div className="d-flex">
<label className="form-label fw-bold align-content-center mb-0 me-2" htmlFor="category_id">Categorias</label>
<button className={`${styles.btnAdicionarCategoria}`} onClick={handleCreateCategory} title="Adicionar categoria"><LuPlus className="mb-1" /></button>
</div>
<div>
{categories && categories.length > 0 ? (
categories.map((category) => (
<div key={category.id}>
<input type="checkbox" name="category_ids[]" id={`category_${category.id}`} value={category.id} className="me-2" checked={category_ids.includes(category.id.toString())} onChange={handleCheckbox} />
<label htmlFor={`category_${category.id}`} key={category.id}>{category.name}</label>
</div>))
) : (
<div>
<span>Nenhuma categoria encontrada</span>
</div>
)}
{/* <button className={`${styles.btnAdicionarVideo} btn mt-5`}><LuPlus className="mb-1" /> Nova categoria</button> */}
</div>
</div>
<div className="col-12 px-1 text-start d-flex flex-column mb-3">
<label className="form-label fw-bold" htmlFor="tags">Estado</label>
<div className="d-flex gap-4">
<span>
<input
type="radio"
name="is_active"
id="is_active_true"
value="1"
checked={isActive === true}
onChange={() => setIsActive(true)}
/>
<label className="ms-1" htmlFor="is_active_true">Ativo</label>
</span>
<span>
<input
type="radio"
name="is_active"
id="is_active_false"
value="0"
checked={isActive === false}
onChange={() => setIsActive(false)}
/>
<label className="ms-1" htmlFor="is_active_false">Inativo</label>
</span>
</div>
{/* <select className="form-control py-2 text-truncate" id="is_active" name="is_active" defaultValue={video?.is_active ? "Ativo" : "Inativo"} onChange={(e) => setIsActive(e.target.value)}>
<option value="1">Ativo</option>
<option value="0">Inativo</option>
</select> */}
</div>
{error?.errors ? (
<div className="">
<div className="alert alert-danger mt-4">
<p>{error.message}</p>
</div>
</div>
) : null}
<div className="buttonsContainer mt-5 d-flex gap-2 justify-content-center" >
<button type="button" className={`${styles.cancelButton}`} onClick={() => setFormEdit(false)}> Cancelar <LuX className="mb-1 text-white" /></button>
<button type="submit" className={`${styles.submitButton} bg-primary`}>Submeter Dados <LuCheck className="mb-1 text-white" /></button>
</div>
</form>
</div>
</div>
) : (
<div>
{messageUpdateVideo ? (
<div className="">
<div className="alert alert-success mt-4">
{messageUpdateVideo}
</div>
</div>
) : null}
{error?.errors ? (
<div className="">
<div className="alert alert-danger mt-4">
<p>{error.message}</p>
</div>
</div>) : null}
{video?.is_active ? (
<div className="text-start mt-3 d-flex flex-column">
<b>Estado: </b>
<span className="bg-success text-white text-center p-2" style={{ width: '100px' }}> <b>Ativo</b></span>
</div>
) : (
<div className="text-start mt-3 d-flex flex-column">
<b>Estado: </b>
<span className="bg-danger text-white text-center p-2" style={{ width: '100px' }}> <b>Inativo</b></span>
</div>
)}
<div className="text-start mt-3">
<b>Descrição: </b>
<p>{video.description}</p>
</div>
<div className="text-start mt-3">
<b>Categorias: </b>
<p>{video.categories?.length ? video.categories.map((category) => category.name).join(", ") : "Sem categorias"}</p>
</div>
<div className="text-start mt-3">
<b>Tags: </b>
<p>{video.tags}</p>
</div>
<div className="text-start mt-3 d-flex flex-column">
<b>Thumbnail: </b>
<img src={`http://127.0.0.1:8000${video.thumbnail}`} alt={video.title} className={`${styles.imgThumbnail} img-fluid img-thumbnail mt-2`} />
</div>
<div className="buttonsContainer mt-5 d-flex gap-2 justify-content-center" >
<button type="button" className={`${styles.updateButton} bg-primary`} onClick={() => setFormEdit(true)}> {formEdit ? "Cancelar" : "Editar"} <LuPencil className="mb-1 text-white" /></button>
<button type="button" className={`${styles.deleteButton}`} onClick={() => handleDeleteVideo()}>Apagar <LuTrash2 className="mb-1 text-white" /></button>
</div>
</div>
)}
</div>
) : error?.errors ? (
<div className="">
<div className="alert alert-danger mt-4">
<p>{error.message}</p>
</div>
</div>
) : (
<div className="text-center alert alert-danger mt-5 align-items-center">
<span>Vídeo não encontrado</span>
</div>
)}
</div>
)
}

View File

@@ -1,224 +0,0 @@
import { useEffect, useState } from "react";
import { Link, useNavigate } from "react-router";
import type { ApiErrorResponse, Workshop } from "../../../types";
import type { CreateWorkshopResponse } from "../../../types";
import styles from "./styles.module.css";
import { LuArrowLeft, LuPlus, LuUpload } from "react-icons/lu";
import DatePicker from "react-datepicker";
import "react-datepicker/dist/react-datepicker.css";
import { pt } from "date-fns/locale";
import { CgSpinner } from "react-icons/cg";
export default function CreateWorkshop() {
const [title, setTitle] = useState<string>("");
const [description, setDescription] = useState<string>("");
const [image, setImage] = useState<File | null>(null);
const [date, setDate] = useState<Date | null>(null);
const [time_start, setTimeStart] = useState<Date | null>(null);
const [time_end, setTimeEnd] = useState<Date | null>(null);
const [createWorkshopResponse, setCreateWorkshopResponse] = useState<CreateWorkshopResponse | null>(null);
const [error, setError] = useState<ApiErrorResponse | null>(null);
const [isLoading, setIsLoading] = useState(false);
const navigate = useNavigate();
/* Verificação do role_id no frontend das páginas do admin */
const [checkingRole, setCheckingRole] = useState<boolean>(true);
useEffect(() => {
getVerifyRole();
}, []);
async function getVerifyRole() {
try {
const response = await fetch("http://127.0.0.1:8000/api/me", {
method: "GET",
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;
}
}
//função para formatar a data para o formato Local
function formatDateLocalISO(d: Date) {
const yyyy = d.getFullYear();
const mm = String(d.getMonth() + 1).padStart(2, "0");
const dd = String(d.getDate()).padStart(2, "0");
return `${yyyy}-${mm}-${dd}`;
}
async function createWorkshop() {
setCreateWorkshopResponse(null);
setError(null);
if (!title || !description || !image || !date || !time_start || !time_end) {
setError({
message: "Todos os campos são obrigatórios",
data: null,
errors: {},
});
return;
}
const formData = new FormData();
formData.append("title", title);
formData.append("description", description);
formData.append("image", image as File);
formData.append("date", formatDateLocalISO(date));
formData.append("time_start", time_start.toTimeString().slice(0, 5));
formData.append("time_end", time_end.toTimeString().slice(0, 5));
const response = await fetch("http://127.0.0.1:8000/api/create-workshop", {
method: "POST",
headers: {
Authorization: `Bearer ${localStorage.getItem("token")}`,
},
body: formData,
});
setIsLoading(true);
const data = await response.json();
if (response.ok) {
setCreateWorkshopResponse({ message: "Workshop criado com sucesso.", data: data.data as Workshop });
setError(null);
setTitle("");
setDescription("");
setImage(null);
setDate(null);
setTimeStart(null);
setTimeEnd(null);
setIsLoading(false);
} else {
setCreateWorkshopResponse(null);
setError(data as ApiErrorResponse);
}
}
if (checkingRole) {
return (
<div className="text-center mt-5">
<CgSpinner className={`${styles.animateSpin} text-2xl fs-3`} />
</div>
)
}
return (
<div className={`${styles.container} p-3 p-xl-0 px-2 px-md-5`}>
<div className={"text-start"}>
<button className={`${styles.button} border-0 bg-transparent fs-5`}><Link className={`${styles.link} text-decoration-none`} to="/workshops"><LuArrowLeft className={`${styles.LinkIcon} mb-1 fs-4 align-items-center`} /> <span className={styles.linkText}>Workshops</span> </Link></button>
</div>
<h1 className={styles.title}>Adicionar Workshop</h1>
<div className="row mx-auto g-4">
<div className="col-12 px-1 text-start">
<label className="form-label fw-bold" htmlFor="title">Título</label>
<input type="text" className="form-control py-2 text-truncate" id="title" name="title" placeholder="Insira o título do workshop" value={title} onChange={(e) => setTitle(e.target.value)} required />
</div>
<div className="col-12 px-1 text-start">
<label className="form-label fw-bold" htmlFor="description">Descrição</label>
<textarea rows={5} className="form-control py-2" id="description" name="description" placeholder="Insira uma descrição para o workshop" value={description} onChange={(e) => setDescription(e.target.value)} />
</div>
<div className="col-12 px-1 text-start">
<label className="form-label fw-bold" htmlFor="image">Adicionar imagem</label>
<div className="position-relative">
<span className={`${styles.inputFiles} form-control py-2 position-absolute top-0 start-0 text-white px-3 w-100 text-decoration-none text-start fw-light fs-6 text-truncate`}><LuUpload className="mb-1" /> {image ? image.name : "Carregar imagem do workshop"} </span>
<input type="file" className={`${styles.inputFiles} form-control py-2 text-truncate`} id="image" name="image" accept="image/*" style={{ width: '100%', height: '100%', cursor: 'pointer', zIndex: 1000, opacity: 0 }} onChange={(e) => setImage(e.target.files?.[0] ?? null)} />
</div>
</div>
<div className="col-12 col-sm-4 px-1 text-start d-flex flex-column">
<label className="form-label fw-bold mb-2" htmlFor="date">Data</label>
<DatePicker
selected={date}
onChange={(d: Date | null) => setDate(d)}
dateFormat="dd/MM/yyyy"
locale={pt}
className="form-control py-2"
placeholderText="Seleciona a data"
id="date"
/>
</div>
<div className="col-12 col-sm-4 px-1 text-start d-flex flex-column">
<label className="form-label fw-bold" htmlFor="time_start">Hora de início</label>
<DatePicker
selected={time_start}
onChange={(t: Date | null) => setTimeStart(t)}
showTimeSelect
showTimeSelectOnly
timeIntervals={10}
timeFormat="HH:mm"
dateFormat="HH:mm"
locale={pt}
className="form-control py-2"
placeholderText="Hora de início"
id="time_start"
minTime={time_end
? new Date(new Date(time_end).getTime() - 30 * 60 * 1000)
: new Date(new Date().setHours(9, 0, 0, 0))
}
maxTime={new Date(new Date().setHours(23, 59, 59, 999))}
/>
</div>
<div className="col-12 col-sm-4 px-1 text-start d-flex flex-column">
<label className="form-label fw-bold" htmlFor="time_end">Hora de término</label>
<DatePicker
selected={time_end}
onChange={(t: Date | null) => setTimeEnd(t)}
showTimeSelect
showTimeSelectOnly
timeIntervals={10}
timeFormat="HH:mm"
dateFormat="HH:mm"
locale={pt}
className="form-control py-2"
placeholderText="Hora de término"
id="time_end"
minTime={time_start
? new Date(new Date(time_start).getTime() + 30 * 60 * 1000)
: new Date(new Date().setHours(23, 59, 59, 999))
}
maxTime={new Date(new Date().setHours(23, 59, 59, 999))}
/>
</div>
</div>
{createWorkshopResponse?.message ? (
<div className="alert alert-success mt-4">
{createWorkshopResponse.message}
</div>
) : error?.message ? (
<div className="alert alert-danger mt-4">
<p>{error.message}</p>
</div>
) : null}
<div className="buttonsContainer mt-4 d-flex gap-2 justify-content-center" >
<button type="submit" onClick={createWorkshop} className={`${styles.btnAdicionarWorkshop}`} disabled={isLoading}><LuPlus className="mb-1 text-white" /> Adicionar Workshop</button>
</div>
</div>
)
}

View File

@@ -1,64 +0,0 @@
.container{
width: 100%;
max-width: 1000px;
align-self: start;
}
.LinkIcon, .linkText{
color: var(--text-black);
}
.button:hover .LinkIcon, .button:hover .linkText{
color: var(--primary-color);
}
.title{
color: var(--primary-contrast-color);
font-size: var(--size-font-title);
}
.inputFiles{
display: block;
width: 100%;
padding: .375rem .75rem;
font-size: 1rem;
font-weight: 400;
line-height: 1.5;
color: #212529 !important;
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
border: var(--bs-border-width) solid var(--bs-border-color);
border-radius: var(--bs-border-radius);
transition: border-color .15s ease-in-out, box-shadow .15s ease-in-out;
}
.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);
}
}
.animateSpin{
animation: spin 1s linear infinite;
}
@keyframes spin{
from{
transform: rotate(0deg);
}
to{
transform: rotate(360deg);
}
}

View File

@@ -1,104 +0,0 @@
.container{
width: 100%;
max-width: 1400px;
align-self: start;
}
.LinkIcon, .linkText{
color: var(--text-black);
}
.button:hover .LinkIcon, .button:hover .linkText{
color: var(--primary-color);
}
.title{
color: var(--primary-contrast-color);
font-size: var(--size-font-title);
}
.statusAlert{
width: 100px;
border-radius: var(--border-radius-input);
}
.imgThumbnail{
max-width: 400px;
border-radius: var(--border-radius);
}
.inputFiles{
display: block;
width: 100%;
padding: .375rem .75rem;
font-size: 1rem;
font-weight: 400;
line-height: 1.5;
color: #212529 !important;
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
border: var(--bs-border-width) solid var(--bs-border-color);
border-radius: var(--bs-border-radius);
transition: border-color .15s ease-in-out, box-shadow .15s ease-in-out;
}
.btnAdicionarCategoria{
background-color: transparent;
color: var(--primary-color);
border: none;
font-size: var(--size-font-subtitle);
padding: 0;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
&:hover{
color: var(--neutral-color);
}
}
.updateButton, .submitButton{
background-color: var(--text-primary);
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);
}
}
.deleteButton, .cancelButton{
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);
}
}
.animateSpin{
animation: spin 1s linear infinite;
}
@keyframes spin{
from{
transform: rotate(0deg);
}
to{
transform: rotate(360deg);
}
}

View File

@@ -1,297 +0,0 @@
import { useEffect, useState } from "react";
import { Link, useParams } from "react-router";
import type { ApiErrorResponse, User, Video } from "../../../types";
import styles from "./styles.module.css";
import { LuCheck, LuTrash2, LuX, LuPencil, LuArrowLeft } from "react-icons/lu";
import { CgSpinner } from "react-icons/cg";
import Swal from "sweetalert2";
export default function User() {
const { id } = useParams();
const [user, setUser] = useState<User | null>(null);
const [messageGetUser, setMessageGetUser] = useState<string>("");
const [messageDeleteUser, setMessageDeleteUser] = useState<string>("");
const [error, setError] = useState<ApiErrorResponse | null>(null);
const [messageUpdateUser, setMessageUpdateUser] = useState<string>("");
const [loading, setLoading] = useState(true);
const [formEdit, setFormEdit] = useState<boolean>(false);
useEffect(() => {
getUser();
}, [id]);
async function getUser() {
setLoading(true);
if (id === user?.id) {
setError({
message: "Não pode editar o seu próprio utilizador",
data: null,
errors: {},
});
return;
}
try {
const response = await fetch(`http://127.0.0.1:8000/api/user/${id}`, {
method: "GET",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
Authorization: `Bearer ${localStorage.getItem("token")}`
},
});
const data = await response.json();
if (response.ok) {
setUser(data.data);
setMessageGetUser("");
} else {
setUser(null);
setError(data as ApiErrorResponse);
}
} catch (error) {
console.error(error);
} finally {
setLoading(false);
}
}
async function destroy(id: number) {
const response = await fetch(`http://127.0.0.1:8000/api/user/${id}`, {
method: "DELETE",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
Authorization: `Bearer ${localStorage.getItem("token")}`
},
});
const data = await response.json();
if (response.ok) {
setMessageDeleteUser(data.message as string);
setError(null);
} else {
setMessageDeleteUser("");
setError(data as ApiErrorResponse);
}
}
function handleDeleteUser() {
Swal.fire({
text: 'Tem a certeza que deseja apagar este utilizador?',
showCancelButton: true,
confirmButtonText: 'Sim, apagar',
confirmButtonColor: 'var(--primary-color)',
cancelButtonText: 'Cancelar',
}).then((result) => {
if (result.isConfirmed) {
destroy(user?.id as number);
}
});
}
async function update(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault();
const formData = new FormData(event.currentTarget);
const user_id = formData.get("user_id");
const name = formData.get("name");
const email = formData.get("email");
const role_id = formData.get("role_id");
const payload = {
user_id: user_id,
name: name,
email: email,
role_id: role_id,
};
const response = await fetch(`http://127.0.0.1:8000/api/user/${id}`, {
method: "PATCH",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
Authorization: `Bearer ${localStorage.getItem("token")}`
},
body: JSON.stringify(payload),
});
const data = await response.json();
if (response.ok) {
setUser(data.data);
setFormEdit(false);
setMessageUpdateUser(data.message as string);
setTimeout(() => {
setMessageUpdateUser("");
}, 3000)
setError(null);
getUser();
} else {
setMessageUpdateUser("");
setError(data as ApiErrorResponse);
}
}
if (loading) {
return (
<div className="text-center">
<CgSpinner className={`${styles.animateSpin} text-2xl fs-3`} />
</div>
)
}
if (error) {
return (
<div className="text-center mt-5">
<p>{(error as ApiErrorResponse).message}</p>
</div>
)
}
return (
<div className={`${styles.container} p-1 p-xl-0`}>
<div className={"text-start mt-5"}>
<button className={`${styles.button} border-0 bg-transparent fs-5`}><Link className={`${styles.link} text-decoration-none`} to="/admin/users"><LuArrowLeft className={`${styles.LinkIcon} mb-1 fs-4 align-items-center`} /> <span className={styles.linkText}>Utilizadores</span> </Link></button>
</div>
{messageDeleteUser ? (
<div className="">
<div className="alert alert-success mt-4">
{messageDeleteUser}
</div>
</div>
) : user ? (
<div className="my-3">
<span className={styles.title}>Dados do Utilizador </span>
{messageUpdateUser ? (
<div className="">
<div className="alert alert-success mt-4">
{messageUpdateUser}
</div>
</div>
) : null}
<div className="row mt-5 justify-content-between">
<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="col-12 col-md-6 d-flex flex-column text-start flex-column mt-xl-0">
<span className={`badge text-uppercase bg-primary fw-bold px-4 py-3 fs-6 mb-3 d-flex d-md-none align-self-center ${user.role_id === 1 ? 'bg-primary bg-gradient text-white' : 'bg-secondary bg-gradient text-white'} `} style={{ width: "fit-content" }}>{user.role_id === 1 ? "Administrador" : "Utilizador"}</span>
<span className="fw-bold fs-4">{user.name}</span>
<span className="fs-5 flex-grow-1">{user.email}</span>
<div className="d-flex flex-column">
<span className="d-flex justify-content-start text-black text-muted flex-wrap flex-sm-nowrap fs-6 fw-medium mt-4 py-0" style={{ maxWidth: "275px" }}>Membro desde: <b className="fw-bold fs-6">{new Date(user.created_at).toLocaleDateString('pt-PT')}</b></span>
<span className="d-flex justify-content-start text-black text-muted flex-wrap flex-sm-nowrap fs-6 fw-medium py-0" style={{ maxWidth: "275px" }}>Última actualização: <b className="fw-bold fs-6">{new Date(user.updated_at).toLocaleDateString('pt-PT')}</b></span>
</div>
</div>
<div className="col-12 col-md-6 d-flex flex-column gap-2 text-end align-items-start align-items-md-end mt-0 justify-content-between mx-auto mx-md-0">
<span className={`badge text-uppercase d-none d-md-flex fw-bold px-4 py-3 fs-6 mt-0 ${user.role_id === 1 ? 'bg-primary bg-gradient text-white' : 'bg-secondary bg-gradient text-white'}`} style={{ width: "fit-content" }}>{user.role_id === 1 ? "Administrador" : "Utilizador"}</span>
<div className="buttonsContainer mt-4 mt-md-5 d-flex gap-2 justify-content-center mx-auto mx-md-0 flex-wrap" >
<a className={`${styles.updateButton} text-decoration-none text-primary`} href="#formEditUser" onClick={() => { setFormEdit(true); setMessageUpdateUser(""); }}> Editar <LuPencil className={`${styles.icon} mb-1 text-primary`} /></a>
<a className={`${styles.deleteButton} text-decoration-none text-danger`} onClick={() => handleDeleteUser()}>Apagar <LuTrash2 className={`${styles.icon} mb-1 text-danger`} /></a>
</div>
</div>
</div>
</div>
<div className="col-12 col-lg-4 p-2">
<div className={`${styles.userVideosWatched} text-start d-flex flex-column h-100 gap-3 p-4`}>
<div className="d-flex flex-column">
<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>
</div>
<div className="d-flex flex-column">
<span className="fw-normal fs-6" style={{ color: "var(--bg-grey)" }}>Workshops</span>
<span className="fs-2 fw-bold text-white">0</span>
</div>
</div>
</div>
</div>
{/* <div className="row g-5 mt-4 px-5 px-xl-0">
<div className="col-12 col-sm-6 col-md-4 col-xl-1 d-flex flex-column text-start flex-column gap-2 mt-3 mt-xl-0">
<b>ID</b>
<span>{user.id}</span>
</div>
<div className="col-12 col-sm-6 col-md-4 col-xl d-flex flex-column text-start flex-column gap-2 mt-3 mt-xl-0 px-xl-1">
<b>Nome</b>
<span>{user.name}</span>
</div>
<div className="col-12 col-sm-6 col-md-4 col-xl d-flex flex-column text-start flex-column gap-2 mt-3 mt-xl-0 px-xl-1">
<b>Email</b>
<span>{user.email}</span>
</div>
<div className="col-12 col-sm-6 col-md-4 col-xl d-flex flex-column text-start flex-column gap-2 mt-3 mt-xl-0 px-xl-1">
<b>Cargo</b>
<span>{user.role_id === 1 ? "Administrador" : "Utilizador"}</span>
</div>
<div className="col-12 col-sm-6 col-md-4 col-xl d-flex flex-column text-start flex-column gap-2 mt-3 mt-xl-0 px-xl-1">
<b>Criado a</b>
<span>{new Date(user.created_at).toLocaleDateString('pt-PT')}</span>
</div>
<div className="col-12 col-sm-6 col-md-4 col-xl d-flex flex-column text-start flex-column gap-2 mt-3 mt-xl-0 px-xl-1">
<b>Actualizado a</b>
<span>{new Date(user.updated_at).toLocaleDateString('pt-PT')}</span>
</div>
</div> */}
<div className="mt-5">
{formEdit ? (
<div className="my-3">
<span className={styles.title}>Editar Utilizador</span>
<form method="patch" onSubmit={update} id="formEditUser">
<div className="row g-3 mt-2">
<input type="hidden" name="user_id" value={user?.id} />
<div className="col-12 col-md-6 text-start">
<label className="form-label fw-bold" htmlFor="name">Nome</label>
<input type="text" className="form-control py-2" id="name" name="name" defaultValue={user?.name} />
</div>
<div className="col-12 col-md-6 text-start">
<label className="form-label fw-bold" htmlFor="email">Email</label>
<input type="email" className="form-control py-2" id="email" name="email" defaultValue={user?.email} />
</div>
<div className="col-12 col-md-6 text-start">
<label className="form-label fw-bold" htmlFor="role_id">Cargo</label>
<select className="form-select py-2" id="role_id" name="role_id" defaultValue={user?.role_id}>
<option value="1">Administrador</option>
<option value="2">Utilizador</option>
</select>
</div>
</div>
{error ? (
<div className="">
<div className="alert alert-danger mt-4">
<p>{(error as ApiErrorResponse).message}</p>
</div>
</div>
) : null}
<div className="buttonsContainer mt-4 d-flex gap-2 justify-content-center" >
<button type="button" className={`${styles.closeFormButton}`} onClick={() => setFormEdit(false)}> Cancelar <LuX className={`${styles.icon} mb-1`} /></button>
<button type="submit" className={`${styles.submitFormButton}`} >Submeter dados <LuCheck className="mb-1" /></button>
</div>
</form>
</div>
) : null}
</div>
</div>
) : (
<div className="text-center alert alert-danger mt-5 align-items-center">
<span>Utilizador não encontrado</span>
</div>
)}
</div>
)
}

View File

@@ -1,123 +0,0 @@
.container{
align-self: start;
width: 100%;
max-width: 1400px;
}
.title{
color: var(--primary-contrast-color);
font-size: var(--size-font-title);
}
.animateSpin{
animation: spin 1s linear infinite;
}
@keyframes spin{
from{
transform: rotate(0deg);
}
to{
transform: rotate(360deg);
}
}
.LinkIcon, .linkText{
color: var(--text-black);
}
.button:hover .LinkIcon, .button:hover .linkText{
color: var(--primary-color);
}
.userCard{
border-radius: var(--border-radius);
background-color: var(--bg-white);
}
.updateButton{
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(--bg-tertiary-color-opacity) !important;
}
}
.icon{
transition: all 0.3s ease;
}
.deleteButton, .cancelButton{
color: var(--text-primary-color);
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(--bg-primary-color-opacity);
}
}
.userVideosWatched{
border-radius: var(--border-radius);
background-color: var(--bg-primary-color);
}
.closeFormButton{
background-color: var(--bg-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(--bg-primary-color-opacity);
color: var(--text-primary-color) !important;
.icon{
color: var(--text-primary-color) !important;
}
}
}
.submitFormButton{
background-color: var(--tertiary-color);
color: var(--text-white) !important;
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(--bs-primary-bg-subtle);
color: var(--tertiary-color) !important;
.icon{
color: var(--tertiary-color) !important;
}
}
}
.linkBack{
color: var(--text-black);
font-weight: 500;
transition: all 0.3s ease;
&:hover{
color: var(--primary-color);
}
}

View File

@@ -1,199 +0,0 @@
import { useEffect, useState } from "react";
import type { ApiErrorResponse, User } from "../../../types";
import { Link, Navigate } from "react-router";
import styles from "./styles.module.css";
import { LuPencil } from "react-icons/lu";
import { CgSpinner } from "react-icons/cg";
import { LuPlus } from "react-icons/lu";
import { Pagination, Table } from "react-bootstrap";
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 [error, setError] = useState<ApiErrorResponse | null>(null);
const [message, setMessage] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
const [selectedUsers, setSelectedUsers] = useState<string>("all");
const [currentPage, setCurrentPage] = useState(1);
const [lastPage, setLastPage] = useState(1);
const [listTotal, setListTotal] = useState(0);
useEffect(() => {
index();
}, [currentPage, selectedUsers]);
async function index() {
setLoading(true);
try {
const response = await fetch(`http://127.0.0.1:8000/api/users?page=${currentPage}&filter=${selectedUsers}`, {
method: "GET",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
Authorization: `Bearer ${localStorage.getItem("token")}`
},
});
const data = await response.json();
if (response.ok) {
setUsers(Array.isArray(data.data.data) ? data.data.data : []);
setLastPage(data.data.last_page);
setError(null);
} else {
setUsers([]);
setLastPage(1);
setListTotal(0);
setMessage((data as ApiErrorResponse).message);
setError(data as ApiErrorResponse);
}
} catch (error) {
console.error(error);
} finally {
setLoading(false);
}
}
if (loading) {
return (
<div className="text-center mt-5">
<CgSpinner className={`${styles.animateSpin} text-2xl fs-3`} />
</div>
)
}
if (error) {
return (
<div className="text-center mt-5">
<p>{(error as ApiErrorResponse).message}</p>
</div>
)
}
return (
<div className={`${styles.container} p-3 p-xl-0`}>
<h1 className={styles.title}>Utilizadores</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 Utilizadores</label>
<select
className="form-control select-filter"
name="filter"
id="filter"
value={selectedUsers}
onChange={(e) => {
setSelectedUsers(e.target.value);
setCurrentPage(1);
}}
>
<option key="all" value="all">Todos</option>
<option key="admin" value="admin">Administradores</option>
<option key="user" value="user">Utilizadores</option>
</select>
</div>
<div className="col-12 col-sm-6 text-center text-sm-end align-content-center px-0">
<Link to="/admin/create-user" className={`${styles.createButton} text-decoration-none`}><LuPlus className="mb-1" /> Novo Utilizador</Link>
</div>
</div>
{users.length > 0 && (
<div>
<div className={`${styles.table} mt-3 mb-2`}>
<Table responsive className="mb-0">
<thead className="table-light">
<tr>
<th className="py-3">ID</th>
<th className="py-3">Name</th>
<th className="py-3">Email</th>
<th className="py-3">Cargo</th>
<th className="py-3">...</th>
</tr>
</thead>
<tbody>
{users.map((user) => (
<tr key={user.id}>
<td className="py-3">{user.id}</td>
<td className="py-3">{user.name}</td>
<td className="py-3">{user.email}</td>
<td className="py-3 px-3" >
<span className={`p-2 rounded-3 ${user.role_id === 1 ? 'text-primary bg-primary-subtle' : 'text-secondary bg-secondary-subtle'}`}>
{user.role_id === 1 ? "Administrador" : "Utilizador"}
</span>
</td>
<td className="py-3">
<div className="d-flex gap-2 justify-content-evenly">
<Link to={`/admin/user/${user.id}`}><LuPencil className="text-red-500" /></Link>
</div>
</td>
</tr>
))}
</tbody>
</Table>
</div>
<div className="d-flex justify-content-center align-items-center gap-3 py-3">
<Pagination>
<Pagination.Prev
disabled={currentPage <= 1}
onClick={() => setCurrentPage((p) => p - 1)}
/>
{currentPage > 3 && <Pagination.Item onClick={() => setCurrentPage(1)}>1</Pagination.Item>}
{currentPage > 4 && <Pagination.Ellipsis disabled />}
{Array.from({ length: 5 }, (_, i) => currentPage - 2 + i)
.filter((p) => p >= 1 && p <= lastPage)
.map((p) => (
<Pagination.Item key={p} active={p === currentPage} onClick={() => setCurrentPage(p)}>
{p}
</Pagination.Item>
))}
{currentPage < lastPage - 3 && <Pagination.Ellipsis disabled />}
{currentPage < lastPage - 2 && (
<Pagination.Item onClick={() => setCurrentPage(lastPage)}>{lastPage}</Pagination.Item>
)}
<Pagination.Next
disabled={currentPage >= lastPage}
onClick={() => setCurrentPage((p) => p + 1)}
/>
</Pagination>
{/* <button type="button" className="btn btn-outline-secondary btn-sm" disabled={currentPage <= 1} onClick={() => setCurrentPage((p) => Math.max(1, p - 1))}>
<LuChevronLeft className="fs-5" />
</button>
<span className="text-muted small">
Página {currentPage} de {lastPage}
</span>
<button type="button" className="btn btn-outline-secondary btn-sm" disabled={currentPage >= lastPage} onClick={() => setCurrentPage((p) => Math.min(lastPage, p + 1))}>
<LuChevronRight className="fs-5" />
</button> */}
</div>
</div>
)}
{users.length === 0 && !error && (
<div className="text-center mt-5 d-flex flex-column align-items-center justify-content-center">
{selectedUsers === "all" && listTotal === 0 ? (
<>
<span className="text-muted">Sem registo de utilizadores</span>
<Link to="/admin/create-user" className={`${styles.createButton} text-decoration-none`}><LuPlus className="mb-1" /> Novo Utilizador</Link>
</>
) : (
<span className="text-muted">Nenhum utilizador encontrado com o filtro selecionado</span>
)}
</div>
)}
{error && <p>{(error as ApiErrorResponse).message}</p>}
</div>
)
}

View File

@@ -1,56 +0,0 @@
.container{
width: 100%;
max-width: 1400px;
align-self: start;
}
.title{
color: var(--primary-contrast-color);
font-size: var(--size-font-title);
}
.table{
overflow-y: scroll;
border-radius: var(--border-radius);
box-shadow: var(--shadow);
}
.createButton{
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);
}
}
.pagination .page-item.active .page-link {
background-color: var(--bg-primary-color-opacity) !important;
border-color: var(--border-primary-color) !important;
color: var(--text-primary-color) !important;
}
.page-link{
color: var(--text-neutral-color) !important;
}
.animateSpin{
animation: spin 1s linear infinite;
}
@keyframes spin{
from{
transform: rotate(0deg);
}
to{
transform: rotate(360deg);
}
}

View File

@@ -2,7 +2,7 @@ import { useEffect, useState } from "react";
import { Link, useParams } from "react-router";
import { CgSpinner } from "react-icons/cg";
import type { ApiErrorResponse, User, Workshop } from "../../../../types";
import { LuArrowLeft, LuCalendar, LuCheck, LuClock3, LuPencil, LuTrash2, LuUpload, LuUsers, LuX, LuClipboardList, LuPlus } from "react-icons/lu";
import { LuArrowLeft, LuCalendar, LuCheck, LuClock3, LuPencil, LuTrash2, LuUpload, LuUsers, LuX, LuClipboardList } from "react-icons/lu";
import { pt } from "date-fns/locale";
import DatePicker from "react-datepicker";
import "react-datepicker/dist/react-datepicker.css";
@@ -27,8 +27,6 @@ export default function Workshop() {
const [time_start, setTimeStart] = useState<Date | null>(null);
const [time_end, setTimeEnd] = useState<Date | null>(null);
const [status, setStatus] = useState<string>("");
const [messageDeleteWorkshop, setMessageDeleteWorkshop] = useState<string>("");
const [messageUpdateWorkshop, setMessageUpdateWorkshop] = useState<string>("");
const [listagemInscritos, setListagemInscritos] = useState<boolean>(false);
/* Para enviar a data no formato que o backend espera */
@@ -109,21 +107,44 @@ export default function Workshop() {
const data = await response.json();
if (response.ok) {
setMessageUpdateWorkshop(data.message as string);
setTimeout(() => {
setMessageUpdateWorkshop("");
}, 3000)
setError(null);
Swal.fire({
title: data.message as string,
icon: 'success',
showConfirmButton: false,
showCloseButton: true,
});
if(overrides?.status === "canceled") {
Swal.fire({
title: "Workshop cancelado com sucesso",
icon: 'error',
showConfirmButton: false,
showCloseButton: true,
});
}
getWorkshop();
setFormEdit(false);
} else {
setMessageUpdateWorkshop("");
setError(data as ApiErrorResponse);
Swal.fire({
title: data.message as string,
icon: 'error',
showConfirmButton: false,
showCloseButton: false,
});
}
}
async function inscrever() {
function handleCancelWorkshop() {
Swal.fire({
text: 'Tem a certeza que deseja cancelar este workshop?',
showCancelButton: true,
confirmButtonText: 'Sim, cancelar',
confirmButtonColor: 'var(--primary-color)',
cancelButtonText: 'Cancelar',
}).then((result) => {
if (result.isConfirmed) {
update(undefined, { status: "canceled" })
}
});
}
/* async function destroy(id: number) {
@@ -184,14 +205,6 @@ export default function Workshop() {
{workshop ? (
<>
<div className={`${styles.container} d-flex flex-column gap-2`}>
{messageUpdateWorkshop ? (
<div className="">
<div className="alert alert-success mt-4">
{messageUpdateWorkshop}
</div>
</div>
) : null}
<span className={styles.title}>{workshop.title}</span>
<div className="d-flex flex-column flex-md-row mt-4 gap-2 gap-md-4 gap-lg-5">
@@ -228,12 +241,6 @@ export default function Workshop() {
</a>
</div>
) : null}
{!isAdmin ? (
<div className={`${styles.buttonsContainer} mt-3 d-flex flex-wrap gap-2`}>
<button type="button" className={`${styles.btnInscrever} btn text-center mt-3 py-2 px-5 text-decoration-none`} onClick={inscrever}>Inscrever-Me</button>
</div>
) : null}
</div>
</div>
@@ -260,9 +267,7 @@ export default function Workshop() {
<LuPencil className="mb-1" /> Editar
</button>
{workshop.status === "pending" ? (
<button type="button" className={`${styles.cancelButton}`} onClick={() => update(undefined, { status: "canceled" })}><LuTrash2 className="mb-1 btn-danger" /> Cancelar Workshop</button>
) : workshop.status === "canceled" ? (
<button type="button" className={`${styles.activateButton}`} onClick={() => update(undefined, { status: "pending" })}><LuPlus className="mb-1" /> Ativar Workshop</button>
<button type="button" className={`${styles.cancelButton}`} onClick={handleCancelWorkshop}><LuTrash2 className="mb-1 btn-danger" /> Cancelar Workshop</button>
) : null}
</div>
) : null}
@@ -443,8 +448,6 @@ export default function Workshop() {
</form>
) : null}
</>
) : messageDeleteWorkshop ? (
<div className="alert alert-success mt-4">{messageDeleteWorkshop}</div>
) : (
<div className="text-center alert alert-danger mt-5 align-items-center">
<span>Workshop não encontrado</span>