I am creating Amazon Prime like design to stream movies, series etc and I’m having trouble with the visibility and positioning of a dropdown component in my MediaDetailPage
. I want the dropdown menu to be positioned at the bottom center of the page, when user clicks on the dropdown text.
sample MediaDetailPage
MediaDetailPage.js
import React, { useState, useEffect } from 'react';
import { useParams, useNavigate, useLocation } from 'react-router-dom';
import { Helmet } from 'react-helmet';
import SeasonEpisodeList from '../components/SeasonEpisodeList';
import '../MediaDetailPage.css';
const MediaDetailPage = () => {
const { id } = useParams();
const [movie, setMovie] = useState(null);
const [loading, setLoading] = useState(true);
const navigate = useNavigate();
const apiBaseUrl = process.env.REACT_APP_API_BASE_URL;
const location = useLocation(); // location object from the current route
// Use content_type from passed state, fallback to 'default' if it's null/undefined
const contentType = location?.state?.item?.content_type || 'default';
useEffect(() => {
const fetchMovieDetails = async () => {
try {
const response = await fetch(`${apiBaseUrl}/api/v1/listings/content/${id}/?content_type=${contentType}`);
if (response.ok) {
const data = await response.json();
setMovie(data);
} else if (response.status === 404) {
navigate('/404'); // Redirect to the 404 page
} else {
console.error('Unexpected response status:', response.status);
}
} catch (error) {
console.error('Error fetching movie details:', error);
} finally {
setLoading(false);
}
};
fetchMovieDetails();
}, [id, contentType, apiBaseUrl, navigate]);
const handlePlay = () => {
navigate(`/video/${id}/`);
};
if (loading) return <div>Loading...</div>;
if (!movie) return null;
return (
<div className="media-detail-page">
<Helmet>
<title>{movie.title} - Cineflare</title>
<meta name="description" content={movie.description} />
<link rel="canonical" href={window.location.href} />
</Helmet>
<div className="media-detail-background-wrapper">
<img
src={movie.thumbnail_url}
alt={movie.title}
className="media-detail-background"
/>
</div>
<div className="media-detail-content">
<h1>{movie.title}</h1>
<p>{movie.description}</p>
<div className="details-row">
<span>{movie.duration}</span>
<span>{movie.release_date}</span>
</div>
<div className="tags-row">
{movie.genres && movie.genres.map((genre) => (
<span key={genre.id} className="tag">{genre.name}</span>
))}
</div>
<div className="button-row">
{
contentType === 'movie' && (
<button className="play-button" onClick={handlePlay}>
Play Now
</button>
)
}
</div>
{/* Render the Season and Episode list if available */}
{movie.latest_season && movie.season_ids && <SeasonEpisodeList data={movie} />}
</div>
</div>
);
};
export default MediaDetailPage;
MediaDetailPage.css
.media-detail-page {
position: relative;
width: 100%;
height: 100vh; /* Full screen height */
overflow: hidden;
display: flex;
flex-direction: column;
justify-content: flex-start; /* Align items to the top */
color: #fff;
background-color: transparent;
}
/* Wrapper for the background image (thumbnail) */
.media-detail-background-wrapper {
position: relative;
width: 98vw; /* Cover 98% of the screen width */
height: 70vh; /* Cover 70% of the screen height */
margin: 0 auto;
overflow: hidden;
background-color: #210d0d; /* Fallback color */
}
/* Background thumbnail image */
.media-detail-background {
width: 100%;
height: 100%;
object-fit: cover; /* Cover the entire background while maintaining aspect ratio */
filter: blur(5px); /* Blur effect */
position: absolute;
top: 0;
left: 0;
z-index: -1; /* Ensures it stays behind the content */
}
/* Main content of the media detail */
.media-detail-content {
position: absolute;
bottom: 0; /* Align the content to the bottom of the thumbnail */
z-index: 1;
width: 100%;
padding: 20px;
box-sizing: border-box;
background: rgba(0, 0, 0, 0.7); /* Semi-transparent background to make content readable */
}
.details-row,
.tags-row {
margin: 10px 0;
}
.tag {
display: inline-block;
margin-right: 10px;
padding: 5px 10px;
background-color: rgba(255, 255, 255, 0.2);
border-radius: 20px;
font-size: 0.9rem;
}
.button-row {
margin-top: 10px;
display: flex;
justify-content: flex-start; /* Align play button to the left */
width: 100%;
}
.play-button {
padding: 10px 20px;
background-color: #00a8e1;
color: #fff;
border: none;
cursor: pointer;
font-size: 1rem;
border-radius: 5px;
}
/* Make the layout responsive */
@media (max-width: 768px) {
.media-detail-background-wrapper {
height: 50vh; /* Reduce thumbnail height on smaller screens */
}
.episode-list {
height: 50vh; /* Adjust episode list height */
}
.play-button {
font-size: 12px;
padding: 6px 12px;
}
}
SeasonEpisodeList.js
import React, { useState } from 'react';
import SeasonSelector from './SeasonSelector';
import axios from 'axios';
import '../SeasonEpisodeList.css';
// Function to check if the device is mobile
const isMobileDevice = () => {
return window.innerWidth <= 768; // Mobile view if width is 768px or less
};
const SeasonEpisodeList = ({ data }) => {
const { latest_season, season_ids } = data;
const [selectedSeasonId, setSelectedSeasonId] = useState(latest_season.id);
const [selectedSeason, setSelectedSeason] = useState(latest_season);
const [isLoading, setIsLoading] = useState(false);
const [expandedEpisodeId, setExpandedEpisodeId] = useState(null);
const apiBaseUrl = process.env.REACT_APP_API_BASE_URL;
const handleSeasonChange = async (newSeasonId) => {
if (typeof newSeasonId !== 'string') {
console.error('Invalid Season ID passed:', newSeasonId);
return;
}
setSelectedSeasonId(newSeasonId);
setIsLoading(true);
try {
const response = await axios.get(`${apiBaseUrl}/api/v1/listings/content/${newSeasonId}/?content_type=season`);
setSelectedSeason(response.data);
} catch (error) {
console.error('Error fetching season data:', error);
} finally {
setIsLoading(false);
}
};
const toggleEpisodeExpand = (episodeId) => {
if (isMobileDevice()) {
setExpandedEpisodeId((prev) => (prev === episodeId ? null : episodeId));
}
};
return (
<div className="season-episode-container">
<SeasonSelector
seasonIds={season_ids}
selectedSeasonId={selectedSeasonId}
onSeasonChange={handleSeasonChange}
/>
{/* Scrollable Episode List */}
<div className="episode-list">
{isLoading ? (
<p>Loading episodes...</p>
) : (
selectedSeason.episodes && selectedSeason.episodes.length > 0 ? (
selectedSeason.episodes.map((episode) => (
<div key={episode.id} className={`episode-card ${expandedEpisodeId === episode.id ? 'expanded' : ''}`}>
<div className="episode-info" onClick={() => toggleEpisodeExpand(episode.id)}>
<div className="episode-text">
<h4>{`S${selectedSeason.season_number} E${episode.episode_number}`}</h4>
<p>{episode.title}</p>
</div>
{isMobileDevice() ? (
<button className="play-button" onClick={(e) => {
e.stopPropagation();
window.location.href = `/video/${episode.id}`;
}}>
Play Now
</button>
) : (
<div className="episode-details">
<img src={episode.thumbnail_url} alt={episode.title} className="episode-thumbnail" />
<p className="release-year">{episode.release_year || 'Release Year Not Available'}</p>
<p className="episode-description">{episode.description}</p>
</div>
)}
</div>
{expandedEpisodeId === episode.id && isMobileDevice() && (
<div className="episode-extra-details">
<img src={episode.thumbnail_url} alt={episode.title} className="episode-thumbnail" />
<div>
<p className="release-year">{episode.release_year || 'Release Year Not Available'}</p>
<p className="episode-description">{episode.description}</p>
</div>
</div>
)}
</div>
))
) : (
<p>No episodes available for this season.</p>
)
)}
</div>
</div>
);
};
export default SeasonEpisodeList;
SeasonEpisodeList.css
/* General styles for the container */
.season-episode-container {
margin: 20px; /* Add margin as needed */
}
/* Scrollable Episode List */
.episode-list {
height: auto; /* Set to auto to accommodate content */
overflow-y: auto; /* Enable scrolling for overflow */
}
/* Episode card styles */
.episode-card {
display: flex;
flex-direction: column; /* Stack elements vertically */
margin-bottom: 10px;
padding: 10px;
background-color: rgba(255, 255, 255, 0.1); /* Semi-transparent card background */
border-radius: 5px;
color: #fff;
transition: all 0.3s ease;
}
/* Styles for expanded episode */
.episode-card.expanded {
background-color: rgba(255, 255, 255, 0.15);
}
/* Episode info layout */
.episode-info {
display: flex; /* Flexbox for layout */
justify-content: space-between; /* Space elements */
align-items: center; /* Center align */
}
/* Episode text section */
.episode-text {
flex: 1; /* Allow this to take remaining space */
}
/* Play button styles */
.play-button {
background-color: #00a8e1;
border: none;
color: white;
padding: 8px 16px;
border-radius: 5px;
cursor: pointer;
transition: background-color 0.3s;
}
/* Play button hover effect */
.play-button:hover {
background-color: #0056b3; /* Darker blue on hover */
}
/* Extra details for expanded episode */
.episode-extra-details {
display: flex; /* Use flexbox for layout */
margin-top: 10px; /* Space above */
}
/* Thumbnail styling */
.episode-thumbnail {
width: 100px; /* Fixed width for thumbnail */
height: auto; /* Maintain aspect ratio */
border-radius: 5px; /* Rounded corners */
margin-right: 10px; /* Space between thumbnail and description */
}
/* Release year style */
.release-year {
font-weight: bold;
color: #f0f0f0; /* Adjust text color for better readability */
}
/* Episode description style */
.episode-description {
margin-top: 5px; /* Space above description */
color: #d0d0d0; /* Adjust text color for better readability */
}
/* Adjustments for mobile view */
@media (max-width: 768px) {
/* Ensure the episode card layout is flexible on mobile */
.episode-card {
flex-direction: column; /* Stack elements on top of each other */
}
}
SeasonSelector.js
import React, { useState, useEffect, useRef } from 'react';
import '../SeasonEpisodeList.css'; // Ensure your CSS file path is correct
const useIsMobile = () => {
const [isMobile, setIsMobile] = useState(window.innerWidth <= 768);
useEffect(() => {
const handleResize = () => {
setIsMobile(window.innerWidth <= 768);
};
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
return isMobile;
};
const SeasonSelector = ({ seasonIds, selectedSeasonId, onSeasonChange }) => {
const [showDropdown, setShowDropdown] = useState(false);
const isMobile = useIsMobile();
const dropdownRef = useRef(null);
const selectedSeasonNumber = Object.keys(seasonIds).find(
(seasonNumber) => seasonIds[seasonNumber] === selectedSeasonId
) || Object.keys(seasonIds)[0];
const handleDropdownToggle = (event) => {
event.stopPropagation();
setShowDropdown((prev) => !prev);
};
const handleSeasonSelect = (seasonId, event) => {
event.stopPropagation();
setShowDropdown(false);
onSeasonChange(seasonId);
};
const handleClickOutside = (event) => {
if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
setShowDropdown(false);
}
};
useEffect(() => {
if (showDropdown) {
window.addEventListener('click', handleClickOutside);
} else {
window.removeEventListener('click', handleClickOutside);
}
return () => {
window.removeEventListener('click', handleClickOutside);
};
}, [showDropdown]);
return (
<div className="dropdown-container" ref={dropdownRef}>
{isMobile ? (
<div onClick={handleDropdownToggle} className="dropdown-toggle">
{`Season ${selectedSeasonNumber}`}
{showDropdown && (
<div className="mobile-dropdown-list">
{Object.entries(seasonIds).map(([seasonNumber, seasonId]) => (
<div
key={seasonId}
onClick={(e) => handleSeasonSelect(seasonId, e)}
className="mobile-dropdown-item"
>
{`Season ${seasonNumber}`}
</div>
))}
</div>
)}
</div>
) : (
<select
value={selectedSeasonId}
onChange={(e) => handleSeasonSelect(e.target.value, e)}
className="dropdown-list"
onClick={(e) => e.stopPropagation()}
>
{Object.entries(seasonIds).map(([seasonNumber, seasonId]) => (
<option key={seasonId} value={seasonId}>
{`Season ${seasonNumber}`}
</option>
))}
</select>
)}
</div>
);
};
export default SeasonSelector;
SeasonSelector.css
/* SeasonEpisodeList.css */
/* General styles for the container */
.season-container {
padding: 20px;
color: white;
}
/* Dropdown styles */
.dropdown-container {
position: relative; /* Allow absolute positioning of dropdown items */
}
/* Mobile dropdown toggle style */
.dropdown-toggle {
cursor: pointer;
padding: 10px;
background-color: #444; /* Background color for the toggle */
border-radius: 10px; /* Rounded corners */
text-align: center; /* Center the text */
margin-bottom: 10px; /* Space below the toggle */
}
/* Mobile dropdown list styles */
.mobile-dropdown-list {
position: fixed; /* Fix it to the bottom of the viewport */
bottom: 0; /* Align to the bottom */
left: 50%; /* Start from the left center */
transform: translateX(-50%); /* Center the dropdown */
background-color: #333; /* Dropdown background */
border-radius: 10px; /* Rounded corners */
z-index: 1000; /* Ensure it appears above other elements */
width: 90%; /* Set width */
max-height: 50vh; /* Maximum height */
overflow-y: auto; /* Allow scrolling */
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.5); /* Optional shadow */
opacity: 1; /* Fully visible when shown */
}
/* Mobile dropdown item styles */
.mobile-dropdown-item {
padding: 10px; /* Padding for items */
color: white; /* Text color */
cursor: pointer; /* Pointer cursor */
}
/* Add hover effect for dropdown items */
.mobile-dropdown-item:hover {
background-color: #555; /* Darker background on hover */
}
/* Episode list styles */
.episode-list {
width: 100%; /* Full width */
padding: 0 15px; /* Optional: Add padding for spacing on sides */
box-sizing: border-box; /* Include padding and border in element's total width */
}
.episode-item {
display: flex;
align-items: center;
gap: 20px;
background-color: #222;
padding: 10px;
border-radius: 10px;
}
.episode-item img {
width: 150px;
height: 100px;
border-radius: 5px;
object-fit: cover;
}
.episode-details {
flex-grow: 1;
}
.episode-player {
margin-top: 30px;
}
With this code, I am unable to bring the dropdown menu at the bottom center, I am expecting the dropdown to be positioned correctly as in this image, but in bottom center – Amazon Prime Dropdown reference