diff --git a/src/app/journey-page/[journeyId]/page.tsx b/src/app/journey-page/[journeyId]/page.tsx new file mode 100644 index 0000000..43b5b4e --- /dev/null +++ b/src/app/journey-page/[journeyId]/page.tsx @@ -0,0 +1,103 @@ +'use client'; + +import React, { useEffect, useState } from 'react'; +import { Box, CircularProgress, Divider, Typography } from '@mui/material'; +import JourneyInfo from '@/components/journey/journeyInfo'; +import JourneyPath from '@/components/journey/journeyPath'; +import { addJourneyToUser, getJourney, getJourneysByUser, getTrails } from '@/services/studioMaker.service'; +import { Journey } from '@/lib/interfaces/journey.interface'; +import { Trail } from '@/lib/interfaces/trails.interface'; +import { useParams } from 'next/navigation'; +import { useSession } from 'next-auth/react'; +import { subscribeJourney, getSubscribedJourneys } from '@/services/user.service'; + +export default function JourneyPage() { + const { journeyId } = useParams(); + const [journey, setJourney] = useState(null); + const [trails, setTrails] = useState([]); + const [error, setError] = useState(null); + const [hasJourney, setHasJourney] = useState(false); + const { data: session } = useSession(); + + useEffect(() => { + const fetchJourneyData = async () => { + try { + const id = Array.isArray(journeyId) ? journeyId[0] : journeyId; + const token = JSON.parse(localStorage.getItem('token')!); + + const journeyData = await getJourney(id); + setJourney(journeyData); + + const trailsData = await getTrails({ id, token }); + setTrails(trailsData); + + if (session?.user?.id) { + const userJourneys = await getSubscribedJourneys(session.user.id); + console.log('User journeys: ', userJourneys); + let isSubscribed = false; + userJourneys.forEach((journeyId: string) => { + if(journeyId === id) { + isSubscribed = true; + } + }); + setHasJourney(isSubscribed); + } + } catch (err) { + setError('Failed to fetch journey data'); + } + }; + + fetchJourneyData(); + }, [journeyId, session?.user?.id]); + + const handleJoin = async () => { + if (session?.user.id) { + const id = Array.isArray(journeyId) ? journeyId[0] : journeyId; + console.log(session?.user.accessToken); + await subscribeJourney({ userId: session.user.id, journeyId: id, accessToken: session?.user.accessToken}); + setHasJourney(true); + } + + + }; + + if (error) { + return
{error}
; + } + + if (!journey) { + return ; + } + + return ( + + + + + + + {!trails.length ? ( + Ainda não há trilhas nessa jornada + ) : ( + + + + )} + + ); +} diff --git a/src/components/home/JourneyCard.tsx b/src/components/home/JourneyCard.tsx new file mode 100644 index 0000000..9e919b2 --- /dev/null +++ b/src/components/home/JourneyCard.tsx @@ -0,0 +1,34 @@ +'use client'; +import React from 'react'; +import Image from 'next/image'; +import 'react-multi-carousel/lib/styles.css'; +import { useRouter } from 'next/navigation'; + +interface JourneyCardProps { + title: string; + image: string; + Id: string; +} + +const JourneyCard: React.FC = ({ title, image, Id }) => { + + const router = useRouter(); + const handleClick = () => { + router.push('/journey-page/' + Id); + } + + return ( +
+ {title} +

{title}

+
+ ); +}; + +export default JourneyCard; diff --git a/src/components/home/StartPointsCard.tsx b/src/components/home/StartPointsCard.tsx new file mode 100644 index 0000000..5290907 --- /dev/null +++ b/src/components/home/StartPointsCard.tsx @@ -0,0 +1,137 @@ +'use client'; +import React, { useState, useEffect } from 'react'; +import Carousel from 'react-multi-carousel'; +import Image from 'next/image'; +import { ChevronUp, ChevronDown } from 'lucide-react'; +import Foto from '@/public/calculus-logo.svg'; +import JourneyService from './service/home.services'; +import { useRouter } from 'next/navigation'; + +interface StartCardProps { + title: string; + image: string; + description?: string; + Id: string; +} + +const StartCard: React.FC = ({ + title, + image, + description, + Id, +}) => { + const [isOpen, setIsOpen] = React.useState(false); + const [journeys, setJourneys] = useState([]); + const router = useRouter(); + + const handleOnclick = (id: string) => { + router.push('/journey-page/' + id); + }; + + useEffect(() => { + const loadJourneys = async () => { + try { + const { fetchJourneybyPoint, fetchJourneyById } = JourneyService(); + const journeysId = await fetchJourneybyPoint(Id); + + if (!journeysId || journeysId.length === 0) { + return; + } + + const j: any[] = []; + for (const journeyId of journeysId) { + const journey = await fetchJourneyById(journeyId); + if (journey) { + j.push(journey); + } else { + console.error(`Journey with ID ${journeyId} not found.`); + } + } + + if (j.length === 0) { + console.error('No valid journeys found.'); + } + + setJourneys(j); + } catch (error) { + console.error('Error loading journeys:', error); + } + }; + + loadJourneys(); + }, [Id]); + + const responsive = { + superLargeDesktop: { + // the naming can be any, depends on you. + breakpoint: { max: 4000, min: 3000 }, + items: 5, + }, + desktop: { + breakpoint: { max: 3000, min: 1024 }, + items: 5, + }, + tablet: { + breakpoint: { max: 1024, min: 464 }, + items: 3, + }, + mobile: { + breakpoint: { max: 464, min: 0 }, + items: 2, + }, + }; + + return ( +
+
{ + setIsOpen(!isOpen); + }} + className="flex gap-3 py-5 px-4 hover:bg-[#ececec] rounded-xl cursor-pointer items-center" + > + {title} +

{title}

+

{description || ""}

+
+ {isOpen ? : } +
+
+ {isOpen && ( +
+ {journeys.length > 0 ? ( + + {journeys.map((jornada, index) => ( +
+
handleOnclick(jornada._id)} + > + {jornada.title} +
+

{jornada.title}

+ + {index < journeys.length - 1 && ( +
+ )} +
+ ))} +
+ ) : ( +

No journeys available.

+ )} +
+ )} +
+
+ ); +}; + +export default StartCard; \ No newline at end of file diff --git a/src/components/home/homeJourneyCard.tsx b/src/components/home/homeJourneyCard.tsx deleted file mode 100644 index 30d3012..0000000 --- a/src/components/home/homeJourneyCard.tsx +++ /dev/null @@ -1,41 +0,0 @@ -'use client'; -import React from 'react'; -import Image from 'next/image'; -import MyButton from '@/components/ui/buttons/myButton.component'; -interface JourneyCardProps { - type: 'emAndamento' | 'geral'; - title: string; - image: string; - description?: string; - URL?: string; -} - -const JourneyCard: React.FC = ({ - type, - title, - image, - description, - URL, -}) => { - if (type === 'emAndamento') { - return ( -
- {title} -

{title}

-
- ); - } else { - return ( -
- {title} -

{title}

-

{description}

- - VER TRILHAS - -
- ); - } -}; - -export default JourneyCard; diff --git a/src/components/home/homePage.tsx b/src/components/home/homePage.tsx index a67b497..bfe1889 100644 --- a/src/components/home/homePage.tsx +++ b/src/components/home/homePage.tsx @@ -2,7 +2,8 @@ import React, { useState, useEffect } from 'react'; import Carousel from 'react-multi-carousel'; import 'react-multi-carousel/lib/styles.css'; import Foto from '@/public/calculus-logo.svg'; -import JourneyCard from '@/components/home/homeJourneyCard'; +import JourneyCard from '@/components/home/JourneyCard'; +import StartCard from '@/components/home/StartPointsCard'; import JourneyService from './service/home.services'; import SearchBar from './SearchBar'; import { useSession } from 'next-auth/react'; @@ -11,7 +12,7 @@ const HomePrincipalPage = () => { const { data: session } = useSession(); const [searchQuery, setSearchQuery] = useState(''); const [userJourneys, setUserJourneys] = useState([]); - const [allJourneys, setAllJourneys] = useState([]); + const [allPoints, setAllPoints] = useState([]); useEffect(() => { try { @@ -23,7 +24,7 @@ const HomePrincipalPage = () => { journeyIds.map(async (id: string) => await fetchJourneyById(id)), ); - setUserJourneys(journeysDetails.filter((j) => j !== null)); // Filtrar jornadas que foram encontradas + setUserJourneys(journeysDetails.filter((j) => j !== null)); }; fetchJourneys(); @@ -32,31 +33,95 @@ const HomePrincipalPage = () => { } }, [session]); + useEffect(() => { try { - const loadJourneys = async () => { - const { fetchJourneys } = JourneyService(); - const allJourneys = await fetchJourneys(); + const loadPoints = async () => { + const { fetchPoints } = JourneyService(); + const allPoints = await fetchPoints(); - setAllJourneys(allJourneys); + setAllPoints(allPoints); }; - loadJourneys(); + loadPoints(); } catch (error) { console.log(error); } }, []); - const filteredJourneys = + const allStartPointsTest = [ + { + _id: '1', + title: 'Ponto de Partida 1', + description: '', + image: Foto, + }, + { + _id: '2', + title: 'Ponto de Partida 2', + description: 'Descrição da Ponto de Partida 2', + image: Foto, + }, + { + _id: '3', + title: 'Ponto de Partida 3', + description: 'Descrição da Ponto de Partida 3', + image: Foto, + }, + { + _id: '4', + title: 'Ponto de Partida 4', + description: '', + image: Foto, + }, + { + _id: '5', + title: 'Ponto de Partida 5', + description: 'Descrição da Ponto de Partida 5', + image: Foto, + }, + { + _id: '6', + title: 'Ponto de Partida 6', + description: '', + image: Foto, + }, + { + _id: '7', + title: 'Ponto de Partida 7', + description: 'Descrição da Ponto de Partida 7', + image: Foto, + }, + { + _id: '8', + title: 'Ponto de Partida 8', + description: 'Descrição da Ponto de Partida 8', + image: Foto, + }, + { + _id: '9', + title: 'Ponto de Partida 9', + description: 'Descrição da Ponto de Partida 9', + image: Foto, + }, + { + _id: '10', + title: 'Ponto de Partida 10', + description: '', + image: Foto, + }, + ]; + + const filteredPoints = searchQuery.length > 0 - ? allJourneys.filter( + ? allPoints.filter( (jornada) => - jornada.title.toLowerCase().includes(searchQuery.toLowerCase()) || + jornada.name.toLowerCase().includes(searchQuery.toLowerCase()) || jornada.description .toLowerCase() .includes(searchQuery.toLowerCase()), ) : []; - + const responsive = { superLargeDesktop: { breakpoint: { max: 4000, min: 3000 }, @@ -78,7 +143,8 @@ const HomePrincipalPage = () => { const handleSearch = (query: string) => { setSearchQuery(query); - } + }; + return ( <> @@ -88,10 +154,10 @@ const HomePrincipalPage = () => { {userJourneys.map((jornada) => ( ))} @@ -107,32 +173,30 @@ const HomePrincipalPage = () => { <>
-

Jornadas

+

Pontos de partida

{searchQuery.length > 0 ? (
- {filteredJourneys.map((jornada) => ( - + {filteredPoints.map((jornada) => ( + ))}
) : (
- {allJourneys.map((jornada) => ( - ( + ))}
diff --git a/src/components/home/service/home.services.tsx b/src/components/home/service/home.services.tsx index d4d9c71..865e994 100644 --- a/src/components/home/service/home.services.tsx +++ b/src/components/home/service/home.services.tsx @@ -36,10 +36,32 @@ const JourneyService = () => { } }; + const fetchPoints = async () =>{ + try { + const response = await axios.get(`${process.env.NEXT_PUBLIC_API_URL_STUDIO}/points/`); + return response.data; + } catch (error) { + console.error('Erro ao buscar pontos de partida:', error); + return null; + } + } + + const fetchJourneybyPoint = async (id: string) => { + try { + const response = await axios.get(`${process.env.NEXT_PUBLIC_API_URL_STUDIO}/points/${id}/journeys`); + return response.data; + } catch (error) { + console.error(`Erro ao buscar jornadas do Ponto de Partida com ID ${id}:`, error); + return null; + } + } + return { fetchUserJourneys, fetchJourneys, fetchJourneyById, + fetchPoints, + fetchJourneybyPoint, }; }; diff --git a/src/components/journey/journeyInfo.tsx b/src/components/journey/journeyInfo.tsx new file mode 100644 index 0000000..c6c356a --- /dev/null +++ b/src/components/journey/journeyInfo.tsx @@ -0,0 +1,91 @@ +'use client'; + +import React from 'react'; +import { Box, Typography } from '@mui/material'; +import CollectionsBookmarkIcon from '@mui/icons-material/CollectionsBookmark'; +import MyButton from '@/components/ui/buttons/myButton.component'; +import { useSession } from 'next-auth/react'; + +interface JourneyInfoProps { + title: string; + description: string; + trailCount: number; + hasJourney: boolean; + onJoin: () => void; +} + +const JourneyInfo: React.FC = ({ + title, + description, + trailCount, + hasJourney, + onJoin, +}) => { + + const handleJoinClick = () => { + onJoin(); + }; + + return ( + + + {title} + + + +

{description}

+
+ + + + + + + Número de trilhas: {trailCount} + + {!hasJourney && ( + + + Ingressar + + + )} +
+ ); +}; + +export default JourneyInfo; diff --git a/src/components/journey/journeyPath.tsx b/src/components/journey/journeyPath.tsx new file mode 100644 index 0000000..f10909d --- /dev/null +++ b/src/components/journey/journeyPath.tsx @@ -0,0 +1,150 @@ +import React, { useEffect, useRef } from 'react'; +import { Box, Button, Typography } from '@mui/material'; +import { Trail } from '@/lib/interfaces/trails.interface'; + +interface JourneyPathProps { + trails: Trail[]; +} + +const JourneyPath: React.FC = ({ trails }) => { + const nodeSpacing = 120; + const nodeSize = 80; + const zigzagOffset = 100; + + const svgRef = useRef(null); + const containerRef = useRef(null); + + useEffect(() => { + const drawLines = () => { + if (!svgRef.current || !containerRef.current) return; + const svg = svgRef.current; + const svgNS = "http://www.w3.org/2000/svg"; + const svgWidth = svg.clientWidth; + const container = containerRef.current; + const svgHeight = svg.clientHeight; + + while (svg.firstChild) { + svg.removeChild(svg.firstChild); + } + + trails.forEach((_, index) => { + if (index < trails.length - 1) { + const isLeft1 = index % 2 === 0; + const offsetX1 = isLeft1 ? -zigzagOffset : zigzagOffset; + const top1 = 50 + index * nodeSpacing + nodeSize / 2; + const left1 = svgWidth / 2 + offsetX1; + + const isLeft2 = (index + 1) % 2 === 0; + const offsetX2 = isLeft2 ? -zigzagOffset : zigzagOffset; + const top2 = 50 + (index + 1) * nodeSpacing + nodeSize / 2; + const left2 = svgWidth / 2 + offsetX2; + + const line = document.createElementNS(svgNS, "line"); + line.setAttribute("x1", `${left1}`); + line.setAttribute("y1", `${top1}`); + line.setAttribute("x2", `${left2}`); + line.setAttribute("y2", `${top2}`); + line.setAttribute("stroke", "silver"); + line.setAttribute("stroke-width", "20"); + + svg.appendChild(line); + } + }); + }; + + drawLines(); + + const resizeObserver = new ResizeObserver(drawLines); + if (containerRef.current) { + resizeObserver.observe(containerRef.current); + } + + return () => { + resizeObserver.disconnect(); + }; + }, [trails]); + + return ( + + + {trails.map((trail, index) => { + const isLeft = index % 2 === 0; + const offsetX = isLeft ? -zigzagOffset : zigzagOffset; + + return ( + +