From 761729995f586ced21b5c77c77bae406cf73f865 Mon Sep 17 00:00:00 2001 From: Anastasia Date: Sun, 28 Jun 2026 09:08:09 +0300 Subject: [PATCH] add videoResource page --- src/pages/VideoResourcePage/index.ts | 3 + .../ui/VideoResourcePage.css | 164 +++++++++++++++ .../ui/VideoResourcePage.tsx | 193 ++++++++++++++++++ src/shared/config/routeConfig/routeConfig.tsx | 7 + src/shared/ui/Carousel/Carousel.css | 84 ++++++++ src/shared/ui/Carousel/Carousel.tsx | 82 ++++++++ src/shared/ui/Carousel/index.ts | 1 + src/shared/ui/index.ts | 3 +- 8 files changed, 536 insertions(+), 1 deletion(-) create mode 100644 src/pages/VideoResourcePage/index.ts create mode 100644 src/pages/VideoResourcePage/ui/VideoResourcePage.css create mode 100644 src/pages/VideoResourcePage/ui/VideoResourcePage.tsx create mode 100644 src/shared/ui/Carousel/Carousel.css create mode 100644 src/shared/ui/Carousel/Carousel.tsx create mode 100644 src/shared/ui/Carousel/index.ts diff --git a/src/pages/VideoResourcePage/index.ts b/src/pages/VideoResourcePage/index.ts new file mode 100644 index 0000000..23bc60c --- /dev/null +++ b/src/pages/VideoResourcePage/index.ts @@ -0,0 +1,3 @@ +import VideoResourcePage from "./ui/VideoResourcePage"; + +export { VideoResourcePage }; \ No newline at end of file diff --git a/src/pages/VideoResourcePage/ui/VideoResourcePage.css b/src/pages/VideoResourcePage/ui/VideoResourcePage.css new file mode 100644 index 0000000..d40a805 --- /dev/null +++ b/src/pages/VideoResourcePage/ui/VideoResourcePage.css @@ -0,0 +1,164 @@ +.videoPage { + max-width: 1200px; + padding-left: 1.25rem; + padding-right: 1.25rem; + margin-left: auto; + margin-right: auto; +} + +.videoPageHeader { + margin-top: 2.5rem; + padding: 0 10px; +} + +.videoPageHeader h2 { + font-size: 1.875rem; + line-height: 2.25rem; + font-weight: 700; +} + +.videoPageContent { + display: flex; + gap: 20px; + padding: 10px; + align-items: flex-start; +} + +.videoMainContent { + display: flex; + flex: 1 1 740px; + flex-direction: column; + gap: 20px; + max-width: 740px; +} + +.videoContainer { + padding: 24px; + border-radius: 24px; + background-color: rgb(255 255 255 / 0.4); +} + +.videoFrame { + width: 100%; + aspect-ratio: 16 / 9; + height: auto; + border: 0; + border-radius: 16px; +} + +.playlistCarouselWrapper { + margin-top: 16px; +} + +.descriptionCard { + padding: 1.5rem; + border-radius: 1.5rem; + background-color: rgba(255, 255, 255, 0.4); + box-shadow: 0 6px 10px rgba(255, 166, 173, 0.4); +} + +.descriptionCard p { + font-size: 1.125rem; + line-height: 1.6; +} + +.tagsContainer { + position: relative; + display: flex; + flex-wrap: wrap; + gap: 12px; + align-items: center; + justify-content: flex-start; + background: rgba(250, 103, 115, 0.15); + backdrop-filter: blur(30px); + border-radius: 30px; + box-shadow: 0px 6px 10px 0px #fe444440; + padding: 24px; + overflow: hidden; + flex-shrink: 0; + max-width: 280px; + width: 100%; +} + +.tagsContainer::before { + content: ""; + position: absolute; + inset: 0; + background-color: rgba(250, 103, 115, 0.2); + border-radius: inherit; + filter: blur(30px); + z-index: -1; + box-shadow: 0px 6px 10px 0px #fe444440; +} + +.tags { + position: relative; + display: inline-block; + background-color: #fef7f8; + padding: 10px 16px; + min-width: 60px; + text-align: center; + border-radius: 100px; + z-index: 0; +} + +.tags::before { + content: ""; + position: absolute; + inset: 0; + padding: 1px; + border-radius: inherit; + background: linear-gradient(90deg, + rgba(255, 55, 127, 1) 0%, + rgba(255, 212, 130, 1) 85%); + mask: + linear-gradient(90deg, #fff 0%, #fff 100%) content-box, + linear-gradient(90deg, #fff 0%, #fff 100%); + -webkit-mask-composite: destination-out; + mask-composite: exclude; + z-index: -1; +} + +@media (max-width: 768px) { + .videoPageContent { + flex-direction: column; + } + + .videoMainContent { + max-width: 100%; + flex-basis: auto; + } + + .videoContainer { + padding: 12px; + } + + .tagsContainer { + max-width: 100%; + } +} + +@media (max-width: 480px) { + .videoPageHeader { + padding: 0 12px; + } + + .videoContainer { + padding: 10px; + } + + .descriptionCard { + padding: 16px; + } + + .descriptionCard p { + font-size: 1rem; + } +} + +@media (min-width: 768px) { + .videoPageHeader h2 { + font-size: 2.25rem; + line-height: 2.5rem; + } +} \ No newline at end of file diff --git a/src/pages/VideoResourcePage/ui/VideoResourcePage.tsx b/src/pages/VideoResourcePage/ui/VideoResourcePage.tsx new file mode 100644 index 0000000..a54658d --- /dev/null +++ b/src/pages/VideoResourcePage/ui/VideoResourcePage.tsx @@ -0,0 +1,193 @@ +import axios from "axios"; +import { useCallback, useEffect, useState } from "react"; +import Footer from "../../../shared/ui/Footer/Footer"; +import Navbar from "../../../shared/ui/Navbar/Navbar"; +import { Carousel } from "../../../shared/ui"; +import { useUser } from "../../../shared/lib/customHooks/useUser"; +import "./VideoResourcePage.css"; + + +type ResourceAudience = 'public' | 'member' | 'mentor'; + +type Resource = { + id: string; + title: string; + audience: ResourceAudience | ResourceAudience[]; + tags: string[]; + description?: string | null; + attachment_filename?: string | null; + link?: string | null; +} + +type VideoItem = { + snippet: { + thumbnails: { + medium: { + url: string; + }; + }; + title: string; + resourceId: { + videoId: string; + }; + }; +}; + +const youtubeApi = 'https://www.googleapis.com/youtube/v3/playlistItems'; +const apiKey = 'AIzaSyBxyNYafSBI9o_TWoejaSR-ADuO_qGWmjk'; + +const VideoResourcePage = () => { + const [videos, setVideos] = useState([]); + const [activeVideoId, setActiveVideoId] = useState(''); + const [resource, setResource] = useState(); + const [playlistId, setPlaylistId] = useState(''); + const [isUserHasAccess, setIsUserHasAccess] = useState(false) + + const userIdFromLocalStorage = localStorage.getItem("userId") ? Number(localStorage.getItem("userId")) : null; + const { user } = useUser(userIdFromLocalStorage); + + const selectVideo = (videoId: string) => { + setActiveVideoId(videoId) + } + + const checkVideoOrPlaylist = (link?: string | null) => { + if (!link) return; + + try { + const url = new URL(link); + const path = url.pathname; + + if (path.includes('playlist')) { + const playlistId = url.searchParams.get('list') ?? ''; + + setPlaylistId(playlistId); + return; + } + + if (path.includes('watch')) { + const videoId = url.searchParams.get('v') ?? undefined; + + if (videoId) setActiveVideoId(videoId); + + return; + } + + return ; + } catch (e) { + console.error('Invalid URL:', e); + return ; + } + } + + const checkUserHasAccess = useCallback(() => { + if (!resource) { + setIsUserHasAccess(false); + return; + } + + const userRoles = user?.roles.map(role => role.role_name) ?? []; + const audiences = Array.isArray(resource.audience) + ? resource.audience + : [resource.audience]; + + const hasAccess = audiences.includes('public') || audiences.some((role) => userRoles.includes(role)); + setIsUserHasAccess(hasAccess); + }, [resource, user]); + + useEffect(() => { + const fetchedResource: Resource = { + id: crypto.randomUUID(), + title: "Test resource", + audience: ["mentor"], + description: + "Lorem Ipsum is simply dummy text of the printing and typesetting industry", + tags: ["react", "javascript"], + link: 'https://www.youtube.com/playlist?list=PLfvcvNruup8o8ANWqcn8tSfx4Hgr0H6dj', + }; + + setResource(fetchedResource); + }, []) + + + useEffect(() => { + const getPlaylist = async (id: string) => { + try { + const response = await axios.get( + `${youtubeApi}?part=snippet&maxResults=30&playlistId=${id}&key=${apiKey}` + ); + + const data = response.data; + + console.log(data.items); + setVideos(data.items); + setActiveVideoId(data.items[0].snippet.resourceId.videoId); + } catch (e) { + console.log(`Something went wrong when getting playlist videos: ${e}`); + } + } + + checkVideoOrPlaylist(resource?.link); + + if (playlistId) { + getPlaylist(playlistId) + } + + }, [playlistId, resource]); + + useEffect(() => { + checkUserHasAccess(); + }, [checkUserHasAccess]); + + if (!resource) { + return ( +
Resource loading ...
+ ) + } + + if (!isUserHasAccess) { + return ( +

You don't have access to view this page

+ ) + } + + return ( +
+ +
+

{resource?.title}

+
+ +
+
+
+