feat- Organização da estrutura da app
This commit is contained in:
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -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>
|
|
||||||
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,7 +2,7 @@ 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, 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 { pt } from "date-fns/locale";
|
||||||
import DatePicker from "react-datepicker";
|
import DatePicker from "react-datepicker";
|
||||||
import "react-datepicker/dist/react-datepicker.css";
|
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_start, setTimeStart] = useState<Date | null>(null);
|
||||||
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 [messageDeleteWorkshop, setMessageDeleteWorkshop] = useState<string>("");
|
|
||||||
const [messageUpdateWorkshop, setMessageUpdateWorkshop] = useState<string>("");
|
|
||||||
const [listagemInscritos, setListagemInscritos] = useState<boolean>(false);
|
const [listagemInscritos, setListagemInscritos] = useState<boolean>(false);
|
||||||
|
|
||||||
/* Para enviar a data no formato que o backend espera */
|
/* Para enviar a data no formato que o backend espera */
|
||||||
@@ -109,21 +107,44 @@ export default function Workshop() {
|
|||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
setMessageUpdateWorkshop(data.message as string);
|
Swal.fire({
|
||||||
setTimeout(() => {
|
title: data.message as string,
|
||||||
setMessageUpdateWorkshop("");
|
icon: 'success',
|
||||||
}, 3000)
|
showConfirmButton: false,
|
||||||
setError(null);
|
showCloseButton: true,
|
||||||
|
});
|
||||||
|
if(overrides?.status === "canceled") {
|
||||||
|
Swal.fire({
|
||||||
|
title: "Workshop cancelado com sucesso",
|
||||||
|
icon: 'error',
|
||||||
|
showConfirmButton: false,
|
||||||
|
showCloseButton: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
getWorkshop();
|
getWorkshop();
|
||||||
setFormEdit(false);
|
setFormEdit(false);
|
||||||
} else {
|
} else {
|
||||||
setMessageUpdateWorkshop("");
|
Swal.fire({
|
||||||
setError(data as ApiErrorResponse);
|
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) {
|
/* async function destroy(id: number) {
|
||||||
@@ -184,14 +205,6 @@ export default function Workshop() {
|
|||||||
{workshop ? (
|
{workshop ? (
|
||||||
<>
|
<>
|
||||||
<div className={`${styles.container} d-flex flex-column gap-2`}>
|
<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>
|
<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">
|
<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>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : 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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -260,9 +267,7 @@ export default function Workshop() {
|
|||||||
<LuPencil className="mb-1" /> Editar
|
<LuPencil className="mb-1" /> Editar
|
||||||
</button>
|
</button>
|
||||||
{workshop.status === "pending" ? (
|
{workshop.status === "pending" ? (
|
||||||
<button type="button" className={`${styles.cancelButton}`} onClick={() => update(undefined, { status: "canceled" })}><LuTrash2 className="mb-1 btn-danger" /> Cancelar Workshop</button>
|
<button type="button" className={`${styles.cancelButton}`} onClick={handleCancelWorkshop}><LuTrash2 className="mb-1 btn-danger" /> Cancelar Workshop</button>
|
||||||
) : workshop.status === "canceled" ? (
|
|
||||||
<button type="button" className={`${styles.activateButton}`} onClick={() => update(undefined, { status: "pending" })}><LuPlus className="mb-1" /> Ativar Workshop</button>
|
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -443,8 +448,6 @@ export default function Workshop() {
|
|||||||
</form>
|
</form>
|
||||||
) : null}
|
) : null}
|
||||||
</>
|
</>
|
||||||
) : messageDeleteWorkshop ? (
|
|
||||||
<div className="alert alert-success mt-4">{messageDeleteWorkshop}</div>
|
|
||||||
) : (
|
) : (
|
||||||
<div className="text-center alert alert-danger mt-5 align-items-center">
|
<div className="text-center alert alert-danger mt-5 align-items-center">
|
||||||
<span>Workshop não encontrado</span>
|
<span>Workshop não encontrado</span>
|
||||||
|
|||||||
Reference in New Issue
Block a user