-
-
Save ShariqAnsari88/09dbadfd81c41b399a30f6eb9f1f9548 to your computer and use it in GitHub Desktop.
import React, { useRef } from "react"; | |
import { | |
BsFillArrowLeftCircleFill, | |
BsFillArrowRightCircleFill, | |
} from "react-icons/bs"; | |
import { useNavigate } from "react-router-dom"; | |
import { useSelector } from "react-redux"; | |
import dayjs from "dayjs"; | |
import ContentWrapper from "../contentWrapper/ContentWrapper"; | |
import Img from "../lazyLoadImage/Img"; | |
import PosterFallback from "../../assets/no-poster.png"; | |
import "./style.scss"; | |
// CSS | |
@import "../../mixins.scss"; | |
.carousel { | |
margin-bottom: 50px; | |
.contentWrapper { | |
position: relative; | |
} | |
.carouselTitle { | |
font-size: 24px; | |
color: white; | |
margin-bottom: 20px; | |
font-weight: normal; | |
} | |
.arrow { | |
font-size: 30px; | |
color: black; | |
position: absolute; | |
top: 44%; | |
transform: translateY(-50%); | |
cursor: pointer; | |
opacity: 0.5; | |
z-index: 1; | |
display: none; | |
@include md { | |
display: block; | |
} | |
&:hover { | |
opacity: 0.8; | |
} | |
} | |
.carouselLeftNav { | |
left: 30px; | |
} | |
.carouselRighttNav { | |
right: 30px; | |
} | |
.loadingSkeleton { | |
display: flex; | |
gap: 10px; | |
overflow-y: hidden; | |
margin-right: -20px; | |
margin-left: -20px; | |
padding: 0 20px; | |
@include md { | |
gap: 20px; | |
overflow: hidden; | |
margin: 0; | |
padding: 0; | |
} | |
.skeletonItem { | |
width: 125px; | |
@include md { | |
width: calc(25% - 15px); | |
} | |
@include lg { | |
width: calc(20% - 16px); | |
} | |
flex-shrink: 0; | |
.posterBlock { | |
border-radius: 12px; | |
width: 100%; | |
aspect-ratio: 1 / 1.5; | |
margin-bottom: 30px; | |
} | |
.textBlock { | |
display: flex; | |
flex-direction: column; | |
.title { | |
width: 100%; | |
height: 20px; | |
margin-bottom: 10px; | |
} | |
.date { | |
width: 75%; | |
height: 20px; | |
} | |
} | |
} | |
} | |
.carouselItems { | |
display: flex; | |
gap: 10px; | |
overflow-y: hidden; | |
margin-right: -20px; | |
margin-left: -20px; | |
padding: 0 20px; | |
@include md { | |
gap: 20px; | |
overflow: hidden; | |
margin: 0; | |
padding: 0; | |
} | |
.carouselItem { | |
width: 125px; | |
cursor: pointer; | |
@include md { | |
width: calc(25% - 15px); | |
} | |
@include lg { | |
width: calc(20% - 16px); | |
} | |
flex-shrink: 0; | |
.posterBlock { | |
position: relative; | |
width: 100%; | |
aspect-ratio: 1 / 1.5; | |
background-size: cover; | |
background-position: center; | |
margin-bottom: 30px; | |
display: flex; | |
align-items: flex-end; | |
justify-content: space-between; | |
padding: 10px; | |
.lazy-load-image-background { | |
position: absolute; | |
top: 0; | |
left: 0; | |
width: 100%; | |
height: 100%; | |
border-radius: 12px; | |
overflow: hidden; | |
img { | |
width: 100%; | |
height: 100%; | |
object-fit: cover; | |
object-position: center; | |
} | |
} | |
.circleRating { | |
width: 40px; | |
height: 40px; | |
position: relative; | |
top: 30px; | |
background-color: white; | |
flex-shrink: 0; | |
@include md { | |
width: 50px; | |
height: 50px; | |
} | |
} | |
.genres { | |
display: none; | |
position: relative; | |
@include md { | |
display: flex; | |
flex-flow: wrap; | |
justify-content: flex-end; | |
} | |
} | |
} | |
.textBlock { | |
color: white; | |
display: flex; | |
flex-direction: column; | |
.title { | |
font-size: 16px; | |
margin-bottom: 10px; | |
line-height: 24px; | |
@include ellipsis(1); | |
@include md { | |
font-size: 20px; | |
} | |
} | |
.date { | |
font-size: 14px; | |
opacity: 0.5; | |
} | |
} | |
} | |
} | |
} |
import React from "react"; | |
import { useSelector } from "react-redux"; | |
import "./style.scss"; | |
import ContentWrapper from "../../../components/contentWrapper/ContentWrapper"; | |
import Img from "../../../components/lazyLoadImage/Img"; | |
import avatar from "../../../assets/avatar.png"; | |
const Cast = ({ data, loading }) => { | |
const { url } = useSelector((state) => state.home); | |
const skeleton = () => { | |
return ( | |
<div className="skItem"> | |
<div className="circle skeleton"></div> | |
<div className="row skeleton"></div> | |
<div className="row2 skeleton"></div> | |
</div> | |
); | |
}; | |
return ( | |
<div className="castSection"> | |
<ContentWrapper> | |
<div className="sectionHeading">Top Cast</div> | |
{!loading ? ( | |
<div className="listItems"> | |
Cast Data.... | |
</div> | |
) : ( | |
<div className="castSkeleton"> | |
{skeleton()} | |
{skeleton()} | |
{skeleton()} | |
{skeleton()} | |
{skeleton()} | |
{skeleton()} | |
</div> | |
)} | |
</ContentWrapper> | |
</div> | |
); | |
}; | |
export default Cast; | |
// CSS | |
@import "../../../mixins.scss"; | |
.castSection { | |
position: relative; | |
margin-bottom: 50px; | |
.sectionHeading { | |
font-size: 24px; | |
color: white; | |
margin-bottom: 25px; | |
} | |
.listItems { | |
display: flex; | |
gap: 20px; | |
overflow-y: hidden; | |
margin-right: -20px; | |
margin-left: -20px; | |
padding: 0 20px; | |
@include md { | |
margin: 0; | |
padding: 0; | |
} | |
.listItem { | |
text-align: center; | |
color: white; | |
.profileImg { | |
width: 125px; | |
height: 125px; | |
border-radius: 50%; | |
overflow: hidden; | |
margin-bottom: 15px; | |
@include md { | |
width: 175px; | |
height: 175px; | |
margin-bottom: 25px; | |
} | |
img { | |
width: 100%; | |
height: 100%; | |
object-fit: cover; | |
object-position: center top; | |
display: block; | |
} | |
} | |
.name { | |
font-size: 14px; | |
line-height: 20px; | |
font-weight: 600; | |
@include md { | |
font-size: 18px; | |
line-height: 24px; | |
} | |
} | |
.character { | |
font-size: 14px; | |
line-height: 20px; | |
opacity: 0.5; | |
@include md { | |
font-size: 16px; | |
line-height: 24px; | |
} | |
} | |
} | |
} | |
.castSkeleton { | |
display: flex; | |
gap: 20px; | |
overflow-y: hidden; | |
margin-right: -20px; | |
margin-left: -20px; | |
padding: 0 20px; | |
@include md { | |
margin: 0; | |
padding: 0; | |
} | |
.skItem { | |
.circle { | |
width: 125px; | |
height: 125px; | |
border-radius: 50%; | |
margin-bottom: 15px; | |
@include md { | |
width: 175px; | |
height: 175px; | |
margin-bottom: 25px; | |
} | |
} | |
.row { | |
width: 100%; | |
height: 20px; | |
border-radius: 10px; | |
margin-bottom: 10px; | |
} | |
.row2 { | |
width: 75%; | |
height: 20px; | |
border-radius: 10px; | |
margin: 0 auto; | |
} | |
} | |
} | |
} |
import React from "react"; | |
import { CircularProgressbar, buildStyles } from "react-circular-progressbar"; | |
import "react-circular-progressbar/dist/styles.css"; | |
import "./style.scss"; | |
const CircleRating = ({ rating }) => { | |
return ( | |
<div className="circleRating"> | |
<CircularProgressbar | |
value={rating} | |
maxValue={10} | |
text={rating} | |
styles={buildStyles({ | |
pathColor: | |
rating < 5 ? "red" : rating < 7 ? "orange" : "green", | |
})} | |
/> | |
</div> | |
); | |
}; | |
export default CircleRating; | |
// CSS | |
.circleRating { | |
background-color: var(--black); | |
border-radius: 50%; | |
padding: 2px; | |
.CircularProgressbar-text { | |
font-size: 34px; | |
font-weight: 700; | |
fill: var(--black); | |
} | |
.CircularProgressbar-trail { | |
stroke: transparent; | |
} | |
} |
import React from "react"; | |
import "./style.scss"; | |
const ContentWrapper = ({ children }) => { | |
return <div className="contentWrapper">{children}</div>; | |
}; | |
export default ContentWrapper; | |
// CSS | |
.contentWrapper { | |
width: 100%; | |
max-width: 1200px; | |
margin: 0 auto; | |
padding: 0 20px; | |
} |
// Similar | |
import React from "react"; | |
import Carousel from "../../../components/carousel/Carousel"; | |
import useFetch from "../../../hooks/useFetch"; | |
const Similar = ({ mediaType, id }) => { | |
const { data, loading, error } = useFetch(`/${mediaType}/${id}/similar`); | |
const title = mediaType === "tv" ? "Similar TV Shows" : "Similar Movies"; | |
return ( | |
<Carousel | |
title={title} | |
data={data?.results} | |
loading={loading} | |
endpoint={mediaType} | |
/> | |
); | |
}; | |
export default Similar; | |
// Recommendation | |
import React from "react"; | |
import Carousel from "../../../components/carousel/Carousel"; | |
import useFetch from "../../../hooks/useFetch"; | |
const Recommendation = ({ mediaType, id }) => { | |
const { data, loading, error } = useFetch( | |
`/${mediaType}/${id}/recommendations` | |
); | |
return ( | |
<Carousel | |
title="Recommendations" | |
data={data?.results} | |
loading={loading} | |
endpoint={mediaType} | |
/> | |
); | |
}; | |
export default Recommendation; |
import React, { useState, useEffect } from "react"; | |
import { useParams } from "react-router-dom"; | |
import InfiniteScroll from "react-infinite-scroll-component"; | |
import Select from "react-select"; | |
import "./style.scss"; | |
import useFetch from "../../hooks/useFetch"; | |
import { fetchDataFromApi } from "../../utils/api"; | |
import ContentWrapper from "../../components/contentWrapper/ContentWrapper"; | |
import MovieCard from "../../components/movieCard/MovieCard"; | |
import Spinner from "../../components/spinner/Spinner"; | |
let filters = {}; | |
const sortbyData = [ | |
{ value: "popularity.desc", label: "Popularity Descending" }, | |
{ value: "popularity.asc", label: "Popularity Ascending" }, | |
{ value: "vote_average.desc", label: "Rating Descending" }, | |
{ value: "vote_average.asc", label: "Rating Ascending" }, | |
{ | |
value: "primary_release_date.desc", | |
label: "Release Date Descending", | |
}, | |
{ value: "primary_release_date.asc", label: "Release Date Ascending" }, | |
{ value: "original_title.asc", label: "Title (A-Z)" }, | |
]; | |
const Explore = () => { | |
const [data, setData] = useState(null); | |
const [pageNum, setPageNum] = useState(1); | |
const [loading, setLoading] = useState(false); | |
const [genre, setGenre] = useState(null); | |
const [sortby, setSortby] = useState(null); | |
const { mediaType } = useParams(); | |
const { data: genresData } = useFetch(`/genre/${mediaType}/list`); | |
const fetchInitialData = () => { | |
setLoading(true); | |
fetchDataFromApi(`/discover/${mediaType}`, filters).then((res) => { | |
setData(res); | |
setPageNum((prev) => prev + 1); | |
setLoading(false); | |
}); | |
}; | |
const fetchNextPageData = () => { | |
fetchDataFromApi( | |
`/discover/${mediaType}?page=${pageNum}`, | |
filters | |
).then((res) => { | |
if (data?.results) { | |
setData({ | |
...data, | |
results: [...data?.results, ...res.results], | |
}); | |
} else { | |
setData(res); | |
} | |
setPageNum((prev) => prev + 1); | |
}); | |
}; | |
useEffect(() => { | |
filters = {}; | |
setData(null); | |
setPageNum(1); | |
setSortby(null); | |
setGenre(null); | |
fetchInitialData(); | |
}, [mediaType]); | |
const onChange = (selectedItems, action) => { | |
if (action.name === "sortby") { | |
setSortby(selectedItems); | |
if (action.action !== "clear") { | |
filters.sort_by = selectedItems.value; | |
} else { | |
delete filters.sort_by; | |
} | |
} | |
if (action.name === "genres") { | |
setGenre(selectedItems); | |
if (action.action !== "clear") { | |
let genreId = selectedItems.map((g) => g.id); | |
genreId = JSON.stringify(genreId).slice(1, -1); | |
filters.with_genres = genreId; | |
} else { | |
delete filters.with_genres; | |
} | |
} | |
setPageNum(1); | |
fetchInitialData(); | |
}; | |
return ( | |
<div className="explorePage"> | |
<ContentWrapper> | |
<div className="pageHeader"> | |
<div className="pageTitle"> | |
{mediaType === "tv" | |
? "Explore TV Shows" | |
: "Explore Movies"} | |
</div> | |
<div className="filters"> | |
<Select | |
isMulti | |
name="genres" | |
value={genre} | |
closeMenuOnSelect={false} | |
options={genresData?.genres} | |
getOptionLabel={(option) => option.name} | |
getOptionValue={(option) => option.id} | |
onChange={onChange} | |
placeholder="Select genres" | |
className="react-select-container genresDD" | |
classNamePrefix="react-select" | |
/> | |
<Select | |
name="sortby" | |
value={sortby} | |
options={sortbyData} | |
onChange={onChange} | |
isClearable={true} | |
placeholder="Sort by" | |
className="react-select-container sortbyDD" | |
classNamePrefix="react-select" | |
/> | |
</div> | |
</div> | |
{loading && <Spinner initial={true} />} | |
{!loading && ( | |
<> | |
{data?.results?.length > 0 ? ( | |
<InfiniteScroll | |
className="content" | |
dataLength={data?.results?.length || []} | |
next={fetchNextPageData} | |
hasMore={pageNum <= data?.total_pages} | |
loader={<Spinner />} | |
> | |
{data?.results?.map((item, index) => { | |
if (item.media_type === "person") return; | |
return ( | |
<MovieCard | |
key={index} | |
data={item} | |
mediaType={mediaType} | |
/> | |
); | |
})} | |
</InfiniteScroll> | |
) : ( | |
<span className="resultNotFound"> | |
Sorry, Results not found! | |
</span> | |
)} | |
</> | |
)} | |
</ContentWrapper> | |
</div> | |
); | |
}; | |
export default Explore; | |
// CSS | |
@import "../../mixins.scss"; | |
.explorePage { | |
min-height: 700px; | |
padding-top: 100px; | |
.resultNotFound { | |
font-size: 24px; | |
color: var(--black-light); | |
} | |
.pageHeader { | |
display: flex; | |
justify-content: space-between; | |
margin-bottom: 25px; | |
flex-direction: column; | |
@include md { | |
flex-direction: row; | |
} | |
} | |
.pageTitle { | |
font-size: 24px; | |
line-height: 34px; | |
color: white; | |
margin-bottom: 20px; | |
@include md { | |
margin-bottom: 0; | |
} | |
} | |
.filters { | |
display: flex; | |
gap: 10px; | |
flex-direction: column; | |
@include md { | |
flex-direction: row; | |
} | |
.react-select-container { | |
&.genresDD { | |
width: 100%; | |
@include md { | |
max-width: 500px; | |
min-width: 250px; | |
} | |
} | |
&.sortbyDD { | |
width: 100%; | |
flex-shrink: 0; | |
@include md { | |
width: 250px; | |
} | |
} | |
.react-select__control { | |
border: 0; | |
outline: 0; | |
box-shadow: none; | |
background-color: var(--black-light); | |
border-radius: 20px; | |
.react-select__value-container { | |
.react-select__placeholder, | |
.react-select__input-container { | |
color: white; | |
margin: 0 10px; | |
} | |
} | |
.react-select__single-value { | |
color: white; | |
} | |
.react-select__multi-value { | |
background-color: var(--black3); | |
border-radius: 10px; | |
.react-select__multi-value__label { | |
color: white; | |
} | |
.react-select__multi-value__remove { | |
background-color: transparent; | |
color: white; | |
cursor: pointer; | |
&:hover { | |
color: var(--black-lighter); | |
} | |
} | |
} | |
} | |
.react-select__menu { | |
top: 40px; | |
margin: 0; | |
padding: 0; | |
} | |
} | |
} | |
.content { | |
display: flex; | |
flex-flow: row wrap; | |
gap: 10px; | |
margin-bottom: 50px; | |
@include md { | |
gap: 20px; | |
} | |
.movieCard { | |
.posterBlock { | |
margin-bottom: 30px; | |
} | |
} | |
} | |
} |
import React, { useState, useEffect } from "react"; | |
import { HiOutlineSearch } from "react-icons/hi"; | |
import { SlMenu } from "react-icons/sl"; | |
import { VscChromeClose } from "react-icons/vsc"; | |
import { useNavigate, useLocation } from "react-router-dom"; | |
import "./style.scss"; | |
import ContentWrapper from "../contentWrapper/ContentWrapper"; | |
import logo from "../../assets/movix-logo.svg"; | |
const Header = () => { | |
const [show, setShow] = useState("top"); | |
const [lastScrollY, setLastScrollY] = useState(0); | |
const [mobileMenu, setMobileMenu] = useState(false); | |
const [query, setQuery] = useState(""); | |
const [showSearch, setShowSearch] = useState(""); | |
const navigate = useNavigate(); | |
const location = useLocation(); | |
return ( | |
<div>Header</div> | |
); | |
}; | |
export default Header; | |
// CSS | |
@import "../../mixins.scss"; | |
.header { | |
position: fixed; | |
transform: translateY(0); | |
width: 100%; | |
height: 60px; | |
z-index: 1; | |
display: flex; | |
align-items: center; | |
transition: all ease 0.5s; | |
z-index: 2; | |
&.top { | |
background: rgba(0, 0, 0, 0.25); | |
backdrop-filter: blur(3.5px); | |
-webkit-backdrop-filter: blur(3.5px); | |
} | |
&.show { | |
background-color: var(--black3); | |
} | |
&.hide { | |
transform: translateY(-60px); | |
} | |
.contentWrapper { | |
display: flex; | |
align-items: center; | |
justify-content: space-between; | |
} | |
.logo { | |
cursor: pointer; | |
img { | |
height: 50px; | |
} | |
} | |
.menuItems { | |
list-style-type: none; | |
display: none; | |
align-items: center; | |
@include md { | |
display: flex; | |
} | |
.menuItem { | |
height: 60px; | |
display: flex; | |
align-items: center; | |
margin: 0 15px; | |
color: white; | |
font-weight: 500; | |
position: relative; | |
&.searchIcon { | |
margin-right: 0; | |
} | |
svg { | |
font-size: 18px; | |
} | |
cursor: pointer; | |
&:hover { | |
color: var(--pink); | |
} | |
} | |
} | |
.mobileMenuItems { | |
display: flex; | |
align-items: center; | |
gap: 20px; | |
@include md { | |
display: none; | |
} | |
svg { | |
font-size: 18px; | |
color: white; | |
} | |
} | |
&.mobileView { | |
background: var(--black3); | |
.menuItems { | |
display: flex; | |
position: absolute; | |
top: 60px; | |
left: 0; | |
background: var(--black3); | |
flex-direction: column; | |
width: 100%; | |
padding: 20px 0; | |
border-top: 1px solid rgba(255, 255, 255, 0.1); | |
animation: mobileMenu 0.3s ease forwards; | |
.menuItem { | |
font-size: 20px; | |
width: 100%; | |
height: auto; | |
padding: 15px 20px; | |
margin: 0; | |
display: flex; | |
flex-direction: column; | |
align-items: flex-start; | |
&:last-child { | |
display: none; | |
} | |
} | |
} | |
} | |
.searchBar { | |
width: 100%; | |
height: 60px; | |
background-color: white; | |
position: absolute; | |
top: 60px; | |
animation: mobileMenu 0.3s ease forwards; | |
.searchInput { | |
display: flex; | |
align-items: center; | |
height: 40px; | |
margin-top: 10px; | |
width: 100%; | |
svg { | |
font-size: 20px; | |
flex-shrink: 0; | |
margin-left: 10px; | |
cursor: pointer; | |
} | |
input { | |
width: 100%; | |
height: 50px; | |
background-color: white; | |
outline: 0; | |
border: 0; | |
border-radius: 30px 0 0 30px; | |
padding: 0 15px; | |
font-size: 14px; | |
@include md { | |
height: 60px; | |
font-size: 20px; | |
padding: 0 30px; | |
} | |
} | |
} | |
} | |
} | |
@keyframes mobileMenu { | |
0% { | |
transform: translateY(-130%); | |
} | |
100% { | |
transform: translateY(0); | |
} | |
} | |
import React from "react"; | |
import { LazyLoadImage } from "react-lazy-load-image-component"; | |
import "react-lazy-load-image-component/src/effects/blur.css"; | |
const Img = ({ src, className }) => { | |
return ( | |
<LazyLoadImage | |
className={className || ""} | |
alt="" | |
effect="blur" | |
src={src} | |
/> | |
); | |
}; | |
export default Img; |
:root { | |
font-family: Inter, Avenir, Helvetica, Arial, sans-serif; | |
font-size: 16px; | |
line-height: 1; | |
font-weight: 500; | |
font-synthesis: none; | |
text-rendering: optimizeLegibility; | |
-webkit-font-smoothing: antialiased; | |
-moz-osx-font-smoothing: grayscale; | |
-webkit-text-size-adjust: 100%; | |
--black: #04152d; | |
--black2: #041226; | |
--black3: #020c1b; | |
--black-lighter: #1c4b91; | |
--black-light: #173d77; | |
--pink: #da2f68; | |
--orange: #f89e00; | |
--gradient: linear-gradient(98.37deg, #f89e00 0.99%, #da2f68 100%); | |
} | |
* { | |
margin: 0; | |
padding: 0; | |
box-sizing: border-box; | |
} | |
body { | |
background-color: var(--black); | |
} | |
::-webkit-scrollbar { | |
display: none; | |
} | |
.skeleton { | |
position: relative; | |
overflow: hidden; | |
background-color: #0a2955; | |
&::after { | |
position: absolute; | |
top: 0; | |
right: 0; | |
bottom: 0; | |
left: 0; | |
transform: translateX(-100%); | |
background-image: linear-gradient( | |
90deg, | |
rgba(#193763, 0) 0, | |
rgba(#193763, 0.2) 20%, | |
rgba(#193763, 0.5) 60%, | |
rgba(#193763, 0) | |
); | |
animation: shimmer 2s infinite; | |
content: ""; | |
} | |
@keyframes shimmer { | |
100% { | |
transform: translateX(100%); | |
} | |
} | |
} |
@mixin sm { | |
@media only screen and (min-width: 640px) { | |
@content; | |
} | |
} | |
@mixin md { | |
@media only screen and (min-width: 768px) { | |
@content; | |
} | |
} | |
@mixin lg { | |
@media only screen and (min-width: 1024px) { | |
@content; | |
} | |
} | |
@mixin xl { | |
@media only screen and (min-width: 1280px) { | |
@content; | |
} | |
} | |
@mixin xxl { | |
@media only screen and (min-width: 1536px) { | |
@content; | |
} | |
} | |
@mixin ellipsis($line: 2) { | |
display: -webkit-box; | |
-webkit-line-clamp: $line; | |
-webkit-box-orient: vertical; | |
overflow: hidden; | |
text-overflow: ellipsis; | |
} |
import React from "react"; | |
import dayjs from "dayjs"; | |
import { useNavigate } from "react-router-dom"; | |
import { useSelector } from "react-redux"; | |
import "./style.scss"; | |
import Img from "../lazyLoadImage/Img"; | |
import CircleRating from "../circleRating/CircleRating"; | |
import Genres from "../genres/Genres"; | |
import PosterFallback from "../../assets/no-poster.png"; | |
const MovieCard = ({ data, fromSearch, mediaType }) => { | |
const { url } = useSelector((state) => state.home); | |
const navigate = useNavigate(); | |
const posterUrl = data.poster_path | |
? url.poster + data.poster_path | |
: PosterFallback; | |
return ( | |
<div | |
className="movieCard" | |
onClick={() => | |
navigate(`/${data.media_type || mediaType}/${data.id}`) | |
} | |
> | |
<div className="posterBlock"> | |
<Img className="posterImg" src={posterUrl} /> | |
{!fromSearch && ( | |
<React.Fragment> | |
<CircleRating rating={data.vote_average.toFixed(1)} /> | |
<Genres data={data.genre_ids.slice(0, 2)} /> | |
</React.Fragment> | |
)} | |
</div> | |
<div className="textBlock"> | |
<span className="title">{data.title || data.name}</span> | |
<span className="date"> | |
{dayjs(data.release_date).format("MMM D, YYYY")} | |
</span> | |
</div> | |
</div> | |
); | |
}; | |
export default MovieCard; | |
// CSS | |
@import "../../mixins.scss"; | |
.movieCard { | |
width: calc(50% - 5px); | |
margin-bottom: 25px; | |
cursor: pointer; | |
flex-shrink: 0; | |
@include md { | |
width: calc(25% - 15px); | |
} | |
@include lg { | |
width: calc(20% - 16px); | |
} | |
.posterBlock { | |
position: relative; | |
width: 100%; | |
aspect-ratio: 1 / 1.5; | |
background-size: cover; | |
background-position: center; | |
margin-bottom: 30px; | |
display: flex; | |
align-items: flex-end; | |
justify-content: space-between; | |
padding: 10px; | |
transition: all ease 0.5s; | |
.lazy-load-image-background { | |
position: absolute; | |
top: 0; | |
left: 0; | |
width: 100%; | |
height: 100%; | |
border-radius: 12px; | |
overflow: hidden; | |
img { | |
width: 100%; | |
height: 100%; | |
object-fit: cover; | |
object-position: center; | |
} | |
} | |
.circleRating { | |
width: 40px; | |
height: 40px; | |
position: relative; | |
top: 30px; | |
background-color: white; | |
flex-shrink: 0; | |
@include md { | |
width: 50px; | |
height: 50px; | |
} | |
} | |
.genres { | |
display: none; | |
position: relative; | |
@include md { | |
display: flex; | |
flex-flow: wrap; | |
justify-content: flex-end; | |
} | |
} | |
} | |
.textBlock { | |
color: white; | |
display: flex; | |
flex-direction: column; | |
.title { | |
font-size: 16px; | |
margin-bottom: 10px; | |
line-height: 24px; | |
@include ellipsis(1); | |
@include md { | |
font-size: 20px; | |
} | |
} | |
.date { | |
font-size: 14px; | |
opacity: 0.5; | |
} | |
} | |
&:hover { | |
.posterBlock { | |
opacity: 0.5; | |
} | |
} | |
} |
"dependencies": { | |
"@reduxjs/toolkit": "^1.9.1", | |
"axios": "^1.2.2", | |
"dayjs": "^1.11.7", | |
"react": "^18.2.0", | |
"react-circular-progressbar": "^2.1.0", | |
"react-dom": "^18.2.0", | |
"react-icons": "^4.7.1", | |
"react-infinite-scroll-component": "^6.1.0", | |
"react-lazy-load-image-component": "^1.5.6", | |
"react-player": "^2.11.0", | |
"react-redux": "^8.0.5", | |
"react-router-dom": "^6.6.2", | |
"react-select": "^5.7.0", | |
"sass": "^1.57.1" | |
} |
import React from "react"; | |
import "./style.scss"; | |
import ContentWrapper from "../../components/contentWrapper/ContentWrapper"; | |
const PageNotFound = () => { | |
return ( | |
<div className="pageNotFound"> | |
<ContentWrapper> | |
<span className="bigText">404</span> | |
<span className="smallText">Page not found!</span> | |
</ContentWrapper> | |
</div> | |
); | |
}; | |
export default PageNotFound; | |
// CSS | |
.pageNotFound { | |
height: 700px; | |
padding-top: 200px; | |
.contentWrapper { | |
text-align: center; | |
color: var(--black-light); | |
display: flex; | |
flex-direction: column; | |
.bigText { | |
font-size: 150px; | |
font-weight: 700; | |
} | |
.smallText { | |
font-size: 44px; | |
} | |
} | |
} |
export const PlayIcon = () => { | |
return ( | |
<svg | |
version="1.1" | |
xmlns="http://www.w3.org/2000/svg" | |
xmlnsXlink="http://www.w3.org/1999/xlink" | |
x="0px" | |
y="0px" | |
width="80px" | |
height="80px" | |
viewBox="0 0 213.7 213.7" | |
enableBackground="new 0 0 213.7 213.7" | |
xmlSpace="preserve" | |
> | |
<polygon | |
className="triangle" | |
fill="none" | |
strokeWidth="7" | |
strokeLinecap="round" | |
strokeLinejoin="round" | |
strokeMiterlimit="10" | |
points="73.5,62.5 148.5,105.8 73.5,149.1 " | |
></polygon> | |
<circle | |
className="circle" | |
fill="none" | |
strokeWidth="7" | |
strokeLinecap="round" | |
strokeLinejoin="round" | |
strokeMiterlimit="10" | |
cx="106.8" | |
cy="106.8" | |
r="103.3" | |
></circle> | |
</svg> | |
); | |
}; |
@import "../../mixins.scss"; | |
.searchResultsPage { | |
min-height: 700px; | |
padding-top: 100px; | |
.resultNotFound { | |
font-size: 24px; | |
color: var(--black-light); | |
} | |
.pageTitle { | |
font-size: 24px; | |
line-height: 34px; | |
color: white; | |
margin-bottom: 25px; | |
} | |
.content { | |
display: flex; | |
flex-flow: row wrap; | |
gap: 10px; | |
margin-bottom: 50px; | |
@include md { | |
gap: 20px; | |
} | |
.movieCard { | |
.posterBlock { | |
margin-bottom: 20px; | |
} | |
} | |
} | |
} |
import React from "react"; | |
import "./style.scss"; | |
const Spinner = ({ initial }) => { | |
return ( | |
<div className={`loadingSpinner ${initial ? "initial" : ""}`}> | |
<svg className="spinner" viewBox="0 0 50 50"> | |
<circle | |
className="path" | |
cx="25" | |
cy="25" | |
r="20" | |
fill="none" | |
strokeWidth="5" | |
></circle> | |
</svg> | |
</div> | |
); | |
}; | |
export default Spinner; | |
// CSS | |
.loadingSpinner { | |
width: 100%; | |
height: 150px; | |
position: relative; | |
display: flex; | |
align-items: center; | |
justify-content: center; | |
.spinner { | |
animation: rotate 2s linear infinite; | |
z-index: 2; | |
width: 50px; | |
height: 50px; | |
& .path { | |
stroke: hsl(210, 70, 75); | |
stroke-linecap: round; | |
animation: dash 1.5s ease-in-out infinite; | |
} | |
} | |
&.initial { | |
height: 700px; | |
} | |
@keyframes rotate { | |
100% { | |
transform: rotate(360deg); | |
} | |
} | |
@keyframes dash { | |
0% { | |
stroke-dasharray: 1, 150; | |
stroke-dashoffset: 0; | |
} | |
50% { | |
stroke-dasharray: 90, 150; | |
stroke-dashoffset: -35; | |
} | |
100% { | |
stroke-dasharray: 90, 150; | |
stroke-dashoffset: -124; | |
} | |
} | |
} |
.switchingTabs { | |
height: 34px; | |
background-color: white; | |
border-radius: 20px; | |
padding: 2px; | |
.tabItems { | |
display: flex; | |
align-items: center; | |
height: 30px; | |
position: relative; | |
.tabItem { | |
height: 100%; | |
display: flex; | |
align-items: center; | |
justify-content: center; | |
width: 100px; | |
color: var(--black); | |
font-size: 14px; | |
position: relative; | |
z-index: 1; | |
cursor: pointer; | |
transition: color ease 0.3s; | |
&.active { | |
color: white; | |
} | |
} | |
.movingBg { | |
height: 30px; | |
width: 100px; | |
border-radius: 15px; | |
background-image: var(--gradient); | |
position: absolute; | |
left: 0; | |
transition: left cubic-bezier(0.88, -0.35, 0.565, 1.35) 0.4s; | |
} | |
} | |
} |
import { useEffect, useState } from "react"; | |
import { fetchDataFromApi } from "../utils/api"; | |
const useFetch = (url) => { | |
const [data, setData] = useState(null); | |
const [loading, setLoading] = useState(null); | |
const [error, setError] = useState(null); | |
useEffect(() => { | |
setLoading("loading..."); | |
setData(null); | |
setError(null); | |
fetchDataFromApi(url) | |
.then((res) => { | |
setLoading(false); | |
setData(res); | |
}) | |
.catch((err) => { | |
setLoading(false); | |
setError("Something went wrong!"); | |
}); | |
}, [url]); | |
return { data, loading, error }; | |
}; | |
export default useFetch; |
import React from "react"; | |
import ReactPlayer from "react-player/youtube"; | |
import "./style.scss"; | |
const VideoPopup = ({ show, setShow, videoId, setVideoId }) => { | |
const hidePopup = () => { | |
setShow(false); | |
setVideoId(null); | |
}; | |
return ( | |
<div className={`videoPopup ${show ? "visible" : ""}`}> | |
<div className="opacityLayer" onClick={hidePopup}></div> | |
<div className="videoPlayer"> | |
<span className="closeBtn" onClick={hidePopup}> | |
Close | |
</span> | |
<ReactPlayer | |
url={`https://www.youtube.com/watch?v=${videoId}`} | |
controls | |
width="100%" | |
height="100%" | |
// playing={true} | |
/> | |
</div> | |
</div> | |
); | |
}; | |
export default VideoPopup; | |
// CSS | |
.videoPopup { | |
display: flex; | |
justify-content: center; | |
align-items: center; | |
width: 100%; | |
height: 100%; | |
position: fixed; | |
top: 0; | |
left: 0; | |
opacity: 0; | |
visibility: hidden; | |
z-index: 9; | |
.opacityLayer { | |
position: absolute; | |
top: 0; | |
left: 0; | |
width: 100%; | |
height: 100%; | |
background: rgba(0, 0, 0, 0.25); | |
backdrop-filter: blur(3.5px); | |
-webkit-backdrop-filter: blur(3.5px); | |
opacity: 0; | |
transition: opacity 400ms; | |
} | |
.videoPlayer { | |
position: relative; | |
width: 800px; | |
aspect-ratio: 16 / 9; | |
background-color: white; | |
transform: scale(0.2); | |
transition: transform 250ms; | |
.closeBtn { | |
position: absolute; | |
top: -20px; | |
right: 0; | |
color: white; | |
cursor: pointer; | |
} | |
} | |
&.visible { | |
opacity: 1; | |
visibility: visible; | |
.opacityLayer { | |
opacity: 1; | |
} | |
.videoPlayer { | |
transform: scale(1); | |
} | |
} | |
} |
import React, { useState } from "react"; | |
import "./style.scss"; | |
import ContentWrapper from "../../../components/contentWrapper/ContentWrapper"; | |
import { PlayIcon } from "../playIcon"; | |
import VideoPopup from "../../../components/videoPopup/VideoPopup"; | |
import Img from "../../../components/lazyLoadImage/Img"; | |
const VideosSection = ({ data, loading }) => { | |
const [show, setShow] = useState(false); | |
const [videoId, setVideoId] = useState(null); | |
const loadingSkeleton = () => { | |
return ( | |
<div className="skItem"> | |
<div className="thumb skeleton"></div> | |
<div className="row skeleton"></div> | |
<div className="row2 skeleton"></div> | |
</div> | |
); | |
}; | |
return ( | |
<div className="videosSection"> | |
<ContentWrapper> | |
<div className="sectionHeading">Official Videos</div> | |
{!loading ? ( | |
<div className="videos"> | |
Videos data... | |
</div> | |
) : ( | |
<div className="videoSkeleton"> | |
{loadingSkeleton()} | |
{loadingSkeleton()} | |
{loadingSkeleton()} | |
{loadingSkeleton()} | |
</div> | |
)} | |
</ContentWrapper> | |
<VideoPopup | |
show={show} | |
setShow={setShow} | |
videoId={videoId} | |
setVideoId={setVideoId} | |
/> | |
</div> | |
); | |
}; | |
export default VideosSection; | |
// CSS | |
@import "../../../mixins.scss"; | |
.videosSection { | |
position: relative; | |
margin-bottom: 50px; | |
.sectionHeading { | |
font-size: 24px; | |
color: white; | |
margin-bottom: 25px; | |
} | |
.videos { | |
display: flex; | |
gap: 10px; | |
overflow-x: auto; | |
margin-right: -20px; | |
margin-left: -20px; | |
padding: 0 20px; | |
@include md { | |
gap: 20px; | |
margin: 0; | |
padding: 0; | |
} | |
.videoItem { | |
width: 150px; | |
flex-shrink: 0; | |
@include md { | |
width: 25%; | |
} | |
cursor: pointer; | |
.videoThumbnail { | |
margin-bottom: 15px; | |
position: relative; | |
img { | |
width: 100%; | |
display: block; | |
border-radius: 12px; | |
transition: all 0.7s ease-in-out; | |
} | |
svg { | |
position: absolute; | |
top: 50%; | |
left: 50%; | |
transform: translate(-50%, -50%); | |
width: 50px; | |
height: 50px; | |
} | |
.triangle { | |
stroke-dasharray: 240; | |
stroke-dashoffset: 480; | |
stroke: white; | |
transform: translateY(0); | |
transition: all 0.7s ease-in-out; | |
} | |
.circle { | |
stroke: white; | |
stroke-dasharray: 650; | |
stroke-dashoffset: 1300; | |
transition: all 0.5s ease-in-out; | |
} | |
&:hover { | |
img { | |
opacity: 0.5; | |
} | |
.triangle { | |
stroke-dashoffset: 0; | |
opacity: 1; | |
stroke: var(--pink); | |
animation: trailorPlay 0.7s ease-in-out; | |
} | |
.circle { | |
stroke-dashoffset: 0; | |
stroke: var(--pink); | |
} | |
} | |
} | |
.videoTitle { | |
color: white; | |
font-size: 14px; | |
line-height: 20px; | |
@include md { | |
font-size: 16px; | |
line-height: 24px; | |
} | |
} | |
} | |
} | |
.videoSkeleton { | |
display: flex; | |
gap: 10px; | |
overflow-x: auto; | |
margin-right: -20px; | |
margin-left: -20px; | |
padding: 0 20px; | |
@include md { | |
gap: 20px; | |
margin: 0; | |
padding: 0; | |
} | |
.skItem { | |
width: 150px; | |
flex-shrink: 0; | |
@include md { | |
width: 25%; | |
} | |
.thumb { | |
width: 100%; | |
aspect-ratio: 16 / 9; | |
border-radius: 12px; | |
margin-bottom: 10px; | |
} | |
.row { | |
height: 20px; | |
width: 100%; | |
border-radius: 10px; | |
margin-bottom: 10px; | |
} | |
.row2 { | |
height: 20px; | |
width: 75%; | |
border-radius: 10px; | |
} | |
} | |
} | |
} |
Boht kuch sikhne ko mile. Sukriya. Currently working on this project and this is really a great project. There are little bugs and I'm working on it and xtra kuch features add krne ka soch rha hoon. Sukriya sir.
I keep getting the cors origin error and I have no idea how to fix it please help.
bro i have a problem in hero banner its show undefined every time
and bro when is check the url of image from component and paste it in browser is show image size not supported
bro orignal value of image is not supported in my pc what to do now
bro i have a problem in hero banner its show undefined every time
try async thunk
bro mera Api call nhi ho raha hai kesse solve karen
/movie/popular -- ye tmdb me nhi aa raha
https://api.themoviedb.org/3/discover/movie -- ye link aa raha h
I am still working on this project but your walkthrough was outstanding i will definitely learn lot of thing till end moviex project Thankyou Sharique sir thankyou so much for your worth