diff --git a/src/components/ActionButtons/ActionButtons.jsx b/src/components/ActionButtons/ActionButtons.jsx
new file mode 100644
index 0000000..6dce90c
--- /dev/null
+++ b/src/components/ActionButtons/ActionButtons.jsx
@@ -0,0 +1,96 @@
+import React, { useState } from 'react';
+import { useCart } from '../../context/context';
+import './ActionButtons.scss';
+
+const ActionButtons = ({
+ onPlay,
+ onAddToCart,
+ onBuyNow,
+ movie,
+ variant = 'primary' // primary, secondary, compact
+}) => {
+ const { cart } = useCart();
+ const [isAdding, setIsAdding] = useState(false);
+
+ const isInCart = cart.some(item => item.id === movie?.id);
+
+ const handleAddToCart = async () => {
+ if (isInCart || isAdding) return;
+
+ setIsAdding(true);
+ await onAddToCart();
+
+ setTimeout(() => {
+ setIsAdding(false);
+ }, 1000);
+ };
+
+ const getAddToCartText = () => {
+ if (isAdding) return '✓ Adding...';
+ if (isInCart) return '✓ In Cart';
+ return '+ My List';
+ };
+
+ const getAddToCartClass = () => {
+ let baseClass = `action-buttons__btn action-buttons__btn--${variant}`;
+ if (isInCart) baseClass += ' action-buttons__btn--added';
+ if (isAdding) baseClass += ' action-buttons__btn--adding';
+ return baseClass;
+ };
+
+ return (
+
-
{source}
-
{value}
+// Loading component following Single Responsibility Principle
+const LoadingState = () => (
+
+
+
Loading movie details...
+
Please wait while we fetch the information
);
-function Details() {
- const [movieData, setMovieData] = useState(null);
- const [isLoading, setIsLoading] = useState(true);
- const [error, setError] = useState(null);
- const { title } = useParams();
- const { addToCart } = useCart();
- const navigate = useNavigate();
-
- const [currentIndex, setCurrentIndex] = useState(0);
-
- // ... (useEffect for fetching data should be here) ...
-
- // NEW: Carousel navigation functions
- const goToPrevious = () => {
- // Check if images exist before trying to access length
- if (!movieData || !movieData.Images) return;
- const isFirstSlide = currentIndex === 0;
- const newIndex = isFirstSlide ? movieData.Images.length - 1 : currentIndex - 1;
- setCurrentIndex(newIndex);
- };
-
- const goToNext = () => {
- // Check if images exist
- if (!movieData || !movieData.Images) return;
- const isLastSlide = currentIndex === movieData.Images.length - 1;
- const newIndex = isLastSlide ? 0 : currentIndex + 1;
- setCurrentIndex(newIndex);
- };
-
- // NEW: Cart functionality
- const handleAddToCart = () => {
- if (!movieData) return;
-
- const movie = {
- id: movieData.imdbID || movieData.title,
- title: movieData.title,
- poster: movieData.Poster,
- year: movieData.year,
- genre: movieData.genre,
- price: 9.99 // Default price, could be dynamic
- };
-
- addToCart(movie);
-
- // Show feedback to user
- const button = document.querySelector('.btn-info');
- if (button) {
- const originalText = button.textContent;
- button.textContent = '✓ Added!';
- button.style.background = '#28a745';
- setTimeout(() => {
- button.textContent = originalText;
- button.style.background = '';
- }, 2000);
- }
- };
-
- const handleBuyNow = () => {
- if (!movieData) return;
-
- const movie = {
- id: movieData.imdbID || movieData.title,
- title: movieData.title,
- poster: movieData.Poster,
- year: movieData.year,
- genre: movieData.genre,
- price: 9.99 // Default price, could be dynamic
- };
-
- addToCart(movie);
- navigate('/checkout');
- };
+// Error component following Single Responsibility Principle
+const ErrorState = ({ error, onRetry, onGoHome }) => (
+
+
🎬
+
Oops! Something went wrong
+
{error}
+
+
+
+
+
+);
- const API_URL = `https://www.apirequest.in/movie/api/title/${title}`; // Using a mocky URL with your exact data
+// Movie info section component
+const MovieInfo = ({ movieData }) => {
+ if (!movieData) return null;
- useEffect(() => {
- const fetchMovie = async () => {
- try {
- const response = await fetch(API_URL);
- if (!response.ok) {
- throw new Error('Network response was not ok');
- }
- const data = await response.json();
+ const infoItems = [
+ { label: 'Genre', value: movieData.genre },
+ { label: 'Director', value: movieData.director },
+ { label: 'Cast', value: movieData.actors },
+ { label: 'Language', value: movieData.language },
+ { label: 'Runtime', value: movieData.runtime }
+ ].filter(item => item.value && item.value !== 'N/A');
- // The API returns an array, we take the first element
- const movie = data[0];
+ return (
+
+
Movie Information
+
+ {infoItems.map(({ label, value }) => (
+
+
{label}:
+ {value}
+
+ ))}
+
+
+ );
+};
- if (movie.Response === "False") {
- throw new Error(movie.Error);
- }
-
- setMovieData(movie);
- } catch (err) {
- setError(err.message);
- } finally {
- setIsLoading(false);
- }
- };
+// Main Details component following SOLID principles
+function Details() {
+ const { title } = useParams();
+ const navigate = useNavigate();
+ const { movieData, similarMovies, isLoading, error, refetch } = useMovieDetails(title);
- fetchMovie();
- }, [title]);
+ // Event handlers following Single Responsibility Principle
+ const handleRetry = () => refetch();
+ const handleGoHome = () => navigate('/');
+ // Loading state
if (isLoading) {
+ return
;
+ }
+
+ // Error state
+ if (error) {
return (
-
+
);
}
- if (error) {
+ // No data state
+ if (!movieData) {
return (
-
-
Error: {error}
-
-
+
);
}
return (
- {movieData && (
- <>
- {/* Background Hero Section */}
-
-
-
- {/* CHANGE: Properties are capitalized */}
-
{movieData.title}
-
- {movieData.year}
- {movieData.rated}
- {movieData.runtime}
- $9.99
-
-
{movieData.plot}
-
-
-
-
-
-
+ {/* Hero Section */}
+
+
+ {/* Content Section */}
+
- {/* Detailed Info Section */}
-
-
- {/* CHANGE: Properties are capitalized */}
-
Genre: {movieData.genre}
-
Director: {movieData.director}
-
Actors: {movieData.actors}
-
Language: {movieData.language}
- {movieData.Awards !== 'N/A' &&
Awards: {movieData.Awards}
}
-
- {/* CHANGE: Displaying ratings from new properties */}
-
-
Ratings
- {movieData.imdbRating && }
- {movieData.Metascore && }
-
-
-
- {/* NEW: Image Gallery Section */}
- {movieData.Images && movieData.Images.length > 0 && (
-
-
Gallery
-
-
-
-
- {movieData.Images.map((image, index) => (
-
-

-
- ))}
-
-
-
-
-
- )}
- >
+ {/* Image Gallery */}
+ {movieData.images && movieData.images.length > 0 && (
+
)}
+
+ {/* Similar Movies */}
+
);
}
diff --git a/src/pages/Details/Details.scss b/src/pages/Details/Details.scss
index a9138d0..d1caba2 100644
--- a/src/pages/Details/Details.scss
+++ b/src/pages/Details/Details.scss
@@ -1,259 +1,245 @@
-/* src/components/Details/Details.scss */
-
@use '../../styles/variables' as *;
-// --- Loading/Error states ---
-
-.loading-state, .error-state {
- @include flex-center;
- height: 100vh;
- width: 100%;
- background-color: $background-primary;
- color: $text-primary;
- flex-direction: column;
-
- h1 {
- margin-top: $spacing-xl;
- font-size: $font-size-2xl;
- }
-
- .btn-back {
- @include button-primary;
- margin-top: $spacing-xl;
- }
-}
-
-.loading-spinner {
- @include loading-spinner;
-}
-
-// --- Main Page Styles ---
.details-page {
- background-color: $background-primary;
+ background: $background-primary;
color: $text-primary;
font-family: $font-family-primary;
min-height: 100vh;
+ position: relative;
- // --- Hero Section (Top part with background image) ---
- .details-hero {
- position: relative;
- height: 90vh;
- background-size: cover;
- background-position: top center;
- display: flex;
+ // Loading state
+ &__loading {
+ @include flex-center;
flex-direction: column;
- justify-content: center;
- padding: 0 5%;
+ height: 100vh;
+ text-align: center;
+ animation: fadeIn 0.3s ease-in;
- .hero-overlay {
- position: absolute;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- // Gradient to make text readable over the image
- background: linear-gradient(to top, $background-primary 10%, rgba(20, 20, 20, 0.5) 50%, rgba(20, 20, 20, 0.2) 100%);
+ .loading-spinner {
+ @include loading-spinner;
+ margin-bottom: $spacing-xl;
}
- .hero-content {
- position: relative; // To stay on top of the overlay
- z-index: 2;
- max-width: 50%;
+ h1 {
+ font-size: $font-size-2xl;
+ font-weight: $font-weight-semibold;
+ margin-bottom: $spacing-md;
+ }
- .movie-title {
- font-size: $font-size-6xl;
- font-weight: $font-weight-bold;
- margin-bottom: $spacing-xl;
- }
+ p {
+ color: $text-secondary;
+ font-size: $font-size-base;
+ }
+ }
- .movie-meta {
- display: flex;
- align-items: center;
- gap: $spacing-xl;
- margin-bottom: $spacing-xl;
- color: $text-secondary;
- font-weight: $font-weight-medium;
-
- .rated {
- border: 1px solid $text-secondary;
- padding: $spacing-xs $spacing-sm;
- border-radius: $radius-sm;
- }
-
- .price {
- background: $netflix-red;
- color: $text-primary;
- padding: $spacing-sm $spacing-md;
- border-radius: $radius-sm;
- font-weight: $font-weight-bold;
- }
- }
+ // Error state
+ &__error {
+ @include flex-center;
+ flex-direction: column;
+ height: 100vh;
+ text-align: center;
+ padding: $spacing-2xl;
+ animation: fadeIn 0.3s ease-in;
+
+ .error-icon {
+ font-size: $font-size-6xl;
+ margin-bottom: $spacing-xl;
+ opacity: 0.6;
+ }
+
+ h1 {
+ font-size: $font-size-3xl;
+ font-weight: $font-weight-bold;
+ margin-bottom: $spacing-lg;
+ color: $text-primary;
+ }
+
+ .error-message {
+ font-size: $font-size-lg;
+ color: $text-secondary;
+ margin-bottom: $spacing-2xl;
+ max-width: 500px;
+ line-height: $line-height-relaxed;
+ }
- .movie-plot {
- font-size: $font-size-lg;
- line-height: $line-height-relaxed;
- max-width: 600px;
- margin-bottom: $spacing-2xl;
+ .error-actions {
+ display: flex;
+ gap: $spacing-lg;
+ flex-wrap: wrap;
+ justify-content: center;
+
+ .btn-retry {
+ @include button-primary;
}
- .hero-buttons {
- display: flex;
- gap: $spacing-lg;
-
- button {
- padding: $spacing-md $spacing-2xl;
- border: none;
- border-radius: $radius-sm;
- font-size: $font-size-lg;
- font-weight: $font-weight-bold;
- cursor: pointer;
- transition: $transition-all;
- }
-
- .btn-play {
- background-color: $text-primary;
- color: #000;
- &:hover {
- background-color: rgba(255, 255, 255, 0.8);
- transform: translateY(-2px);
- }
- }
-
- .btn-info {
- background-color: rgba(109, 109, 110, 0.7);
- color: $text-primary;
- &:hover {
- background-color: rgba(109, 109, 110, 0.5);
- transform: translateY(-2px);
- }
- }
+ .btn-home {
+ @include button-secondary;
}
}
}
- // --- Body Section (Below the hero) ---
- .details-body {
- padding: $spacing-3xl 5%;
- display: flex;
+ // Main content layout
+ &__content {
+ display: grid;
+ grid-template-columns: 2fr 1fr;
gap: $spacing-3xl;
+ padding: $spacing-3xl 5%;
+ max-width: 1400px;
+ margin: 0 auto;
- .info-section {
- flex: 2;
- p {
- margin-bottom: $spacing-md;
- line-height: $line-height-normal;
- font-size: $font-size-base;
- strong {
- color: $text-secondary;
- }
- }
+ @media (max-width: $breakpoint-lg) {
+ grid-template-columns: 1fr;
+ gap: $spacing-2xl;
+ padding: $spacing-2xl 5%;
}
+ }
- .ratings-section {
- flex: 1;
- background-color: $surface-primary;
- padding: $spacing-xl;
- border-radius: $radius-lg;
-
- h3 {
- margin-top: 0;
- margin-bottom: $spacing-xl;
- border-bottom: 1px solid $text-secondary;
- padding-bottom: $spacing-md;
- }
- }
+ &__main {
+ min-width: 0; // Prevent flex item overflow
}
-}
-// --- Individual Rating Component Style ---
-.rating {
- display: flex;
- justify-content: space-between;
- margin-bottom: $spacing-md;
- font-size: $font-size-base;
-
- .rating-source {
- color: $text-secondary;
+ &__sidebar {
+ @media (max-width: $breakpoint-lg) {
+ order: -1; // Move ratings above info on mobile
+ }
}
-
- .rating-value {
- font-weight: $font-weight-bold;
+
+ &__gallery {
+ padding: 0 5%;
+ margin-bottom: $spacing-3xl;
}
}
-/* src/components/Details/Details.scss */
-
-// ... (all your existing SCSS styles up to the gallery)
-
-// --- Image Carousel Section ---
-.carousel-section {
- padding: 0 5% $spacing-3xl 5%;
-
- h2 {
- font-size: $font-size-3xl;
- margin-bottom: $spacing-xl;
- border-bottom: 1px solid $text-secondary;
- padding-bottom: $spacing-md;
+// Movie Info component styles
+.movie-info {
+ background: linear-gradient(135deg, $surface-primary 0%, $surface-secondary 100%);
+ border-radius: $radius-xl;
+ padding: $spacing-2xl;
+ border: 1px solid $border-primary;
+ position: relative;
+ overflow: hidden;
+
+ // Subtle background pattern
+ &::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background-image: radial-gradient(circle at 20% 50%, rgba(229, 9, 20, 0.1) 0%, transparent 50%),
+ radial-gradient(circle at 80% 20%, rgba(229, 9, 20, 0.05) 0%, transparent 50%);
+ pointer-events: none;
}
- .carousel {
+ &__title {
+ font-size: $font-size-2xl;
+ font-weight: $font-weight-bold;
+ color: $text-primary;
+ margin-bottom: $spacing-xl;
position: relative;
- width: 100%;
- margin: auto;
+ z-index: 1;
+
+ &::after {
+ content: '';
+ position: absolute;
+ bottom: -$spacing-sm;
+ left: 0;
+ width: 60px;
+ height: 3px;
+ background: linear-gradient(90deg, $netflix-red, transparent);
+ border-radius: $radius-sm;
+ }
}
- // This container clips the content that overflows
- .carousel-inner-container {
- overflow: hidden;
- width: 100%;
- border-radius: $radius-lg;
+ &__content {
+ position: relative;
+ z-index: 1;
}
- // This is the strip of images that moves
- .carousel-inner {
+ .info-item {
display: flex;
- transition: transform 0.5s ease-in-out;
+ padding: $spacing-md 0;
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
+ transition: $transition-all;
+
+ &:last-child {
+ border-bottom: none;
+ }
+
+ &:hover {
+ background: rgba(255, 255, 255, 0.05);
+ margin: 0 (-$spacing-md);
+ padding-left: $spacing-md;
+ padding-right: $spacing-md;
+ border-radius: $radius-sm;
+ }
+
+ .info-label {
+ font-weight: $font-weight-semibold;
+ color: $text-secondary;
+ min-width: 100px;
+ margin-right: $spacing-lg;
+ font-size: $font-size-sm;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+ }
+
+ .info-value {
+ color: $text-primary;
+ font-size: $font-size-base;
+ line-height: $line-height-relaxed;
+ flex: 1;
+ }
}
+}
- .carousel-item {
- min-width: 100%;
- flex-shrink: 0;
-
- img {
- width: 100%;
- height: auto;
- display: block;
+// Responsive design
+@media (max-width: $breakpoint-md) {
+ .details-page {
+ &__content {
+ padding: $spacing-xl 3%;
+ }
+
+ &__gallery {
+ padding: 0 3%;
}
}
- .carousel-btn {
- position: absolute;
- top: 50%;
- transform: translateY(-50%);
- background-color: rgba(0, 0, 0, 0.5);
- color: $text-primary;
- border: none;
- font-size: $font-size-5xl;
- cursor: pointer;
- z-index: $z-dropdown;
- padding: 0 $spacing-lg;
- height: 100%;
- transition: $transition-normal;
+ .movie-info {
+ padding: $spacing-xl;
- &:hover {
- background-color: rgba(0, 0, 0, 0.8);
+ .info-item {
+ flex-direction: column;
+ gap: $spacing-xs;
+
+ .info-label {
+ min-width: auto;
+ margin-right: 0;
+ }
}
+ }
+}
- &.prev {
- left: 0;
- border-top-left-radius: $radius-lg;
- border-bottom-left-radius: $radius-lg;
+@media (max-width: $breakpoint-sm) {
+ .details-page {
+ &__error .error-actions {
+ flex-direction: column;
+ align-items: center;
+
+ .btn-retry,
+ .btn-home {
+ width: 200px;
+ }
}
+ }
+
+ .movie-info {
+ padding: $spacing-lg;
+ border-radius: $radius-lg;
- &.next {
- right: 0;
- border-top-right-radius: $radius-lg;
- border-bottom-right-radius: $radius-lg;
+ &__title {
+ font-size: $font-size-xl;
}
}
}
\ No newline at end of file
diff --git a/src/services/movieService.js b/src/services/movieService.js
new file mode 100644
index 0000000..c0a88e2
--- /dev/null
+++ b/src/services/movieService.js
@@ -0,0 +1,69 @@
+// Movie service for handling all API interactions
+class MovieService {
+ constructor() {
+ this.baseURL = 'https://www.apirequest.in/movie/api';
+ }
+
+ async fetchMovieByTitle(title) {
+ try {
+ const response = await fetch(`${this.baseURL}/title/${title}`);
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+ const data = await response.json();
+ return Array.isArray(data) ? data[0] : data;
+ } catch (error) {
+ throw new Error(`Failed to fetch movie: ${error.message}`);
+ }
+ }
+
+ async fetchSimilarMovies(genre, excludeTitle) {
+ try {
+ // This would typically be a real API call
+ // For now, we'll simulate it with a mock response
+ const mockSimilarMovies = [
+ {
+ id: 'similar1',
+ title: 'Similar Movie 1',
+ Poster: 'https://via.placeholder.com/300x400',
+ Year: '2023',
+ Genre: genre
+ },
+ {
+ id: 'similar2',
+ title: 'Similar Movie 2',
+ Poster: 'https://via.placeholder.com/300x400',
+ Year: '2022',
+ Genre: genre
+ }
+ ];
+ return mockSimilarMovies.filter(movie => movie.title !== excludeTitle);
+ } catch (error) {
+ throw new Error(`Failed to fetch similar movies: ${error.message}`);
+ }
+ }
+
+ // Transform API response to consistent format
+ transformMovieData(apiData) {
+ return {
+ id: apiData.imdbID || apiData.title,
+ title: apiData.title || apiData.Title,
+ poster: apiData.Poster,
+ year: apiData.year || apiData.Year,
+ rated: apiData.rated || apiData.Rated,
+ runtime: apiData.runtime || apiData.Runtime,
+ genre: apiData.genre || apiData.Genre,
+ director: apiData.director || apiData.Director,
+ actors: apiData.actors || apiData.Actors,
+ plot: apiData.plot || apiData.Plot,
+ language: apiData.language || apiData.Language,
+ awards: apiData.Awards,
+ imdbRating: apiData.imdbRating,
+ metascore: apiData.Metascore,
+ images: apiData.Images || [],
+ price: 9.99 // Default price
+ };
+ }
+}
+
+export default new MovieService();
\ No newline at end of file
diff --git a/src/utils/movieUtils.js b/src/utils/movieUtils.js
new file mode 100644
index 0000000..5dccfb7
--- /dev/null
+++ b/src/utils/movieUtils.js
@@ -0,0 +1,50 @@
+// Utility functions for movie-related operations
+export const createMovieObject = (movieData) => ({
+ id: movieData.id || movieData.imdbID || movieData.title,
+ title: movieData.title,
+ poster: movieData.poster,
+ year: movieData.year,
+ genre: movieData.genre,
+ price: movieData.price || 9.99
+});
+
+export const formatRuntime = (runtime) => {
+ if (!runtime || runtime === 'N/A') return '';
+ // Convert "120 min" to "2h 0m" format
+ const match = runtime.match(/(\d+)/);
+ if (!match) return runtime;
+
+ const minutes = parseInt(match[1]);
+ const hours = Math.floor(minutes / 60);
+ const remainingMinutes = minutes % 60;
+
+ if (hours === 0) return `${minutes}m`;
+ return remainingMinutes === 0 ? `${hours}h` : `${hours}h ${remainingMinutes}m`;
+};
+
+export const parseRating = (rating) => {
+ if (!rating || rating === 'N/A') return null;
+ const numericRating = parseFloat(rating);
+ return isNaN(numericRating) ? null : numericRating;
+};
+
+export const getRatingColor = (rating) => {
+ if (!rating) return '#666';
+ if (rating >= 8) return '#4CAF50'; // Green
+ if (rating >= 6) return '#FF9800'; // Orange
+ return '#f44336'; // Red
+};
+
+export const truncateText = (text, maxLength = 150) => {
+ if (!text || text.length <= maxLength) return text;
+ return text.substring(0, maxLength).trim() + '...';
+};
+
+export const formatGenres = (genreString) => {
+ if (!genreString) return [];
+ return genreString.split(',').map(genre => genre.trim());
+};
+
+export const getImageWithFallback = (primaryImage, fallbackImage) => {
+ return primaryImage && primaryImage !== 'N/A' ? primaryImage : fallbackImage;
+};
\ No newline at end of file