From 05b93b8a0f76d7f42b7742d178aaeb9b3be2699c Mon Sep 17 00:00:00 2001 From: Gustavo S Frehse <gsf20@inf.ufpr.br> Date: Tue, 12 Nov 2024 10:43:34 -0300 Subject: [PATCH] ADD Collection page --- CHANGELOG.md | 3 + .../[id]/components/collectionItems.js | 35 ++++++ .../components/publisherInfoCollection.js | 106 +++++++++++++++++ src/app/colecao/[id]/page.js | 107 ++++++++++++++++++ src/app/components/Cards.js | 2 +- src/app/components/GroupCardsCollections.js | 3 +- src/app/components/Header.js | 2 +- src/app/components/InfiniteScrollCards.js | 4 +- src/app/components/SideBar.js | 2 +- src/app/components/collectionPreview.js | 92 +++++++++++++++ .../[id] => }/components/needLoginModal.js | 0 .../[id] => }/components/publisherInfo.js | 0 src/app/{recurso/[id] => }/components/tags.js | 0 src/app/perfil/[id]/components/UserCard.js | 2 +- src/app/publicar/components/RevisionForm.js | 4 +- .../recurso/[id]/components/collectionInfo.js | 76 +++++++++++++ .../recurso/[id]/components/createComments.js | 2 +- src/app/recurso/[id]/page.js | 15 ++- 18 files changed, 441 insertions(+), 14 deletions(-) create mode 100644 src/app/colecao/[id]/components/collectionItems.js create mode 100644 src/app/colecao/[id]/components/publisherInfoCollection.js create mode 100644 src/app/colecao/[id]/page.js create mode 100644 src/app/components/collectionPreview.js rename src/app/{recurso/[id] => }/components/needLoginModal.js (100%) rename src/app/{recurso/[id] => }/components/publisherInfo.js (100%) rename src/app/{recurso/[id] => }/components/tags.js (100%) create mode 100644 src/app/recurso/[id]/components/collectionInfo.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 53697bf9..c724d274 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - New page for denounced resources. + - New page visualizing a specific collection. + - Component to view current collection in resource page. + ## [1.2.2] - 2024-11-13 diff --git a/src/app/colecao/[id]/components/collectionItems.js b/src/app/colecao/[id]/components/collectionItems.js new file mode 100644 index 00000000..249d599d --- /dev/null +++ b/src/app/colecao/[id]/components/collectionItems.js @@ -0,0 +1,35 @@ +import { useMediaQuery } from "@mui/material"; +import Cards from "@/app/components/Cards"; + +export default function CollectionItems({ collection }) { + const isSm = useMediaQuery((theme) => theme.breakpoints.down('sm')); + + return ( + <div className="flex flex-col bg-white p-4 rounded-lg"> + <div className="text-main-text font-bold text-xl ml-2">Recursos na coleção</div> + <div> + {collection.collection_items.map((item, index) => { + return ( + <Cards + collectionSource={collection["id"]} + horizontal={!isSm} + noAvatar={!isSm} + id={item["collectionable"]["id"]} + key={index} + title={item["collectionable"]["name"]} + author={item["collectionable"]["publisher"]["name"]} + avatar={item["collectionable"]["publisher"]["avatar"]} + image={item["collectionable"]["thumbnail"]} + type={item["collectionable"]["object_type"]} + updated_at={item["collectionable"]["updated_at"]} + thumbWidth={isSm ? "100%" : "230px"} + thumbHeight="auto" + width="100%" + page="recurso" + /> + ); + })} + </div> + </div> + ); +} \ No newline at end of file diff --git a/src/app/colecao/[id]/components/publisherInfoCollection.js b/src/app/colecao/[id]/components/publisherInfoCollection.js new file mode 100644 index 00000000..577f9f71 --- /dev/null +++ b/src/app/colecao/[id]/components/publisherInfoCollection.js @@ -0,0 +1,106 @@ +import { Avatar, Button, Paper } from "@mui/material"; +import { useLoginBarrier } from "@/app/handlers/loginHandler"; +import { useState, useEffect } from "react"; +import mecredApi, { mecredURL } from "@/axiosConfig"; +import { getStoredValue } from "@/app/handlers/localStorageHandler"; + +export default function PublisherInfoCollection({ publisher, disabledButton = false }) { + const [followed, setFollowed] = useState(false); + + const loginBarrier = useLoginBarrier(); + const token = getStoredValue("access_token") + const client = getStoredValue("client") + const uid = getStoredValue("uid") + + useEffect(() => { + setFollowed(publisher?.followed ?? false); + }, [publisher]); + + if (!publisher) { + return <></>; + } + + const followHandler = () => { + loginBarrier(); + mecredApi.put( + `users/${publisher.id}/follow/`, + {}, + { + headers: { + "access-token": token, + "token-type": "Bearer", + client: client, + uid: uid, + Expires: 0, + }, + } + ); + setFollowed((old) => !old); + }; + + function getRandomBg(id) { + const colors = [ + "bg-secondary", + "bg-orange", + "bg-secondary-hover", + "bg-orange-hover", + "bg-violet", + "bg-pink", + "bg-red", + "bg-text-color", + "bg-text-color-click", + "bg-button-filters", + "bg-text-filter", + "bg-other-links", + "bg-blue-button", + ] + + return colors[id % colors.length]; + } + + return ( + <> + <div className="bg-white outline outline-1 outline-outlineColor shadow-none flex flex-col flex-shrink-0 rounded-xl p-0 my-1 normal-case text-sm font-bold"> + <Button href={`/perfil/${publisher.id}`} > + <div className="flex flex-row truncate"> + {publisher?.avatar ? ( + <Avatar + fill="true" + src={mecredURL + publisher.avatar} + alt="Publisher Avatar" + title={publisher.name} + className="m-2" + /> + ) : ( + <div className={`flex items-center justify-center text-xl m-2 font-bold text-main rounded-full h-10 w-10 ${getRandomBg(publisher.id)}`} >{publisher.name[0]}</div> + )} + <div className="flex flex-col justify-center p-1 text-main-text truncate"> + <div className="font-bold text-md truncate"> + {publisher.name} + </div> + <div className="font-normal text-sm truncate"> + {publisher.description} + </div> + </div> + </div> + </Button> + <div className="flex justify-center flex-grow flex-shrink-0 p-3 truncate max-sm:justify-start"> + <Button + disabled={disabledButton} + href={`/perfil/${publisher.id}`} + className="outline outline-1 flex-grow outline-outlineColor hover:bg-gray-color bg-white h-12 text-text-color border border-solid border-main shadow-none rounded-xl p-3 m-1 mr-3 px-8 normal-case font-bold" + > + Ver Perfil + </Button> + <Button + disabled={disabledButton} + onClick={followHandler} + className="bg-secondary h-12 flex-grow text-white hover:bg-secondary-hover shadow-none rounded-xl m-1 px-2 sm:px-8 normal-case font-bold" + > + {followed ? "Seguindo" : "Seguir"} + </Button> + </div> + </div> + </> + ); +} diff --git a/src/app/colecao/[id]/page.js b/src/app/colecao/[id]/page.js new file mode 100644 index 00000000..ccc5ef5f --- /dev/null +++ b/src/app/colecao/[id]/page.js @@ -0,0 +1,107 @@ +"use client"; +import { useEffect, useState } from "react"; +import Overlay from "@/app/components/Overlay"; +import { isLoggedIn } from "@/app/handlers/loginHandler"; +import mecredApi from "@/axiosConfig"; +import { getStoredValue } from "@/app/handlers/localStorageHandler"; +import Loading from "@/app/components/Loading"; +import NeedLoginModal from "@/app/components/needLoginModal"; +import ErrorComponent from "@/app/components/ErrorComponent"; +import CollectionPreview from "@/app/components/collectionPreview"; +import Tags from "@/app/components/tags"; +import CollectionItems from "./components/collectionItems"; +import PublisherInfoCollection from "./components/publisherInfoCollection"; + +export default function Colecao({ params }) { + const [collection, setCollection] = useState(undefined); + const [needLoginOpen, setNeedLoginOpen] = useState(false); + const [error, setError] = useState(false) + + const token = getStoredValue("access_token"); + const client = getStoredValue("client"); + const uid = getStoredValue("uid"); + + useEffect(() => { + const fetchData = async () => { + try { + let headers = {}; + + if (isLoggedIn()) { + headers = { + "access-token": token, + "token-type": "Bearer", + client: client, + uid: uid, + Expires: 0, + }; + } + + const response = await mecredApi.get(`collections/${params.id}`, { + headers: headers, + }); + + setCollection(response.data); + } catch (error) { + setError(true); + } + }; + + fetchData(); +}, [params.id, client, token, uid]); + + return ( + <> + <Overlay> + {!collection ? (error ? + <ErrorComponent name="Recurso" /> : + + <Loading scroll /> + ) : ( + <> + <NeedLoginModal open={needLoginOpen} setOpen={setNeedLoginOpen} /> + <div className="flex flex-col lg:flex-row"> + <div className="flex flex-col bg-main p-3 lg:w-[30rem] flex-shrink-0"> + <div className=" flex justify-center"> + {/* Pré-visualização */} + <CollectionPreview collection={collection} /> + </div> + <div className="flex flex-row justify-between"> + <div className="flex flex-col"> + <div className="text-main-text mt-5 text-2xl font-bold"> + {/* Título */} + <h1>{collection.name}</h1> + </div> + <div className="text-main-text text-sm font-bold"> + {/* tags */} + <Tags tags={collection.tags} /> + </div> + </div> + </div> + <div className=""> + {/* Modal de entrar */} + {/* <ActionButtons learningObject={collection} setNeedLoginOpen={setNeedLoginOpen} state={state} /> */} + </div> + <div className=""> + {/* Publicador */} + <PublisherInfoCollection publisher={collection?.owner} /> + </div> + <div className=""> + {/* Informações */} + {/* <ResourceInfo learningObject={collection} /> */} + </div> + <div className=""> + {/* comments */} + {/* <Comments learningObjectId={params.id}/> */} + </div> + </div> + <div className="flex-grow-0 p-3 bg-main"> + <div className=""> {/* recommendations */} </div> + <CollectionItems collection={collection} /> + </div> + </div> + </> + )} + </Overlay> + </> + ); +} diff --git a/src/app/components/Cards.js b/src/app/components/Cards.js index f0774c4a..36183d52 100644 --- a/src/app/components/Cards.js +++ b/src/app/components/Cards.js @@ -122,7 +122,7 @@ export default function Cards(props) { padding: "10px", }} component={Link} - href={`/recurso/${props["id"]}`} + href={`/recurso/${props["id"]}` + (props.collectionSource ? `?collectionId=${props.collectionSource}` : "")} > <CardMedia diff --git a/src/app/components/GroupCardsCollections.js b/src/app/components/GroupCardsCollections.js index 334ed398..f8d6fd94 100644 --- a/src/app/components/GroupCardsCollections.js +++ b/src/app/components/GroupCardsCollections.js @@ -7,7 +7,7 @@ import { useRouter } from "next/navigation" * @param {Array.<Object>} data - recursos da coleção * @param {number} cardsPerRow - quantidade de cards que cabem na linha */ -export default function GroupCardsCollections({ data, cardsPerRow }) { +export default function GroupCardsCollections({ data, cardsPerRow, collectionId }) { const [expanded, setExpanded] = useState(false); const [showButton, setShowButton] = useState(false); const router = useRouter() @@ -33,6 +33,7 @@ export default function GroupCardsCollections({ data, cardsPerRow }) { {data?.length !== 0 ? data?.map((item, index) => { return ( <Cards + collectionSource={collectionId} key={index} id={item['collectionable']['id']} title={item["collectionable"]["name"]} diff --git a/src/app/components/Header.js b/src/app/components/Header.js index edbdd906..0bcb0136 100644 --- a/src/app/components/Header.js +++ b/src/app/components/Header.js @@ -8,7 +8,7 @@ import MenuIcon from "@mui/icons-material/Menu"; import SearchIcon from "@mui/icons-material/Search"; import AccountMenu from "./MenuProfile"; import SearchComponent from "./SearchComponent"; -import NeedLoginModal from "../recurso/[id]/components/needLoginModal"; +import NeedLoginModal from "./needLoginModal"; import { isLoggedIn, useLoginBarrier } from "@/app/handlers/loginHandler"; import { usePathname } from "next/navigation"; import { useRouter } from "next/navigation"; diff --git a/src/app/components/InfiniteScrollCards.js b/src/app/components/InfiniteScrollCards.js index 18cead1f..379797ea 100644 --- a/src/app/components/InfiniteScrollCards.js +++ b/src/app/components/InfiniteScrollCards.js @@ -82,7 +82,7 @@ export default function InfiniteScrollCards({ data, searchClass, setNewSize, new {data?.map((item, index) => ( <Fragment key={item['id']} > <div className="bg-white mb-10 pt-6 pl-4 mx-7 rounded-2xl outline outline-2 outline-outlineColor"> - <p className="text-2xl max-md:text-center font-bold mb-0 text-text-color mr-2 inline-block">{item['name']}</p> + <Link href={"/colecao/" + item['id']} className="text-2xl max-md:text-center font-bold mb-0 text-text-color mr-2 inline-block">{item['name']}</Link> <div className="flex flex-wrap justify-between mr-8 max-md:flex-col "> <p className=" ml-1 max-md:text-center text-main-text mb-0"> por <Link href={"/perfil/" + item["owner"]?.["id"]}> {item["owner"]?.["name"]}</Link>{timeFunction(item["updated_at"])} @@ -91,7 +91,7 @@ export default function InfiniteScrollCards({ data, searchClass, setNewSize, new <DownloadButton id={item['id']} objects={item['collection_items']} /> </div> </div> - <GroupCardsCollections cardsPerRow={cardsPerRow} data={item['collection_items']} /> + <GroupCardsCollections cardsPerRow={cardsPerRow} data={item['collection_items']} collectionId={item["id"]} /> </div> </Fragment> ))} diff --git a/src/app/components/SideBar.js b/src/app/components/SideBar.js index b0d6e710..c7e3cea1 100644 --- a/src/app/components/SideBar.js +++ b/src/app/components/SideBar.js @@ -14,7 +14,7 @@ import { Person } from "@mui/icons-material"; import { subjectsAvailable } from "./SubjectsAvailable"; import FileUploadIcon from '@mui/icons-material/FileUpload'; import { isLoggedIn } from "../handlers/loginHandler"; -import NeedLoginModal from "../recurso/[id]/components/needLoginModal"; +import NeedLoginModal from "./needLoginModal"; const acessoRapido = [ diff --git a/src/app/components/collectionPreview.js b/src/app/components/collectionPreview.js new file mode 100644 index 00000000..db3b0736 --- /dev/null +++ b/src/app/components/collectionPreview.js @@ -0,0 +1,92 @@ +import Image from "next/image"; +import { mecredURL } from "@/axiosConfig"; + + +const youtubeURLtoID = (url) => { + if (!url) return null; + + let regex = /^(?:https?:\/\/)?(?:m\.|www\.)?(?:youtu\.be\/|youtube\.com\/(?:embed\/|v\/|watch\?v=|watch\?.+&v=))((\w|-){11})(?:\S+)?$/; + + let result = url.match(regex); + if (result) return result[1]; + else return null; +}; + +const getDefaultThumbnail = (type) => { + let thumbnail_url; + + switch (type) { + case "Apresentação": + thumbnail_url = "/images/red.png"; + break; + case "Livro digital": + thumbnail_url = "/images/pdf.png"; + break; + case "Texto": + thumbnail_url = "/images/pdf.png"; + break; + case "Vídeo": + thumbnail_url = "/images/video.png"; + break; + case "Áudio": + thumbnail_url = "/images/audio.png"; + break; + case "Imagem": + thumbnail_url = "/images/red.png"; + break; + case "Animação": + thumbnail_url = "/images/red.png"; + break; + case "Jogo": + thumbnail_url = "/images/red.png"; + case "Software Educacional": + thumbnail_url = "/images/red.png"; + break; + default: + thumbnail_url = "/images/red.png"; + break; + } + + return thumbnail_url; +}; + +export default function CollectionPreview({ collection }) { + if (!collection?.id) return <></>; + + const uri = mecredURL + "inline/" + collection.id; + + let content = ( + <div className="relative aspect-video w-full h-full"> + <Image + fill + priority + style={{ + width: "100%", + objectFit: "cover", + }} + src={"/images/red.png"} + alt={collection.name} + /> + </div> + ); + + if (collection.items_thumbnails.length > 0) { + content = ( + <div className="relative aspect-video w-full h-full"> + <Image + priority + sizes="50vw" + fill + style={{ + width: '100%', + objectFit: 'cover' + }} + src={mecredURL + collection.items_thumbnails[0]} + alt={collection.name} + /> + </div> + ); + } + + return <div className="w-full overflow-hidden rounded-xl">{content}</div>; +} diff --git a/src/app/recurso/[id]/components/needLoginModal.js b/src/app/components/needLoginModal.js similarity index 100% rename from src/app/recurso/[id]/components/needLoginModal.js rename to src/app/components/needLoginModal.js diff --git a/src/app/recurso/[id]/components/publisherInfo.js b/src/app/components/publisherInfo.js similarity index 100% rename from src/app/recurso/[id]/components/publisherInfo.js rename to src/app/components/publisherInfo.js diff --git a/src/app/recurso/[id]/components/tags.js b/src/app/components/tags.js similarity index 100% rename from src/app/recurso/[id]/components/tags.js rename to src/app/components/tags.js diff --git a/src/app/perfil/[id]/components/UserCard.js b/src/app/perfil/[id]/components/UserCard.js index 3a50a21e..52da11a4 100644 --- a/src/app/perfil/[id]/components/UserCard.js +++ b/src/app/perfil/[id]/components/UserCard.js @@ -23,7 +23,7 @@ const roles = [ { id: 3, role: "admin", - translate: "Admnistrador" + translate: "Administrador" }, { id: 4, diff --git a/src/app/publicar/components/RevisionForm.js b/src/app/publicar/components/RevisionForm.js index 7726d40b..1b35fcb2 100644 --- a/src/app/publicar/components/RevisionForm.js +++ b/src/app/publicar/components/RevisionForm.js @@ -3,8 +3,8 @@ import mecredApi from "@/axiosConfig" import { Button, Paper } from "@mui/material" import { getStoredValue } from "@/app/handlers/localStorageHandler"; import { useEffect, useState } from "react"; -import Tags from "@/app/recurso/[id]/components/tags"; -import PublisherInfo from "@/app/recurso/[id]/components/publisherInfo"; +import Tags from "@/app/components/tags"; +import PublisherInfo from "@/app/components/publisherInfo"; import ResourceInfo from "@/app/recurso/[id]/components/resourceInfo"; import ResourcePreview from "@/app/recurso/[id]/components/resourcePreview"; import ModalSuccess from "./ModalSuccess"; diff --git a/src/app/recurso/[id]/components/collectionInfo.js b/src/app/recurso/[id]/components/collectionInfo.js new file mode 100644 index 00000000..3fdec01e --- /dev/null +++ b/src/app/recurso/[id]/components/collectionInfo.js @@ -0,0 +1,76 @@ +import { useEffect, useRef, useState } from "react"; +import { getStoredValue } from "@/app/handlers/localStorageHandler"; +import { useMediaQuery } from "@mui/material"; +import { isLoggedIn } from "@/app/handlers/loginHandler"; +import Loading from "@/app/components/Loading"; +import mecredApi from "@/axiosConfig"; +import Cards from "@/app/components/Cards"; +import Link from "next/link"; + +export default function CollectionInfo({ resourceId, collectionId }) { + const [collection, setCollection] = useState(null); + + const token = getStoredValue("access_token"); + const client = getStoredValue("client"); + const uid = getStoredValue("uid"); + + useEffect(() => { + const fetchData = async () => { + let headers = {}; + + if (isLoggedIn()) { + headers = { + "access-token": token, + "token-type": "Bearer", + client: client, + uid: uid, + Expires: 0, + }; + } + + const response = await mecredApi.get(`collections/${collectionId}`, { + headers: headers, + }); + + setCollection(response.data); + }; + + fetchData(); + }, [collectionId, client, token, uid]); + + const isSm = useMediaQuery((theme) => theme.breakpoints.down('sm')); + + return ( + <div className="flex flex-col bg-white p-4 rounded-lg mb-4"> + <Loading loaded={Boolean(collection)}> + <div className="text-main-text font-normal text-xl ml-2 mb-3">Recursos na coleção <Link className="font-bold" href={"/colecao/" + collectionId}>{collection?.name}</Link></div> + <div className="overflow-auto snap-y"> + {collection?.collection_items.map((item, index) => { + let isCurrent = item["collectionable"]["id"] === Number(resourceId); + return ( + <div key={index} className={"snap-start rounded-lg " + (isCurrent ? "bg-main-hover" : "bg-inherit")}> + <Cards + collectionSource={collectionId} + horizontal={!isSm} + noAvatar={!isSm} + id={item["collectionable"]["id"]} + title={item["collectionable"]["name"]} + author={item["collectionable"]["publisher"]["name"]} + avatar={item["collectionable"]["publisher"]["avatar"]} + image={item["collectionable"]["thumbnail"]} + type={item["collectionable"]["object_type"]} + updated_at={item["collectionable"]["updated_at"]} + thumbWidth={isSm ? "100%" : "230px"} + thumbHeight="auto" + width="100%" + page="recurso" + /> + </div> + ); + })} + </div> + </Loading> + </div> + ); + +} \ No newline at end of file diff --git a/src/app/recurso/[id]/components/createComments.js b/src/app/recurso/[id]/components/createComments.js index 3fee6104..9b77b33e 100644 --- a/src/app/recurso/[id]/components/createComments.js +++ b/src/app/recurso/[id]/components/createComments.js @@ -3,7 +3,7 @@ import PersonIcon from '@mui/icons-material/Person'; import { useEffect, useState } from "react"; import mecredApi, { mecredURL } from "@/axiosConfig"; import { getStoredValue } from "@/app/handlers/localStorageHandler"; -import NeedLoginModal from "./needLoginModal"; +import NeedLoginModal from "../../../components/needLoginModal"; import SendIcon from '@mui/icons-material/Send'; diff --git a/src/app/recurso/[id]/page.js b/src/app/recurso/[id]/page.js index d4b6312a..ee7eb6b2 100644 --- a/src/app/recurso/[id]/page.js +++ b/src/app/recurso/[id]/page.js @@ -1,8 +1,8 @@ "use client"; -import { Suspense, useEffect, useState } from "react"; -import Tags from "./components/tags"; +import { useEffect, useState } from "react"; +import Tags from "../../components/tags"; import ActionButtons from "./components/actionButtons"; -import PublisherInfo from "./components/publisherInfo"; +import PublisherInfo from "../../components/publisherInfo"; import Overlay from "../../components/Overlay"; import ResourceInfo from "./components/resourceInfo"; import RelatedResources from "./components/relatedResources"; @@ -11,11 +11,13 @@ import mecredApi from "@/axiosConfig"; import { getStoredValue } from "@/app/handlers/localStorageHandler"; import Loading from "@/app/components/Loading"; import ResourcePreview from "./components/resourcePreview"; -import NeedLoginModal from "./components/needLoginModal"; +import NeedLoginModal from "../../components/needLoginModal"; import ErrorComponent from "@/app/components/ErrorComponent"; import HomologationModal from "./components/homologationModal"; import ComplaintModal from "./components/complaintsModal"; import Comments from "./components/comments"; +import { useSearchParams } from "next/navigation"; +import CollectionInfo from "./components/collectionInfo"; export default function Recurso({ params }) { const [learningObject, setLearningObject] = useState(undefined); @@ -25,6 +27,10 @@ export default function Recurso({ params }) { const [submitOpen, setSubmitOpen] = useState(false) const [submitComplaintOpen, setSubmitComplaintOpen] = useState(false) const [complained, setComplained] = useState() + const searchParams = useSearchParams(); + + const collectionId = searchParams.get('collectionId'); + const token = getStoredValue("access_token"); const client = getStoredValue("client"); const uid = getStoredValue("uid"); @@ -123,6 +129,7 @@ export default function Recurso({ params }) { </div> <div className="flex-grow p-3 bg-main"> <div className=""> {/* recommendations */} </div> + {(collectionId !== null && collectionId !== undefined) ? <CollectionInfo resourceId={params.id} collectionId={collectionId} /> : <></>} <RelatedResources learningObject={learningObject} /> </div> </div> -- GitLab