diff --git a/script.py b/script.py new file mode 100644 index 0000000000000000000000000000000000000000..fbeea938d967c3290bf2c32282d47cf4898017f6 --- /dev/null +++ b/script.py @@ -0,0 +1,67 @@ +import requests + +# url da rota +url = 'http://localhost:3000/api/resource/create' + +# seu token jwt +token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwiZW1haWwiOiJhZG1pbkBhZG1pbi5jb20ifQ.HkGzCxkCCIKOmDy3FZ7enfaKQUQdAr4QudcBGJtZAEg' + +# json de entrada conforme o esperado pelo backend +data = { + "name": "recurso do thomas ", + "description": "descrição do recurso", + "author": "autorrrr", + "link": "www.gov.br", + "subjects": [1, 2], # ids das disciplinas + "language": [1], # ids dos idiomas + "educational_stages": [1, 2], # ids dos estágios educacionais + "license_id": 1, + "state": "accepted", + "object_type_id": 1, +} + +# cabeçalhos, incluindo o token de autorização +headers = { + 'Authorization': f'Bearer {token}', + 'Content-Type': 'application/json' +} + +original_title = data["name"] + +# faz a requisição post +for i in range(500): + data["name"] = f"{original_title} {i}" + response = requests.post( + url, + json=data, + headers=headers) + + # imprime o resultado + print(response) + print('status:', response.status_code) + print('resposta:', response.json()) + + + # 2. se deu certo, pega o id do recurso + if response.status_code == 200: + resource_id = response.json().get('id') # ajusta aqui conforme a chave real da resposta + + # 3. faz o upload da thumbnail + url_upload = 'http://localhost:3000/api/s3/upload/thumbnail/resource' + headers_upload = { + 'Authorization': f'Bearer {token}', + } + + files = { + 'file': ('thumbnail.png', open('thumbnail.png', 'rb'), 'image/png'), + 'id_resource': (None, str(resource_id)), + 'content_type': (None, 'image/png'), + } + + upload_response = requests.post(url_upload, files=files, headers=headers_upload) + print('upload status:', upload_response.status_code) + print('upload resposta:', upload_response.json()) + else: + print('erro ao criar recurso') + + print(i) \ No newline at end of file diff --git a/src/app/biblioteca/page.js b/src/app/biblioteca/page.js new file mode 100644 index 0000000000000000000000000000000000000000..85a4076429db245cf6c1789108c0190fb79c7913 --- /dev/null +++ b/src/app/biblioteca/page.js @@ -0,0 +1,34 @@ + +import { Suspense } from "react"; +import Loading from "../components/Loading"; +import Content from "../components/Content"; + + + +function tradutor(name) { + switch (name) { + case "resources": + return "Recursos" + case "collections": + return "Coleções" + case "MEC": + return "MEC" + case "users": + return "Usuários" + default: + return + } +} + +/** + * @param {Object} props + * @param {string} props.inputFilter +*/ +export default function Library({ searchParams }) { + return ( + <Suspense fallback={<Loading />}> + <Content searchParams={searchParams}/> + </Suspense> + ) + +} \ No newline at end of file diff --git a/src/app/busca/page.js b/src/app/busca/page.js index 896f7b1983a7a727cec572928aa1b7c0aac279db..432a56af9d59d91cdcb953d2499a15ff6743007f 100644 --- a/src/app/busca/page.js +++ b/src/app/busca/page.js @@ -9,13 +9,13 @@ import Content from "../components/Content"; function tradutor(name) { switch (name) { - case "LearningObject": + case "resources": return "Recursos" - case "Collection": + case "collections": return "Coleções" case "MEC": return "MEC" - case "User": + case "users": return "Usuários" default: return @@ -24,7 +24,6 @@ function tradutor(name) { /** * @param {Object} props - * @param {string} props.name informna se é recursos, coleções ou MEC * @param {string} props.inputFilter */ export default function Busca() { diff --git a/src/app/components/About.js b/src/app/components/About.js index cd8d63e9a1b891e6de4418a2133de15164a076fb..eb829269af706c29c65c6ed88ad9f5e5f18190dd 100644 --- a/src/app/components/About.js +++ b/src/app/components/About.js @@ -16,64 +16,64 @@ import AccountCircleRoundedIcon from '@mui/icons-material/AccountCircleRounded'; * @returns tela de sobre */ export default function AboutComponent() { - const [statistics, setStatistics] = useState({}); + // const [statistics, setStatistics] = useState({}); - useEffect(() => { - mecredApi - .get("/statistics") - .then(({ data }) => { - setStatistics(data); - }) - .catch((error) => console.error(error)); - }, []); + // useEffect(() => { + // mecredApi + // .get("/statistics") + // .then(({ data }) => { + // setStatistics(data); + // }) + // .catch((error) => console.error(error)); + // }, []); - const StatisticInfo = ({ name, data, color, icon }) => { - return ( - <div className="flex flex-col items-center text-center max-sm:flex-row"> - <div - className={`h-24 w-24 my-5 mx-10 pt-5 flex justify-center rounded-full ${color["bg"]}`} - > - <Image - className={`rounded-lg w-14 h-14 invertIcon-HC-black`} - style={{ }} - alt={name} - src={icon} - width={10} - height={10} - /> - </div> - <div> - <h1 className={`text-xl mb-1 font-bold max-sm:text-left ${color["text"]}`}>{data}</h1> - <h2 className={`text-base leading-tight font-bold max-sm:text-left ${color["text"]}`}>{name}</h2> - </div> - </div> - ); - }; + // const StatisticInfo = ({ name, data, color, icon }) => { + // return ( + // <div className="flex flex-col items-center text-center max-sm:flex-row"> + // <div + // className={`h-24 w-24 my-5 mx-10 pt-5 flex justify-center rounded-full ${color["bg"]}`} + // > + // <Image + // className={`rounded-lg w-14 h-14 invertIcon-HC-black`} + // style={{ }} + // alt={name} + // src={icon} + // width={10} + // height={10} + // /> + // </div> + // <div> + // <h1 className={`text-xl mb-1 font-bold max-sm:text-left ${color["text"]}`}>{data}</h1> + // <h2 className={`text-base leading-tight font-bold max-sm:text-left ${color["text"]}`}>{name}</h2> + // </div> + // </div> + // ); + // }; - const Statistics = () => { - return ( - <div className="flex max-sm:flex-col mb-10"> - <StatisticInfo - name={<p>Recursos <br /> Disponíveis</p>} - data={statistics["count"]} - color={{ text: "text-orange-HC-white", bg: "bg-orange-HC-white" }} - icon="/redigitais.svg" - /> - <StatisticInfo - name={<p>Recursos <br /> Visualizados <br /> por mês</p>} - data={statistics["month_downloads"]} - color={{ text: "text-violet-HC-white", bg: "bg-violet-HC-white" }} - icon="/download.svg" - /> - <StatisticInfo - name={<p>Usuários <br /> Cadastrados </p>} - data="31207" - color={{ text: "text-pink-HC-white", bg: "bg-pink-HC-white" }} - icon="/seguir.svg" - /> - </div> - ); - }; + // const Statistics = () => { + // return ( + // <div className="flex max-sm:flex-col mb-10"> + // <StatisticInfo + // name={<p>Recursos <br /> Disponíveis</p>} + // data={statistics["count"]} + // color={{ text: "text-orange-HC-white", bg: "bg-orange-HC-white" }} + // icon="/redigitais.svg" + // /> + // <StatisticInfo + // name={<p>Recursos <br /> Visualizados <br /> por mês</p>} + // data={statistics["month_downloads"]} + // color={{ text: "text-violet-HC-white", bg: "bg-violet-HC-white" }} + // icon="/download.svg" + // /> + // <StatisticInfo + // name={<p>Usuários <br /> Cadastrados </p>} + // data="31207" + // color={{ text: "text-pink-HC-white", bg: "bg-pink-HC-white" }} + // icon="/seguir.svg" + // /> + // </div> + // ); + // }; const ActorInfo = ({ name, description, nameImage }) => { return ( @@ -338,7 +338,7 @@ export default function AboutComponent() { <div> <Title /> </div> - <Statistics /> + {/* <Statistics /> */} </div> <div className="flex flex-col text-center items-center mt-12 rounded-lg bg-white-HC-dark max-sm:hidden outline outline-1 outline-ice-HC-white"> <Actors /> @@ -379,7 +379,7 @@ export default function AboutComponent() { </p> <Button - href="/busca?page=LearningObject" + href="/busca?page=resources" className="bg-turquoise-HC-white mt-2 text-xl text-white-HC-dark-underline py-4 w-full text-center rounded-lg hover:bg-darkTurquoise-HC-dark hover:text-white font-bold normal-case outline outline-1 outline-white" > Continuar diff --git a/src/app/components/CardsLibrary.js b/src/app/components/CardsLibrary.js new file mode 100644 index 0000000000000000000000000000000000000000..9caf04f52bdeb63dd518f531b78ab1e9a801d9e6 --- /dev/null +++ b/src/app/components/CardsLibrary.js @@ -0,0 +1,211 @@ +import PersonAddIcon from '@mui/icons-material/PersonAdd'; +import EmojiEventsIcon from '@mui/icons-material/EmojiEvents'; +import ContentPasteIcon from '@mui/icons-material/ContentPaste'; + +const timeFunction = (updated_time) => { + let data = new Date(updated_time) + let dataAtual = new Date(); + + let time = dataAtual.getTime() - data.getTime(); + let dia = Math.floor(time / (1000 * 60 * 60 * 24)); + let ano; + let mes; + + if ((ano = Math.floor(dia / 365)) > 0) + return <p> há {ano} {ano === 1 ? "ano" : "anos"} </p> + else if (((mes = Math.floor(dia / 31)) > 0)) + return <p> há {mes} {mes === 1 ? "mês" : "meses"}</p> + + if (dia === 0) + return <p>hoje</p> + return <p>há {dia} {dia === 1 ? "dia" : "dias"}</p> + +} + +function getRandomBg(id) { + const colors = [ + "bg-turquoise", + "bg-orange", + "bg-turquoise-hover", + "bg-darkOrange-HC-gray ", + "bg-violet", + "bg-pink", + "bg-red", + "bg-darkGray-HC-white", + "bg-darkGray-HC-white-click", + "bg-ice-HC-dark ", + "bg-darkGray-HC-white", + "bg-darkGray-HC-white", + "bg-turquoise-HC-dark", + ] + + return colors[id % colors.length]; +} + + + + + +function AvatarUsuario({ src, alt = "Avatar" }) { + return src ? + <img + src={src} + alt={alt} + className="w-[45px] h-[45px] bg-darkGray-HC-dark rounded-full object-cover" + /> + : <div className="flex items-center justify-center text-xl font-bold ml-1 text-ice-HC-dark rounded-full h-[33px] w-[33px] bg-gray-300"> + A + </div> +} + + + + +function CardsResources({ }) { + return <> + <div className="flex mt-4 "> + <AvatarUsuario /> + <div className="flex flex-col ml-4 max-sm:ml-0 max-sm:justify-stretch"> + <div className="line-clamp-2 text-lg font-bold text-darkGray-HC-white-underline min-h-4 max-sm:w-full"> + Título de exemplo do recurso + </div> + <div className="flex flex-row"> + <div className="flex flex-col"> + <div className="line-clamp-1 text-darkGray-HC-white text-sm font-light"> + Autor Exemplo + </div> + <div className="flex items-center gap-1 text-darkGray-HC-white text-sm font-light"> + <span className="text-gray-400 text-xl">★</span> + <p>4.8</p> + <span className="inline-block w-1.5 h-1.5 bg-gray-400 rounded-full mx-1"></span> + <p>há 2 dias</p> + <span className="inline-block w-1.5 h-1.5 bg-gray-400 rounded-full mx-1"></span> + <p>2,4 mil visualizações</p> + </div> + </div> + </div> + </div> + </div> + </> +} + + +function CardCollections({ }) { + return <div className="flex mt-4 "> + <AvatarUsuario /> + <div className="flex flex-col ml-4 max-sm:ml-0 max-sm:justify-stretch"> + <div className="line-clamp-2 text-lg font-bold text-darkGray-HC-white-underline min-h-4 max-sm:w-full"> + Título da colecao + </div> + <div className="flex flex-row"> + <div className="flex flex-col"> + <div className="line-clamp-1 text-darkGray-HC-white text-sm font-light"> + Autor Exemplo + </div> + <div className="flex items-center gap-1 text-darkGray-HC-white text-sm font-light"> + + <svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}> + <path strokeLinecap="round" strokeLinejoin="round" d="M12 2l9 6-9 6-9-6 9-6z" /> + <path strokeLinecap="round" strokeLinejoin="round" d="M12 14l9-6v6l-9 6-9-6v-6l9 6z" /> + </svg> + + <span>15 recursos</span> + <span className="inline-block w-1.5 h-1.5 bg-gray-400 rounded-full mx-2"></span> + <span>há 2 dias</span> + </div> + + </div> + </div> + </div> + </div> + +} + +function CardUsers({ }) { + return ( + <div className="bg-[#f0f6f7] rounded-xl p-6 w-[280px] text-center text-gray-600 relative select-none"> + {/* ícone de adicionar usuário no topo esquerdo */} + {/* onclick para seguir, onclick para deseguir se ja segue */} + <div className="absolute top-4 left-4 text-gray-400"> + <PersonAddIcon/> + </div> + + + {/* avatar */} + <div className="mx-auto mb-4 w-24 h-24 rounded-full bg-gray-300 flex items-center justify-center text-white text-4xl font-bold"> + R + </div> + + {/* nome */} + <h2 className="font-semibold text-darkGray-HC-white-underline text-lg mb-1">Roberto Pereira</h2> + + {/* profissão */} + <p className="line-clamp-1 text-darkGray-HC-white-underline text-sm mb-0.5">Professor da E.E. Américo...</p> + + {/* local */} + <p className="text-darkGray-HC-white-underline text-sm mb-6">Curitiba (PR)</p> + + {/* linha separadora */} + <hr className="border-gray-300 mb-6" /> + + {/* informações com ícones */} + <div className="grid grid-cols-2 gap-y-4 text-darkGray-HC-white-underline text-xs font-light"> + + {/* recursos */} + <div className="flex items-center gap-2 "> + <ContentPasteIcon fontSize="small" /> + <span>Recursos 25</span> + </div> + + {/* seguidores */} + <div className="flex items-center gap-2"> + <span>Seguidores 23</span> + </div> + + {/* insígnias */} + <div className="flex items-center gap-2"> + <EmojiEventsIcon fontSize="small" /> + <span>Insígnias 6</span> + </div> + + {/* seguindo */} + <div className="flex items-center gap-2"> + + <span>Seguindo 126</span> + </div> + + </div> + </div> + ); + +} + + +export default function CardsLibrary({ page }) { + return ( + <div + tabIndex="-1" + className="transition ease-in-out active:bg-ice-HC-dark active:rounded-3xl 2xl:w-[400px] xl:w-[360px] lg:w-[340px] max-lg:w-[320px] flex flex-col" + > + <a href="#" className="flex flex-col"> + {(page === "Recursos" || page === "Coleções") && ( + <img + tabIndex="0" + src="https://via.placeholder.com/327x181" + alt="imagem" + title="Título do conteúdo" + className="hover:scale-[1.02] p-1 focus:border-turquoise-HC-white bg-slate-600 focus:border-4 border-gray-color transition-transform rounded-xl aspect-video w-[327px] h-[181px] object-cover" + /> + )} + + {page === "Recursos" && <CardsResources />} + + {page === "Coleções" && <CardCollections />} + + {page === "Usuários" && <CardUsers />} + + + </a> + </div> + ); +} diff --git a/src/app/components/Content.js b/src/app/components/Content.js index 51baea37c829305cf8d60119b6054b4d3b241afd..d5df6ce26cafa1cdd437c880b71d803fd29fe220 100644 --- a/src/app/components/Content.js +++ b/src/app/components/Content.js @@ -4,117 +4,338 @@ import { useEffect, useState } from "react"; import InfiniteScroll from "../components/InfiniteScroll"; import Overlay from "../components/Overlay"; import GroupButtonsFilters from "./GroupButtonsFilters"; -import FiltersModal from "./FiltersModal"; -import { useSearchParams, useRouter } from "next/navigation"; - -function tradutor(name) { - switch (name) { - case "LearningObject": - return "Recursos" - case "Collection": - return "Coleções" - case "MEC": - return "MEC" - case "User": - return "Usuários" - default: - return - } +import TuneIcon from '@mui/icons-material/Tune'; +import CloseIcon from '@mui/icons-material/Close'; +import { useSearchParams, useRouter, usePathname } from "next/navigation"; +import TypeAndFilterModal from "./TypeAndFilterModal"; +import SearchIcon from '@mui/icons-material/Search'; +import CardsLibrary from "./CardsLibrary"; + + + + + +function CategoryAndFilterButtons({ searchParams, filters, setFilters }) { + const router = useRouter(); + const pathname = usePathname(); + const [open, setOpen] = useState(false); + const handleOpen = () => setOpen(true); + const handleClose = () => setOpen(false); + + + const categories = [ + { name: "tudo", text: "Tudo" }, + { name: "mec", text: "MEC Recomenda" }, + { name: "recentes", text: "+ Recentes" }, + { name: "relevantes", text: "+ Relevantes" }, + { name: "comentados", text: "+ Comentados" }, + { name: "avaliados", text: "+ Avaliados" }, + { name: "baixados", text: "+ Baixados" }, + // { name: "salvos", text: "+ Salvos" }, + { name: "compartilhados", text: "+ Compartilhados" }]; + + const currentCategory = searchParams["categoria"] ? searchParams["categoria"] : "tudo"; + + const updateSearchParams = + (name, value) => { + const params = new URLSearchParams(searchParams); + params.set(name, value) + const url = pathname + '?' + params.toString(); + router.push(url); + } + + return <div className={`fixed flex flex-row gap-1 ml-36 pb-4 overflow-auto w-full bg-fundo bg-repeat bg-fixed max-md:ml-3 z-30 animate-scrollHint ${filters.pesquisa ? "mt-14" : ""}`}> + {categories.map(({ name, text }) => { + return <button key={name} onClick={() => updateSearchParams("categoria", name)} className={"p-2 rounded-md shrink-0 " + (currentCategory === name ? "bg-turquoise-HC-dark text-white" : "bg-lightGray-HC-dark text-darkGray-HC-white")}> {text}</button> + })} + <button startIcon={<TuneIcon />} onClick={handleOpen} className='bg-darkGray-HC-white hover:bg-mediumGray-HC-dark font-semibold normal-case text-white-HC-dark-underline hover:text-white rounded-lg px-3 mr-2 outline outline-1 outline-ice-HC-white'>Filtros</button> + <TypeAndFilterModal searchParams={searchParams} open={open} handleClose={handleClose} filters={filters} setFilters={setFilters} /> + </div> +} + + + +function AllResult({ filters }) { + const categories = [ + { name: "recentes", text: "mais recentes", entity: { collections: true, resources: true, users: true } }, + { name: "relevantes", text: "mais relevantes", entity: { collections: true, resources: true, users: true } }, // recurso e colecao e usuario + { name: "comentados", text: "mais comentados", entity: { collections: false, resources: true, users: false } }, //recurso + { name: "avaliados", text: "melhores avaliados", entity: { collections: false, resources: true, users: false } }, //recurso + { name: "baixados", text: "mais baixados", entity: { collections: true, resources: true, users: false } },//recurso e colecoes + // { name: "salvos", text: "+ Salvos", collections: true, resources: false, users: false }, + { name: "compartilhados", text: "mais compartilhados", entity: { collections: true, resources: true, users: false } }//recurso e colecao + ]; + + // fazer requisicao para o mec separada + const entityLabels = { + collections: "Coleções", + resources: "Recursos", + users: "Usuários" + }; + + return ( + <div className={`w-screen overflow-auto mt-10 ${filters.pesquisa !== "" ? "mt-36" : ""}`}> + <div className="bg-white flex flex-col justify-between rounded-md max-w-screen-xl h-[28rem] mb-20 p-4 shadow"> + <p className="text-2xl font-semibold text-darkGray-HC-white mb-4">Coleções Recomendadas pelo MEC </p> + {/* AQUI FAZ A REQUISIÇÃO PARA O BACK SÓ DAS COISAS DO MEC */} + <div className="flex gap-4 "> + / /cards aqui + </div> + <button className="flex justify-center items-center hover:bg-ice rounded-md font-semibold text-darkGray-HC-white h-10 mt-4 "> + Ver mais... + </button> + </div> + {categories.map(category => ( + <div key={category.name} > + {Object.entries(category.entity).map(([entityKey, isEnabled]) => ( + isEnabled && ( + <div key={entityKey} className={`bg-white flex flex-col justify-between rounded-md max-w-screen-xl mb-20 p-4 shadow ${entityLabels[entityKey] === 'Usuários' ? 'h-[32rem]' : 'h-[28rem]'}`}> + <p className="text-2xl font-semibold text-darkGray-HC-white mb-8 mt-2 mx-4 "> {entityLabels[entityKey]} {category.text}</p> + {/* AQUI FAZ A REQUISIÇÃO PARA O BACK */} + <div className="flex gap-4 mx-4 "> + <CardsLibrary page={entityLabels[entityKey]} /> + </div> + <button className="flex justify-center items-center hover:bg-ice rounded-md font-semibold text-darkGray-HC-white h-10 mt-4 "> + Ver mais... + </button> + </div> + ) + ))} + </div> + ))} + </div> + ); +} + +function FilterMessageAndCategory({ searchParams = { searchParams } }) { + + const [entity, setEntity] = useState(searchParams["entidade"] ? searchParams["entidade"] : "recursos"); + const query = searchParams["pesquisa"] ? searchParams["pesquisa"] : ""; + const params = new URLSearchParams(searchParams); + const pathname = usePathname(); + const router = useRouter(); + + + const labelMap = { + recursos: "Recursos", + colecoes: "Coleções", + usuarios: "Usuários", + }; + + return <div className="fixed flex flex-row gap-1 h-78 ml-36 overflow-auto w-full bg-fundo bg-repeat bg-fixed max-md:ml-3 z-30 animate-scrollHint"> + <div className="flex "> + <p className=" text-darkGray-HC-white-underline text-lg"> + <SearchIcon /> Exibindo resultados da busca por <a className="font-bold"> "{query}" </a> na categoria: + </p> + <div className="flex text-darkGray-HC-white-underline ml-4 gap-8 items-center"> + {["recursos", "colecoes", "usuarios"].map((label) => ( + <label key={label} className="flex items-center gap-2 cursor-pointer text-gray-700 text-lg"> + <input + type="radio" + name="entity" + value={label} + checked={entity === label} + onChange={() => { + setEntity(label); params.set("entidade", label); const url = pathname + '?' + params.toString(); + router.push(url); + }} + className="accent-cyan-500 w-4 h-4" + /> + {labelMap[label]} + </label> + ))} + </div> + </div> + </div> } -function filterTradutor(name) { - switch (name) { - case "publicationdesc": - return "recentes" - case "score": - return "relevantes" - case "likes": - return "colecionados" - case "downloads": - return "visualizados" - } +//AQUI: +//QUANDO CLICA EM +RECENTE, POR PADRAO VAI PARA RECURSOS +// APARECEER: EXIBINDO RESULTADOS DA BUSCA NA CATEGORIA ... + + + +export default function Content({ searchParams }) { + const [filters, setFilters] = useState({ + pesquisa: [searchParams["pesquisa"] ? searchParams["pesquisa"] : ""], + formato: [searchParams["formato"] ? searchParams["formato"] : []], + nivel: [searchParams["nivel"] ? searchParams["nivel"] : []], + idioma: [searchParams["idioma"] ? searchParams["idioma"] : []], + materias: [searchParams["materias"] ? searchParams["materias"] : []], + entity: [searchParams["entidade"] ? searchParams["entidade"] : "recursos"] + }); + const [currentCategory, setCurrentCategory] = useState(searchParams["categoria"] ? searchParams["categoria"] : "tudo"); + + useEffect(() => { + const params = new URLSearchParams(searchParams); + + setFilters({ + pesquisa: params.get("pesquisa") ? params.get("pesquisa") : "", + formato: params.get("formato") ? params.get("formato").split(",") : [], + nivel: params.get("nivel") ? params.get("nivel").split(",") : [], + idioma: params.get("idioma") ? params.get("idioma").split(",") : [], + materias: params.get("materias") ? params.get("materias").split(",") : [], + entity: [params.get("entidade") || "recursos"] + }); + + setCurrentCategory(searchParams["categoria"] ? searchParams["categoria"] : "tudo") + }, [searchParams]); + + + + + return <Overlay searchParams={searchParams} > + {(filters.pesquisa !== "") && <FilterMessageAndCategory searchParams={searchParams} />} + + <CategoryAndFilterButtons searchParams={searchParams} filters={filters} setFilters={setFilters} /> + + {/* sem filtraar os booes ali*/} + {currentCategory === "tudo" ? + <AllResult filters={filters} /> + : + <></> + } + </Overlay>; } + + + + + + + /** * @param {Object} props * @param {string} props.name informna se é recursos, coleções ou MEC * @param {string} props.inputFilter */ -export default function Content({ name, inputFilter, searchPage }) { +export function Contenta({ name, inputFilter, searchPage }) { const [titlePage, setTitlePage] = useState("Recentes"); const [newSize, setNewSize] = useState(false); const [activeFilters, setActiveFilters] = useState(false) const router = useRouter() - + //tem que tirar os ids e colocar as strings const [filterState, setFilterState] = useState({ languages: [], // em ids - searchClass: searchPage ? searchPage : "LearningObject", // lo, user, collection + searchClass: searchPage ? searchPage : "resources", // lo, user, collection subjects: [], // matéria (portugues, matemática) objectTypes: [], // pdf, video, etc educationalStages: [], // ensino fundamental, médio, etc - query: "*", // string de busca - order: "publicationdesc", // ordem + query: "", // string de busca, caso nao tenha query mandar vazia + order: "created_at", // ordem title: "Recentes" }); + const [abortControllers, setAbortControllers] = useState({ + Resources: new AbortController(), + Collection: new AbortController(), + User: new AbortController(), + MEC: new AbortController(), + }); + + const [items, setItems] = useState({ + Resources: [], + Collection: [], + User: [], + MEC: [], + }); + + useEffect(() => { setTitlePage(filterState.title) }, [filterState]) - const itemsBySearchClass = {}; - const setItemsBySearchClass = {}; - const abortControllerBySearchClass = {}; - const setAbortControllerBySearchClass = {}; - - [abortControllerBySearchClass["LearningObject"], setAbortControllerBySearchClass["LearningObject"]] = useState(new AbortController()); - [abortControllerBySearchClass["Collection"], setAbortControllerBySearchClass["Collection"]] = useState(new AbortController()); - [abortControllerBySearchClass["User"], setAbortControllerBySearchClass["User"]] = useState(new AbortController()); - [abortControllerBySearchClass["MEC"], setAbortControllerBySearchClass["MEC"]] = useState(new AbortController()); - [itemsBySearchClass["LearningObject"], setItemsBySearchClass["LearningObject"]] = useState([]); - [itemsBySearchClass["Collection"], setItemsBySearchClass["Collection"]] = useState([]); - [itemsBySearchClass["User"], setItemsBySearchClass["User"]] = useState([]); - [itemsBySearchClass["MEC"], setItemsBySearchClass["MEC"]] = useState([]); useEffect(() => { - for (const [searchClass, abortController] of Object.entries(abortControllerBySearchClass)) { - setItemsBySearchClass[searchClass]([]); - abortController.abort(); - if (searchClass === searchPage) { - setAbortControllerBySearchClass[searchClass](new AbortController()); - } - } - setFilterState(old => { return { ...old, searchClass: searchPage, query: (inputFilter === null ? "*" : inputFilter) } }) - }, [inputFilter, searchPage]) + // Aborta todos os abortControllers existentes + Object.values(abortControllers).forEach(controller => controller.abort()); + + // Cria um novo AbortController apenas para a searchPage atual + setAbortControllers(prev => ({ + ...prev, + [searchPage]: new AbortController(), + })); + + // Limpa os itens da searchPage atual + setItems(prev => ({ + ...prev, + [searchPage]: [], + })); + + // Atualiza o estado do filtro + setFilterState(prev => ({ + ...prev, + searchClass: searchPage, + query: inputFilter ?? "", + })); + }, [inputFilter, searchPage]); + {/* GroupButtonsFilters: Botões para seleção do tipo de filtro usado (selectFilter) */ } //caso MEC não apresenta o GroupButtonsFilters pois não há conteudo suficiente para ser filtrado return ( - <Overlay filterState={filterState} setFilterState={setFilterState} setNewSize={setNewSize} newSize={newSize} type="twoColumns"> + <Overlay + filterState={filterState} + setFilterState={setFilterState} + setNewSize={setNewSize} + newSize={newSize} + type="twoColumns" + > <> - {(filterState.searchClass !== "MEC" && filterState.searchClass != "User") ? - ( - <div> - <div className="pl-3 max-sm:pl-1 fixed w-full bg-fundo bg-repeat bg-fixed max-md:ml-3 z-30" > - <h1 className="text-2xl ml-5 font-bold text-darkGray-HC-white"> - {tradutor(filterState.searchClass)} {(filterState.order === "title") ? "por Ordem alfabética" : " mais " + titlePage} - </h1> - <div className={`flex w-full justify-between`}> - <GroupButtonsFilters pageName={searchPage} activeFilters={activeFilters} setActiveFilters={setActiveFilters} filterState={filterState} setFilterState={setFilterState} setItems={setItemsBySearchClass[filterState.searchClass]} /> - </div> - </div> - {/*caso tenha mais de 15 filtros ativos, o InfiniteScroll é renderizado com um padding-top maior, esse valor foi definido empiricamente*/} - <div className="pt-36 max-md:pt-28"> - <InfiniteScroll filterState={filterState} setNewSize={setNewSize} setItems={setItemsBySearchClass[filterState.searchClass]} items={itemsBySearchClass[filterState.searchClass]} abortController={abortControllerBySearchClass[filterState.searchClass]} /> + {(filterState.searchClass !== "MEC" && filterState.searchClass !== "User") ? ( + <div> + <div className="pl-3 max-sm:pl-1 fixed w-full bg-fundo bg-repeat bg-fixed max-md:ml-3 z-30"> + <h1 className="text-2xl ml-5 font-bold text-darkGray-HC-white"> + {tradutor(filterState.searchClass)}{" "} + {(filterState.order === "title") + ? "por Ordem alfabética" + : " mais " + titlePage} + </h1> + <div className="flex w-full justify-between"> + <GroupButtonsFilters + pageName={searchPage} + activeFilters={activeFilters} + setActiveFilters={setActiveFilters} + filterState={filterState} + setFilterState={setFilterState} + setItems={(items) => setItems(prev => ({ + ...prev, + [filterState.searchClass]: items, + }))} + /> </div> </div> - ) - : - <> - <InfiniteScroll filterState={filterState} setNewSize={setNewSize} setItems={setItemsBySearchClass[filterState.searchClass]} items={itemsBySearchClass[filterState.searchClass]} abortController={abortControllerBySearchClass[filterState.searchClass]} /> - </> - } + <div className="pt-36 max-md:pt-28"> + <InfiniteScroll + filterState={filterState} + setNewSize={setNewSize} + setItems={(items) => setItems(prev => ( + { + ...prev, + [filterState.searchClass]: items, + } + ))} + items={items[filterState.searchClass]} + abortController={abortControllers[filterState.searchClass]} + /> + </div> + </div> + ) : ( + <InfiniteScroll + filterState={filterState} + setNewSize={setNewSize} + setItems={(items) => setItems(prev => ({ + ...prev, + [filterState.searchClass]: items, + }))} + items={items[filterState.searchClass]} + abortController={abortControllers[filterState.searchClass]} + /> + )} </> </Overlay> - ) + ); + } \ No newline at end of file diff --git a/src/app/components/FiltersModal.js b/src/app/components/FiltersModal.js deleted file mode 100644 index c9d23a3fd0968b491743be5c3241cead619b6eb8..0000000000000000000000000000000000000000 --- a/src/app/components/FiltersModal.js +++ /dev/null @@ -1,83 +0,0 @@ -import * as React from 'react'; -import { useState } from 'react'; -import Button from '@mui/material/Button'; -import Modal from '@mui/material/Modal'; -import TuneIcon from '@mui/icons-material/Tune'; -import CloseIcon from '@mui/icons-material/Close'; -import FormFilters from './FormFilters'; - - - -/** - * - * @returns modal de filtros - */ -export default function FiltersModal({ - scholarityLevelsAvailable, - languagesAvailable, - setFilterState, - filterState, - setItems, - activeFilters, - setActiveFilters, - subjectsAvailable -}) { - const [open, setOpen] = useState(false); - const handleOpen = () => setOpen(true); - const handleClose = () => setOpen(false); - - return ( - <div className='justify-self-end'> - <Modal - open={open} - onClose={handleClose} - aria-labelledby="modal-modal-title" - aria-describedby="modal-modal-description" - className='grid place-items-center ' - slotProps={{ - backdrop: { - sx: { - backgroundColor: "rgba(0, 0, 0, 0.3)", // Ajuste a opacidade conforme necessário - }, - }, - }} - > - <div className='flex flex-col w-[60%] h-[80%] bg-white-HC-dark overflow-x-auto rounded-lg outline outline-1 outline-ice-HC-white'> - <div> - <div className='fixed z-20 w-[60%]'> - <div className='flex justify-between bg-white-HC-dark rounded-lg p-4'> - <p className=' text-2xl font-bold text-darkGray-HC-white '> - Filtros de Pesquisa - </p> - <CloseIcon onClick={handleClose} sx={{ color: "#6c8080", fontSize: "35px" }} /> - </div> - </div> - <div className='p-6'> - <FormFilters - activeFilters={activeFilters} - setActiveFilters={setActiveFilters} - handleClose={handleClose} - scholarityLevelsAvailable={scholarityLevelsAvailable} - languagesAvailable={languagesAvailable} - setFilterState={setFilterState} - filterState={filterState} - setItems={setItems} - subjectsAvailable={subjectsAvailable} - /> - </div> - </div> - </div> - </Modal> - <div className='justify-self-end'> - <div className='flex ml-2 mt-2'> - {activeFilters && - <Button onClick={() => { setActiveFilters(false) }} href="/busca?page=LearningObject" className={`normal-case font-semibold text-sm bg-darkGray-HC-white rounded-lg min-w-32 mx-1 text-white-HC-dark-underline hover:bg-slate-300`}> - Limpar Filtros - </Button> - } - <Button startIcon={<TuneIcon />} onClick={handleOpen} className='bg-darkGray-HC-white hover:bg-mediumGray-HC-dark font-semibold normal-case text-white-HC-dark-underline hover:text-white rounded-lg px-3 mr-2 outline outline-1 outline-ice-HC-white'>Filtros</Button> - </div> - </div> - </div> - ); -} \ No newline at end of file diff --git a/src/app/components/GroupButtonsFilters.js b/src/app/components/GroupButtonsFilters.js index eae98a11618ddfc169c0730f5f7fbad509e65f9d..73a84933db867a14933db19e1764a6581879fe16 100644 --- a/src/app/components/GroupButtonsFilters.js +++ b/src/app/components/GroupButtonsFilters.js @@ -1,6 +1,6 @@ import { Button } from "@mui/material"; import { usePathname } from "next/navigation"; -import FiltersModal from "./FiltersModal"; +import FiltersModal from "./TypeAndFilterModal"; import { Chip } from "@mui/material"; import { useState, useEffect } from "react"; import mecredApi from "@/axiosConfig"; @@ -22,40 +22,46 @@ export default function GroupButtonsFilters({ pageName, filterState, setFilterSt const [languagesAvailable, setLanguagesAvailable] = useState([]); const [subjectsAvailable, setSubjectsAvailable] = useState([]); - useEffect(() => { - mecredApi.get("/educational_stages") - .then((response) => { setScholarityLevelAvailable(response.data) }); - - mecredApi.get("/languages") - .then((response) => { setLanguagesAvailable(response.data) }); - - mecredApi - .get("/subjects") - .then(({ data }) => { - setSubjectsAvailable(data); - }) - }, []); + const fetchData = async () => { + try { + const [scholarityRes, languageRes, subjectsRes] = await Promise.all([ + mecredApi.get("public/educationalStage/all"), + mecredApi.get("public/language/all"), + mecredApi.get("public/subjects/all"), + ]); + + setScholarityLevelAvailable(scholarityRes.data); + setLanguagesAvailable(languageRes.data); + setSubjectsAvailable(subjectsRes.data); + } catch (error) { + console.error("Erro ao buscar dados para filtro:", error); + } + }; + + fetchData(); + }, []); + const pathname = usePathname(); const atalhos = [ { title: 'Recentes', titlePage: 'Recentes', - order: 'publicationdesc' + order: 'created_at' }, { title: 'Relevantes', titlePage: 'Relevantes', order: 'score' }, - pageName === "Collection" ? null : + pageName === "collections" ? null : { title: 'Colecionados', titlePage: 'Colecionados', order: 'likes' }, - pageName === "LearningObject" ? + pageName === "resources" ? { title: 'Visualizados', titlePage: 'visualizados', diff --git a/src/app/components/Header.js b/src/app/components/Header.js index 7ae1eaf4445ff27a41c6c3017139531c9dc3009f..83366b42d4019c3884f098129ccce79bc6937b5c 100644 --- a/src/app/components/Header.js +++ b/src/app/components/Header.js @@ -9,7 +9,7 @@ import SearchIcon from "@mui/icons-material/Search"; import AccountMenu from "./MenuProfile"; import SearchComponent from "./SearchComponent"; import NeedLoginModal from "./needLoginModal"; -import { isLoggedIn, useLoginBarrier } from "@/app/handlers/loginHandler"; +import { useLoggedIn, useLoginBarrier } from "@/app/handlers/loginHandler"; import { usePathname } from "next/navigation"; import { useRouter } from "next/navigation"; import { ImArrowLeft } from "react-icons/im"; @@ -26,14 +26,10 @@ function DefaultContent({ const pathname = usePathname(); const router = useRouter(); const loginBarrier = useLoginBarrier(); - const [loggedIn, setLoggedIn] = useState(false); - - useEffect(() => { - setLoggedIn(isLoggedIn()); - }, []) + const loggedIn = useLoggedIn(); const handleOpenSubmit = () => { - if (!isLoggedIn()) { + if (!loggedIn) { setNeedLoginOpen(true); } else { const params = new URLSearchParams(); diff --git a/src/app/components/InfiniteScroll.js b/src/app/components/InfiniteScroll.js index 0a29c19f50dd31274ce4b3a8f636432f59f3ff3f..538f5c0c7fbdd32e772225bb6ecbfbe4c22f1618 100644 --- a/src/app/components/InfiniteScroll.js +++ b/src/app/components/InfiniteScroll.js @@ -6,92 +6,95 @@ import Loading from "./Loading"; const getUrlFromFilterState = (filterState, page) => { const apiParams = new URLSearchParams(); apiParams.append("page", page); - apiParams.append("results_per_page", "20"); - apiParams.append("order", filterState.order); + apiParams.append("page_size", "20"); + apiParams.append("sortBy", filterState.order); apiParams.append("query", filterState.query); - apiParams.append("search_class", filterState.searchClass); - apiParams.append("subjects[]", filterState.subjects.map((obj) => obj.id).toString()); - apiParams.append("object_types[]", filterState.objectTypes.map((obj) => obj.id).toString()); - apiParams.append("educational_stages[]", filterState.educationalStages.map((obj) => obj.id).toString()); - apiParams.append("languages[]", filterState.languages.map((obj) => obj.id).toString()); + apiParams.append("index", filterState.searchClass); + apiParams.append("subjects", filterState.subjects.map((obj) => obj.name)) + apiParams.append("objectType", filterState.objectTypes.map((obj) => obj.name)); + apiParams.append("educational_stages", filterState.educationalStages.map((obj) => obj.name)); + apiParams.append("language", filterState.languages.map((obj) => obj.name)); - return `/search?${apiParams.toString()}`; + return `public/elastic/search?${apiParams.toString()}`; } - /** * @param {Object} props - * @param {String} props.filterSubject matérias selecionadas - string de números inteiros, cada um correspondendo a uma máteria - * @param {String} props.query query digitanda na barra de busca do site - * @param {String} props.type nome em inglês da página (ex:collections) - * @param {String} props.filter nome do filtro selecionado (ex:publicationdesc) - * @param {Function} props.setNewSize - * @param {Boolean} props.newSize boolean que verifica se o tamanho da tela foi alterado - * @returns faz a chamada dos recursos/coleções conforme a tela é scrollada ou os filtros alterados + * @param {Object} props.filterState Estado atual dos filtros + * @param {Function} props.setNewSize Atualizador para mudanças de layout + * @param {Boolean} props.newSize Indica se o tamanho da tela mudou + * @param {Function} props.setItems Função para atualizar os itens renderizados + * @param {Array} props.items Itens renderizados + * @param {AbortController} props.abortController Controlador de abort para chamadas fetch */ export default function InfiniteScroll({ filterState, setNewSize, newSize, setItems, items, abortController }) { const [isLoading, setIsLoading] = useState(false); const [page, setPage] = useState(0); - const [isScroll, setIsScroll] = useState(false) - - // começa com 1 pq quando carrega a página a primeira vez, caso zero, a mensagem aparece por alguns segundos - const [totalCount, setTotalCount] = useState(1) + const [isScroll, setIsScroll] = useState(false); + const [totalCount, setTotalCount] = useState(1); - const fetchData = useCallback(async (page) => { + // função para buscar dados (sem useCallback) + const fetchData = async (page, filterStateSnapshot) => { setIsLoading(true); - const url = getUrlFromFilterState(filterState, page); + const url = getUrlFromFilterState(filterStateSnapshot, page); + console.log("url aqui:", url); + try { const { data, headers } = await mecredApi.get(url, { - signal: abortController.signal + signal: abortController.signal, }); - // verificando se a conteudo para a url passada, caso nao tenha emite mensagem de conteudo nao enconttrado - setTotalCount(Number(headers["x-total-count"])) - setItems((prevItems) => { - // TODO: O backend retorna itens repetidos porque ordena as páginas por - // um atributo que pode repetir. A solução adequada é que o backend ordene - // por mais de um campo, por exemplo pelo filtro passado e depois pelo id, - // assim as ordenações serão estáveis. - // Solução hack atual: remover os itens duplicados. + console.log("Data Results:", data.results); + //AQUI: Acho que da para retirar o total count e substituir pelo page + + + console.log("headers", headers) + setTotalCount(Number(headers["x-total-count"])); + console.log("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", data.results) + + setItems( + prevItems => { const idsSeen = new Set(); const withoutDupes = []; - [...page === 0 ? [] : prevItems, ...data].forEach((item) => { - if (idsSeen.has(item.id)) { return; } - idsSeen.add(item.id); + + [...(page === 0 ? [] : prevItems), ...data.results].forEach((item) => { + if (idsSeen.has(item._id)) return; + idsSeen.add(item._id); withoutDupes.push(item); }); - return withoutDupes; - }); + + return withoutDupes; + } + ); setPage(() => page + 1); } catch (error) { console.error(error); } finally { setIsLoading(false); - setIsScroll(false) + setIsScroll(false); } - }, [filterState, setItems]); + }; - //verifica se o usuário chegou no final da página + // disparado quando o usuário rola até o fim da tela const handleScroll = useCallback(() => { - // Verifica se o usuário está próximo do final da área rolável if ( - window.innerHeight * 4 + window.scrollY < document.documentElement.offsetHeight || + window.innerHeight * 4 + window.scrollY < document.documentElement.offsetHeight || isLoading ) { return; } - - // Busca mais dados - fetchData(page); - }, [fetchData, isLoading, page]); - - //caso o filtro seja alterado, seta o site para o topo da tela + + fetchData(page, filterState); + }, [isLoading, page, filterState]); + + // recarrega os dados ao alterar os filtros useEffect(() => { - fetchData(0).then(() => { window.scrollTo(0, 0) }); + setPage(0); + fetchData(0, filterState).then(() => window.scrollTo(0, 0)); setIsScroll(true); - }, [fetchData]); + }, [filterState]); - //controle do scroll da página + // escuta o scroll da janela useEffect(() => { window.addEventListener("scroll", handleScroll); return () => window.removeEventListener("scroll", handleScroll); @@ -99,24 +102,24 @@ export default function InfiniteScroll({ filterState, setNewSize, newSize, setIt return ( <> - + {items && console.log("itens", items)} <div className={`${isScroll ? " blur-sm" : ""}`}> - {totalCount ? - - <InfiniteScrollCards setNewSize={setNewSize} newSize={newSize} data={items} searchClass={filterState?.searchClass} /> - : + {page && Array.isArray(items)? ( + <InfiniteScrollCards + setNewSize={setNewSize} + newSize={newSize} + data={items} + searchClass={filterState?.searchClass} + /> + ) : ( <div className="flex justify-center text-2xl font-bold text-darkGray-HC-white mt-72 text-center"> Desculpe, não encontramos nada. <br /> {": ("} </div> - - } + )} </div> - {isLoading && - <Loading scroll={isScroll} /> - } + {isLoading && <Loading scroll={isScroll} />} </> - ); } diff --git a/src/app/components/InfiniteScrollCards.js b/src/app/components/InfiniteScrollCards.js index 13d5663e47e72776b4e7489502498edfa4b0d04f..160d6e6a49826078fa44fb373af6ad65355aea20 100644 --- a/src/app/components/InfiniteScrollCards.js +++ b/src/app/components/InfiniteScrollCards.js @@ -40,13 +40,14 @@ export default function InfiniteScrollCards({ data, searchClass, setNewSize, new * get das coleções Oficiais do MEC */ const fetchCollections = async () => { - try { - const { data } = await mecredApi - .get("/users/35342/collections"); - setMecCollection([...data].reverse()); - } catch (error) { - console.error(error); - } + // comentado pq nao tem coleções do mec no teste + // try { + // const { data } = await mecredApi + // .get("/users/35342/collections"); + // setMecCollection([...data].reverse()); + // } catch (error) { + // console.error(error); + // } }; useEffect(() => { @@ -76,7 +77,7 @@ export default function InfiniteScrollCards({ data, searchClass, setNewSize, new function returnContent(type) { switch (type) { - case "Collection": + case "collections": return ( <div className="justify-center ml-8"> {data?.map((item) => ( @@ -123,20 +124,20 @@ export default function InfiniteScrollCards({ data, searchClass, setNewSize, new ); - case "LearningObject": + case "resources": return ( <div className="flex max-2xl:gap-4 flex-wrap ml-7 max-md:ml-0 max-md:justify-center"> {data?.map((item, index) => ( - <Cards - id={item['id']} - key={item['id']} - title={item["name"]} - author={item["publisher"]["name"]} - authorId={item["publisher"]["id"]} + <Cards + id={item['_id']} + key={item['_id']} + title={item["_source"]["name"]} + author={item["_source"]["author"]} + authorId={item["_source"]["user_id"]} avatar={item["publisher"]["avatar"]} image={item["thumbnail"]} - type={item["object_type"]} - updated_at={item["updated_at"]} + type={item["_source"]["objectType"]} + updated_at={item["_source"]["created_at"]} /> ))} </div> @@ -185,7 +186,7 @@ export default function InfiniteScrollCards({ data, searchClass, setNewSize, new </div> ); - case "User": + case "users": return ( <div className="flex flex-wrap justify-center"> {data?.map((item, i) => { diff --git a/src/app/components/NavigationBar.js b/src/app/components/NavigationBar.js index 47b0a79420fb99942ff5e708b558846cb7545be9..16bf260b50ccbbc8bf4321e8fb802e4efcea278c 100644 --- a/src/app/components/NavigationBar.js +++ b/src/app/components/NavigationBar.js @@ -53,8 +53,7 @@ export default function NavigationBar({ mobileSearch}) { const navItems = [ { label: "Pesquisar", href: "#", icon: SearchIcon }, - { label: "Recursos", href: "/busca?page=LearningObject", icon: SubjectIcon }, - { label: "Coleções", href: "/busca?page=Collection", icon: CollectionsBookmarkIcon }, + { label: "Biblioteca", href: "/biblioteca", icon: CollectionsBookmarkIcon }, { label: "Publicar", href: "/publicar", icon: FileUploadIcon }, { label: "MEC", href: "/busca?page=MEC", icon: VerifiedIcon }, { label: "Perfil", href: "/perfil", icon: Person }, diff --git a/src/app/components/Notifications.js b/src/app/components/Notifications.js index 080fb428bf9c1114869a57b9909d2f21daaf7189..94552af5f4165521c475f4fefcf5215e37a44db4 100644 --- a/src/app/components/Notifications.js +++ b/src/app/components/Notifications.js @@ -1,5 +1,5 @@ import mecredApi from "@/axiosConfig"; -import { useLoginBarrier } from "@/app/handlers/loginHandler"; +import { useLoggedIn, useLoginBarrier } from "@/app/handlers/loginHandler"; import { getStoredValue } from "@/app/handlers/localStorageHandler"; import { useEffect, useState } from 'react' import ModalNotifications from "./ModalNotifications"; @@ -9,14 +9,16 @@ export default function Notifications() { const [countNotifications, setCountNotifications] = useState(null); const loginBarrier = useLoginBarrier() + const loggedIn = useLoggedIn(); const token = getStoredValue("access_token") const client = getStoredValue("client") const uid = getStoredValue("uid") useEffect(() => { - if (!loginBarrier()) + if (!loggedIn) return + const url = `/feed?offset=0&limit=30` const getNotifications = async (url) => { @@ -43,7 +45,7 @@ export default function Notifications() { //chama funcao getNotifications(url) - }, [loginBarrier, uid, client, token]) + }, [loggedIn, uid, client, token]) const postViewNotification = async (payload) => { diff --git a/src/app/components/SearchComponent.js b/src/app/components/SearchComponent.js index 05350ceea7b988a114244b4a97d9c63606c3513f..95217906040b2abeed4bfe033f167924fcc60796 100644 --- a/src/app/components/SearchComponent.js +++ b/src/app/components/SearchComponent.js @@ -5,11 +5,6 @@ import KeyboardIcon from "@mui/icons-material/Keyboard"; import { usePathname, useRouter, useSearchParams } from "next/navigation"; import { useEffect, useState, useRef, useCallback } from "react"; -let suggestions = [ - { name: "Recursos", class: "LearningObject", ref: "busca?page=LearningObject" }, - { name: "Coleções", class: "Collection", ref: "busca?page=Collection" }, - { name: "Usuários", class: "User", ref: "busca?page=User" }, -]; /** * @@ -18,16 +13,12 @@ let suggestions = [ * @param {Boolean} props.sizeWindow * @returns searchComponent na header */ -export default function SearchComponent({ setFilterState, filterState, sizeWindow }) { +export default function SearchComponent() { + const searchParams = useSearchParams(); const router = useRouter(); const pathname = usePathname(); - const [input, setInput] = useState(""); - const [drop, setDrop] = useState(false); - const [width, setWidth] = useState(0); - const [selectedIndex, setSelectedIndex] = useState(0); - const dropdownRef = useRef(null); + const [input, setInput] = useState(searchParams["pesquisa"] ? searchParams["pesquisa"] : ""); const searchRef = useRef(null); - const searchParams = useSearchParams() const [isFocused, setIsFocused] = useState(false); const handleFocus = () => { @@ -41,106 +32,21 @@ export default function SearchComponent({ setFilterState, filterState, sizeWindo const handleSubmit = (e) => { e.preventDefault(); - const search = input === "" ? "*" : input; + const search = input === "" ? "" : input; - let defaultType = "/busca?page=Collection"; - - if ( - pathname === "/busca?page=Collection" || - pathname === "/busca?page=LearningObject" || - pathname === "/busca?page=User" - ) { - defaultType = pathname; - } - - if (!drop) { - router.push(`${defaultType}&filter=${search}`); - } else { - router.push(`/${suggestions[selectedIndex].ref}&filter=${search}`); - } - }; + const params = new URLSearchParams(searchParams); + params.set("pesquisa", search) + const url = pathname + '?' + params.toString(); + router.push(url); + + setIsFocused(false); - const handleClickOutside = (e) => { - if (dropdownRef.current && !dropdownRef.current.contains(e.target)) { - setDrop(false); - } }; + - const handleKeyDown = useCallback( - (e) => { - if (!drop) return; - - if (e.key === "ArrowDown") { - e.preventDefault(); - setSelectedIndex((prevIndex) => - prevIndex < suggestions.length - 1 ? prevIndex + 1 : 0 - ); - } else if (e.key === "ArrowUp") { - e.preventDefault(); - setSelectedIndex((prevIndex) => - prevIndex > 0 ? prevIndex - 1 : suggestions.length - 1 - ); - } else if (e.key === "Enter" && selectedIndex >= 0) { - router.push(`/${suggestions[selectedIndex].ref}&filter=${input}`); - setDrop(false) - } - }, - [drop, input, router, selectedIndex] - ); - - useEffect(() => { - - if (drop) { - document.addEventListener("keydown", handleKeyDown); - } else { - document.removeEventListener("keydown", handleKeyDown); - } - - return () => { - document.removeEventListener("keydown", handleKeyDown); - }; - }, [drop, handleKeyDown]); - - useEffect(() => { - document.addEventListener("mousedown", handleClickOutside); - return () => { - document.removeEventListener("mousedown", handleClickOutside); - }; - }, []); - - useEffect(() => { - if (searchRef.current) { - setWidth(searchRef.current.offsetWidth); - } - }, [input, drop]); - - useEffect(() => { - const handleResize = () => { - if (searchRef.current) { - setWidth(searchRef.current.offsetWidth); - } - }; - - window.addEventListener("resize", handleResize); - - return () => { - window.removeEventListener("resize", handleResize); - }; - }, []); - - useEffect(() => { - const index = suggestions.map(s => s.class).indexOf(searchParams.get('page')); - if (index !== -1) { // Estamos em uma página válida - setSelectedIndex(index); - } - }, [searchParams]); return ( <form - onKeyDown={(e) => { - if (e.key === "Enter") e.preventDefault(); - if (drop) e.stopPropagation(); - }} className="w-full max-sm:w-[90%] max-md:mx-2 h-[50px] z-30 items-center" onSubmit={handleSubmit} > @@ -157,15 +63,11 @@ export default function SearchComponent({ setFilterState, filterState, sizeWindo placeholder="Digite aqui o que você deseja pesquisar" className="p-2 px-5 rounded-lg outline outline-1 font-light text-2xl placeholder:text-lightGray-HC-dark outline-ice-HC-white align-middle h-full w-full" onFocus={handleFocus} + value={input} onBlur={handleBlur} onChange={(e) => { setInput(e.target.value); - setDrop(e.target.value !== ""); }} - onClick={(e) => - setDrop(e.target.value !== "") - } - onKeyDown={handleKeyDown} /> </div> <button @@ -177,36 +79,6 @@ export default function SearchComponent({ setFilterState, filterState, sizeWindo <SearchIcon className="h-full text-4xl max-sm:text-3xl" /> </button> </div> - {drop && ( - <div - ref={dropdownRef} - className={`fixed z-50 bg-white-HC-dark rounded-lg shadow-md `} - style={{ width: `${width}px` }} - tabIndex={0} - > - <ul className=" z-10"> - {suggestions.map((suggestion, index) => ( - <li - key={index} - onClick={() => { - router.push(`/${suggestion.ref}&filter=${input}`); - setDrop(false) - - }} - className={`p-2 text-darkGray-HC-white hover:bg-ice-HC-dark cursor-pointer flex ${ - index === selectedIndex ? "bg-ice-HC-dark " : "" - }`} - > - <div className="truncate">{input}</div>{" "} - <div className=" flex-shrink-0 indent-1"> - {" "} - em {suggestion.name} - </div> - </li> - ))} - </ul> - </div> - )} </form> ); } diff --git a/src/app/components/SideBar.js b/src/app/components/SideBar.js index 56834a2bb8f80dcf59191d3fd0c176514f791f66..8214409c94a98abd7e3b4ed56f2c396d848887d8 100644 --- a/src/app/components/SideBar.js +++ b/src/app/components/SideBar.js @@ -32,22 +32,15 @@ const acessoRapido = [ { title: "MEC", icon: VerifiedIcon, - href: "MEC", + href: "/MEC", id: "MEC", }, - { - title: "Coleções", + title: "Biblioteca", icon: CollectionsBookmarkIcon, - href: "Collection", + href: "/biblioteca", id: "Coleções", }, - { - title: "Recursos", - icon: SubjectIcon, - href: "LearningObject", - id: "Recursos", - }, { title: "Perfil", icon: Person, @@ -68,47 +61,12 @@ const acessoRapido = [ } ]; -function tradutor(name) { - switch (name) { - case "LearningObject": - return "Recursos" - case "Collection": - return "Coleções" - case "MEC": - return "MEC" - case "/perfil": - return "Perfil" - case "/publicar": - return "Publicar Recurso" - case "/contato": - return "Entre em contato" - default: - return "Sobre" - } -} - - export default function SideBar({ setFilterState, filterState }) { let searchParams = useSearchParams(); const page = searchParams.get('page') const pathname = usePathname(); const loggedIn = useLoggedIn(); - const getHref = (href) => { - switch (href) { - case "/sobre": - return "/sobre"; - case "/publicar": - return "/publicar"; - case "/contato": - return "/contato" - case "/perfil": - return `/perfil/${id}`; - default: - return `/busca?page=${href}`; - } - }; - const [needLoginOpen, setNeedLoginOpen] = useState(false); const handleOpenLogin = () => { @@ -123,7 +81,6 @@ export default function SideBar({ setFilterState, filterState }) { useEffect(() => { if (loggedIn) { let data = userData(); - console.log(data, "ALKSDJLAKSJD"); setId(data?.["id"]); } }, [loggedIn]); @@ -138,7 +95,7 @@ export default function SideBar({ setFilterState, filterState }) { return ( <Link onClick={item.href === "/publicar" ? handleOpenLogin : () => { }} - href={item.href === "/publicar" ? (loggedIn ? "/publicar" : "") : getHref(item.href)} + href={item.href === "/publicar" ? (loggedIn ? "/publicar" : "") : item.href} key={index} alt={item.title} title={item.title} diff --git a/src/app/components/TypeAndFilterModal.js b/src/app/components/TypeAndFilterModal.js new file mode 100644 index 0000000000000000000000000000000000000000..f0dd4be29822e2a746972d4811c628d31f0b80c1 --- /dev/null +++ b/src/app/components/TypeAndFilterModal.js @@ -0,0 +1,312 @@ +import * as React from 'react'; +import { useState, useEffect } from 'react'; +import Button from '@mui/material/Button'; +import Modal from '@mui/material/Modal'; +import TuneIcon from '@mui/icons-material/Tune'; +import CloseIcon from '@mui/icons-material/Close'; +import FormFilters from './FormFilters'; +import { Divider, FormControlLabel, FormGroup } from '@mui/material'; +import mecredApi from '@/axiosConfig'; +import { usePathname, useRouter } from 'next/navigation'; + + + + +//só seta a url quando finalizar o modal +function FormForFilters({ searchParams, setActiveFilters, handleClose, filtersDataAvailable, filters, setFilters }) { + const pathname = usePathname(); + const router = useRouter(); + + + const handleResetFilters = (e) => { + + const filtersReset = { + formato: [], + nivel: [], + idioma: [], + materias: [], + entity: "recurso", + }; + + setActiveFilters(false); + + setFilters(filtersReset); + + // Atualiza a URL sem parâmetros de pesquisa + const url = pathname; + router.push(url); + handleClose(); + }; + + const handleSubmit = (e) => { + e.preventDefault(); + + // Verificar se o estado filters não está vazio + const isFiltersNotEmpty = Object.values(filters).some(value => value.length > 0); + + // Se algum filtro não estiver vazio, define activeFilters como true + if (isFiltersNotEmpty) { + setActiveFilters(true); + } + + // Cria os parâmetros de busca a partir do estado dos filtros + const params = new URLSearchParams(); + if (filters.formato.length > 0) { + params.set('formato', filters.formato.join(',')); + } + if (filters.nivel.length > 0) { + params.set('nivel', filters.nivel.join(',')); + } + if (filters.idioma.length > 0) { + params.set('idioma', filters.idioma.join(',')); + } + if (filters.materias.length > 0) { + params.set('materias', filters.materias.join(',')); + } + if (filters.entity.length > 0) { + params.set('entidade', filters.entity); + } + + const url = pathname + '?' + params.toString(); + router.push(url); + + // Fecha o modal ou a interface de filtro + handleClose(); + }; + + const entity = [ + { name: "recursos", text: "Recursos" }, + { name: "colecoes", text: "Coleções" }, + { name: "usuarios", text: "Usuários" }, + ] + + + return <form className="mt-12" onSubmit={handleSubmit} onKeyDown={(e) => { if (e.key === "Enter") e.preventDefault() }}> + <Divider /> + <div className='flex gap-4 justify-center my-8'> + {entity.map(({ name, text }) => { + return <button + type="button" + key={name} + onClick={() => setFilters(prev => ({ ...prev, entidade: name }))} + className={"p-2 gap-1 rounded-md w-56 h-20 " + (filters.entidade === name ? "bg-turquoise-HC-dark text-white" : "bg-lightGray-HC-dark text-darkGray-HC-white")}> + {text} + </button> + })} + </div> + + + <Divider /> + + + <p className="font-bold text-darkGray-HC-white text-xl py-2 mt-6"> + Tipo de arquivo + </p> + <div className="flex flex-wrap mt-5 mb-10 "> + {filtersDataAvailable.objectTypes.map(({ name, id }) => ( + <label key={id} className="flex items-center mr-6 mb-4 cursor-pointer"> + <input + type="checkbox" + value={name} + onChange={({ target: { value, checked } }) => + setFilters(prev => ({ + ...prev, + formato: checked + ? [...(prev.formato || []), value] + : (prev.formato || []).filter(f => f !== value), + })) + } + + checked={filters.formato?.includes(name)} + className="mr-2 accent-turquoise-HC-dark" + /> + <span className="text-darkGray-HC-white">{name}</span> + </label> + ))} + </div> + + <Divider /> + + + <p className="font-bold text-darkGray-HC-white text-xl py-2 mt-6"> + Nível de ensino + </p> + <div className="flex flex-wrap mt-5 mb-10 "> + {filtersDataAvailable.educationalStages.map(({ name, id }) => ( + <label key={id} className="flex items-center mr-6 mb-4 cursor-pointer"> + <input + type="checkbox" + value={name} + onChange={({ target: { value, checked } }) => + setFilters(prev => ({ + ...prev, + nivel: checked + ? [...(prev.nivel || []), value] + : (prev.nivel || []).filter(f => f !== value), + })) + } + + checked={filters.nivel?.includes(name)} + className="mr-2 accent-turquoise-HC-dark" + /> + <span className="text-darkGray-HC-white">{name}</span> + </label> + ))} + </div> + + + <Divider /> + + + <p className="font-bold text-darkGray-HC-white text-xl py-2 mt-6"> + Idiomas do recurso + </p> + <div className="flex flex-wrap mt-5 mb-10 "> + {filtersDataAvailable.languages.map(({ name, id }) => ( + <label key={id} className="flex items-center mr-6 mb-4 cursor-pointer"> + <input + type="checkbox" + value={name} + onChange={({ target: { value, checked } }) => + setFilters(prev => ({ + ...prev, + idioma: checked + ? [...(prev.idioma || []), value] + : (prev.idioma || []).filter(f => f !== value), + })) + } + + checked={filters.idioma?.includes(name)} + className="mr-2 accent-turquoise-HC-dark" + /> + <span className="text-darkGray-HC-white">{name}</span> + </label> + ))} + </div> + + <Divider /> + + + <p className="font-bold text-darkGray-HC-white text-xl py-2 mt-6"> + Grandes Áreas + </p> + <div className="flex flex-wrap mt-5 mb-10 "> + {filtersDataAvailable.subjects.map(({ name, id }) => ( + <label key={id} className="flex items-center mr-6 mb-4 cursor-pointer"> + <input + type="checkbox" + value={name} + onChange={({ target: { value, checked } }) => + setFilters(prev => ({ + ...prev, + materias: checked + ? [...(prev.materias || []), value] + : (prev.materias || []).filter(f => f !== value), + })) + } + + checked={filters.materias?.includes(name)} + className="mr-2 accent-turquoise-HC-dark" + /> + <span className="text-darkGray-HC-white">{name}</span> + </label> + ))} + </div> + + <div className="fixed bottom-[10%] z-20 bg-white-HC-dark p-4 w-[56%] flex justify-end"> + <div className="flex"> + <button type="button" onClick={handleResetFilters} className="text-darkGray-HC-white-underline normal-case font-semibold text-base hover:bg-ice-HC-dark cursor-pointer mx-2 outline outline-1 outline-ice-HC-white">Remover filtros</button> + <button type="submit" className="bg-turquoise-HC-white hover:bg-darkTurquoise-HC-dark text-white-HC-dark-underline hover:text-white normal-case font-semibold text-base hover:bg-turquoise-hover cursor-pointer outline outline-1 outline-ice-HC-white">Mostrar resultados</button> + </div> + </div> + + </form> +} + + + +export default function TypeAndFilterModal({ searchParams, open, handleClose, filters, setFilters }) { + const [filtersDataAvailable, setFiltersDataAvailable] = useState({ + objectTypes: [], + educationalStages: [], + languages: [], + subjects: [], + }); + + const [activeFilters, setActiveFilters] = useState(false) + const router = useRouter(); + + + + + + useEffect(()=>{ + const fetchData = async () => { + try { + // faz as duas requisiçoes ao mesmo tempo + //fica mais rapido + const [objectTypesRes, educationalStageRes, languageRes, subjectsRes] = await Promise.all([ + mecredApi.get("public/objectType/all"), + mecredApi.get("public/educationalStage/all"), + mecredApi.get("public/language/all"), + mecredApi.get("public/subjects/all"), + ]); + + setFiltersDataAvailable({ + objectTypes: objectTypesRes.data, + educationalStages: educationalStageRes.data, + languages: languageRes.data, + subjects: subjectsRes.data, + }); + + } catch (error) { + console.error("Erro ao buscar dados:", error); + } + }; + fetchData(); + },[]) + + + return <div className='justify-self-end'> + <Modal + open={open} + onClose={handleClose} + aria-labelledby="modal-modal-title" + aria-describedby="modal-modal-description" + className='grid place-items-center ' + slotProps={{ + backdrop: { + sx: { + backgroundColor: "rgba(0, 0, 0, 0.3)", // Ajuste a opacidade conforme necessário + }, + }, + }} + > + <div className='flex flex-col w-[60%] h-[80%] bg-white-HC-dark overflow-x-auto rounded-lg outline outline-1 outline-ice-HC-white'> + <div> + <div className='fixed z-20 w-[60%]'> + <div className='flex justify-between bg-white-HC-dark rounded-lg p-4'> + <p className=' text-2xl font-bold text-darkGray-HC-white '> + Filtros de Pesquisa + </p> + <CloseIcon onClick={handleClose} sx={{ color: "#6c8080", fontSize: "35px" }} /> + </div> + </div> + <div className='p-6'> + <FormForFilters setActiveFilters={setActiveFilters} handleClose={handleClose} searchParams={searchParams} filters={filters} setFilters={setFilters} filtersDataAvailable={filtersDataAvailable} /> + </div> + </div> + </div> + </Modal> + <div className='justify-self-end'> + <div className='flex ml-2 mt-2'> + {activeFilters && + <button onClick={() => { setActiveFilters(false), router.push("/biblioteca"), setActiveFilters(); }} className={`normal-case font-semibold text-sm bg-darkGray-HC-white rounded-lg min-w-32 text-white-HC-dark-underline hover:bg-slate-300`}> + Limpar Filtros + </button> + } + </div> + </div> + </div> + +} diff --git a/src/app/publicar/components/Form.js b/src/app/publicar/components/Form.js index 58a869e8cd707329f5e5c3942ed8f792da0fbd95..a7db7cc60017ca6af829f8e911de28180fb2fd9d 100644 --- a/src/app/publicar/components/Form.js +++ b/src/app/publicar/components/Form.js @@ -4,27 +4,28 @@ import UploadForm from "./UploadForm"; import InfoForm from "./InfoForm"; import RevisionForm from "./RevisionForm" import AdvanceNotice from "./AdvanceNotice"; +import { userData } from "@/app/handlers/loginHandler"; export default function Form() { const [step, setStep] = useState(0); const [draft, setDraft] = useState(null); - const [userData, setUserData] = useState(null); + const [userDatas, setUserDatas] = useState(null); const [authorType, setAuthorType] = useState("a"); const [file, setFile] = useState(null); const [thumbURL, setThumbURL] = useState(null); const [thumb, setThumb] = useState(null); useEffect(() => { - setUserData(userData()); + setUserDatas(userData()); }, []) - return ( userData && + return ( userDatas && <> <div className="flex flex-col justify-start w-full mt-15 max-sm:mx-0 max-md:mb-16 overflow-auto scrollbar-none"> <HorizontalLinearAlternativeLabelStepper step={step} /> - {step === 0 && <InfoForm setStep={setStep} draft={draft} setDraft={setDraft} userData={userData} authorType={authorType} setAuthorType={setAuthorType} />} + {step === 0 && <InfoForm setStep={setStep} draft={draft} setDraft={setDraft} userData={userDatas} authorType={authorType} setAuthorType={setAuthorType} />} {step === 1 && <UploadForm thumbURL={thumbURL} setThumbURL={setThumbURL} thumb={thumb} setThumb={setThumb} setStep={setStep} draft={draft} setDraft={setDraft} file={file} setFile={setFile} />} - {step === 2 && <RevisionForm setStep={setStep} draft={draft} userData={userData} setDraft={setDraft} />} + {step === 2 && <RevisionForm setStep={setStep} draft={draft} userData={userDatas} setDraft={setDraft} />} <AdvanceNotice /> </div> </> diff --git a/src/app/recurso/[id]/components/comments.js b/src/app/recurso/[id]/components/comments.js index 202fa913799ec274bde002ef89599af37119ecaa..332eec771398af66e57409a386f686ea88cf5932 100644 --- a/src/app/recurso/[id]/components/comments.js +++ b/src/app/recurso/[id]/components/comments.js @@ -1,11 +1,11 @@ import { Paper, Accordion, AccordionSummary, AccordionDetails } from "@mui/material"; import { useEffect, useState } from "react"; import mecredApi from "@/axiosConfig"; -import { getStoredValue } from "@/app/handlers/localStorageHandler"; import PrintComments from "./printComments"; import CreateComments from "./createComments"; import {authHeaders, useLoggedIn } from "@/app/handlers/loginHandler"; import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown'; +import { userData } from "@/app/handlers/loginHandler"; export default function Comments({ learningObjectId }) { @@ -14,15 +14,15 @@ export default function Comments({ learningObjectId }) { const loggedIn = useLoggedIn(); - const [userData, setUserData] = useState({}); + const [userDatas, setUserDatas] = useState({}); const [logged, setLogged] = useState(false); useEffect(() => { if (!loggedIn) return; - setUserData(userData()); + setUserDatas(userData()); setLogged(true); - }, [loggedIn, userData]) + }, [loggedIn, userDatas]) const handleSubmitComment = async () => { @@ -66,7 +66,7 @@ export default function Comments({ learningObjectId }) { {comments?.length === 0 ? <div className="sm:hidden h-auto pt-4 my-4 bg-white-HC-dark flex flex-col items-center gap-4"> <p className="text-darkGray-HC-white text-xl">Ainda não possui comentários...</p> - <CreateComments user={userData} setComments={setComments} comments={comments} logged={logged} learningObjectId={learningObjectId} newComment={newComment} setNewComment={setNewComment} handleSubmitComment={handleSubmitComment} /> + <CreateComments user={userDatas} setComments={setComments} comments={comments} logged={logged} learningObjectId={learningObjectId} newComment={newComment} setNewComment={setNewComment} handleSubmitComment={handleSubmitComment} /> </div> : <Accordion className="sm:hidden bg-white-HC-dark "> @@ -90,7 +90,7 @@ export default function Comments({ learningObjectId }) { return <PrintComments key={index} comment={message} - userData={userData} + userData={userDatas} learningObjectId={learningObjectId} comments={comments} setComments={setComments} @@ -101,7 +101,7 @@ export default function Comments({ learningObjectId }) { </div> <hr className="max-sm:my-2" /> <div className="sm:hidden h-24 pt-4"> - <CreateComments user={userData} setComments={setComments} comments={comments} logged={logged} learningObjectId={learningObjectId} newComment={newComment} setNewComment={setNewComment} handleSubmitComment={handleSubmitComment} /> + <CreateComments user={userDatas} setComments={setComments} comments={comments} logged={logged} learningObjectId={learningObjectId} newComment={newComment} setNewComment={setNewComment} handleSubmitComment={handleSubmitComment} /> </div> </AccordionDetails> </Accordion> @@ -113,7 +113,7 @@ export default function Comments({ learningObjectId }) { {comments?.length} {comments?.length === 1 ? "comentário" : "comentários"} </div> <div> - <CreateComments user={userData} setComments={setComments} comments={comments} logged={logged} learningObjectId={learningObjectId} newComment={newComment} setNewComment={setNewComment} handleSubmitComment={handleSubmitComment} /> + <CreateComments user={userDatas} setComments={setComments} comments={comments} logged={logged} learningObjectId={learningObjectId} newComment={newComment} setNewComment={setNewComment} handleSubmitComment={handleSubmitComment} /> </div> <div> <div className="text-darkGray-HC-white "> @@ -121,7 +121,7 @@ export default function Comments({ learningObjectId }) { return <PrintComments key={index} comment={message} - userData={userData} + userData={userDatas} learningObjectId={learningObjectId} comments={comments} setComments={setComments} diff --git a/thumbnail.png b/thumbnail.png new file mode 100644 index 0000000000000000000000000000000000000000..49c12289a16465bb4401fa2e9aded6d866ec2176 Binary files /dev/null and b/thumbnail.png differ