Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Exam Fullstack Frontend Team 3 #77

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .env.development
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ BACKEND_API_HOST=https://demo.duendesoftware.com
OIDC_ISSUER=https://demo.duendesoftware.com
OIDC_CLIENT_ID=interactive.public.short
OIDC_SCOPE=openid profile email api offline_access
CGV_API=https://m.cgv.id
1 change: 1 addition & 0 deletions appsettings.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ module.exports = {
oidcIssuer: process.env['OIDC_ISSUER'] ?? '',
oidcClientId: process.env['OIDC_CLIENT_ID'] ?? '',
oidcScope: process.env['OIDC_SCOPE'] ?? '',
cgvapi: process.env['CGV_API'] ?? '',
};
157 changes: 157 additions & 0 deletions components/BannerPromotions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import { useState } from 'react';
import { CSSTransition, TransitionGroup } from 'react-transition-group';
import { createUseStyles } from 'react-jss';

const promotions = [
{
id: 1,
title: 'Promo 1',
description: 'Diskon hingga 50% untuk produk tertentu',
imageUrl: 'https://cdn.cgv.id/uploads_v2/promotions/2405/PR202405171125514961.jpg',
},
{
id: 2,
title: 'Promo 2',
description: 'Gratis ongkir untuk pembelian di atas Rp100.000',
imageUrl: 'https://cdn.cgv.id/uploads_v2/promotions/2405/PR202405100933495889.jpg',
},
{
id: 3,
title: 'Promo 3',
description: 'Beli 1 gratis 1 untuk produk tertentu',
imageUrl: 'https://cdn.cgv.id/uploads_v2/promotions/2405/PR202405021526585021.jpg',
},
{
id: 4,
title: 'Promo 4',
description: 'Diskon hingga 30% untuk produk tertentu',
imageUrl: 'https://cdn.cgv.id/uploads_v2/promotions/2405/PR202405021526585021.jpg',
},
{
id: 5,
title: 'Promo 5',
description: 'Beli 2 gratis 1 untuk produk tertentu',
imageUrl: 'https://cdn.cgv.id/uploads_v2/promotions/2405/PR202405021526585021.jpg',
},
{
id: 6,
title: 'Promo 6',
description: 'Cashback hingga 20% untuk produk tertentu',
imageUrl: 'https://cdn.cgv.id/uploads_v2/promotions/2405/PR202405021526585021.jpg',
},
];

const useStyles = createUseStyles({
slideEnter: {
opacity: 0,
transform: 'translateX(100%)',
},
slideEnterActive: {
opacity: 1,
transform: 'translateX(0)',
transition: 'opacity 500ms, transform 500ms',
},
slideExit: {
opacity: 1,
transform: 'translateX(0)',
},
slideExitActive: {
opacity: 0,
transform: 'translateX(-100%)',
transition: 'opacity 500ms, transform 500ms',
},
});

const BannerPromotions = () => {
const classes = useStyles();
const [currentPromotionIndex, setCurrentPromotionIndex] = useState(0);

const handlePrev = () => {
setCurrentPromotionIndex((prevIndex) =>
prevIndex === 0 ? promotions.length - 1 : prevIndex - 1
);
};

const handleNext = () => {
setCurrentPromotionIndex((prevIndex) =>
prevIndex === promotions.length - 1 ? 0 : prevIndex + 1
);
};

return (
<div className="relative w-full max-w-4xl mx-auto my-8 overflow-hidden rounded-lg shadow-lg">
<div className="relative flex items-center justify-center w-full h-64">
<button
className="absolute left-0 z-10 p-2 m-2 text-red-500 bg-black bg-opacity-20 rounded-full hover:bg-opacity-75"
onClick={handlePrev}
>
<svg
className="w-6 h-6"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M15 19l-7-7 7-7"
></path>
</svg>
</button>
<div className="relative w-full h-full overflow-hidden">
<TransitionGroup className="h-full">
<CSSTransition
key={promotions[currentPromotionIndex].id}
timeout={500}
classNames={{
enter: classes.slideEnter,
enterActive: classes.slideEnterActive,
exit: classes.slideExit,
exitActive: classes.slideExitActive,
}}
>
<div className="absolute inset-0 w-full h-full">
<img
src={promotions[currentPromotionIndex].imageUrl}
alt={promotions[currentPromotionIndex].title}
className="object-cover w-full h-full"
/>
<div className="absolute bottom-0 left-0 right-0 p-4 bg-gradient-to-t from-black via-transparent to-transparent">
<h3 className="text-lg font-bold text-white">
{promotions[currentPromotionIndex].title}
</h3>
<p className="text-sm text-white">
{promotions[currentPromotionIndex].description}
</p>
</div>
</div>
</CSSTransition>
</TransitionGroup>
</div>
<button
className="absolute right-0 z-10 p-2 m-2 text-red-500 bg-black bg-opacity-20 rounded-full hover:bg-opacity-75"
onClick={handleNext}
>
<svg
className="w-6 h-6"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M9 5l7 7-7 7"
></path>
</svg>
</button>
</div>
</div>
);
};

export default BannerPromotions;
51 changes: 51 additions & 0 deletions components/DayButtons.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import React, { useState } from 'react';

const getNextFiveDays = (): Date[] => {
const days: Date[] = [];
const today = new Date();
for (let i = 0; i < 5; i++) {
const date = new Date(today);
date.setDate(today.getDate() + i);
days.push(date);
}
return days;
};

const formatDate = (date: Date): string => {
const options: Intl.DateTimeFormatOptions = { weekday: 'short', month: 'short', day: 'numeric' };
return date.toLocaleDateString(undefined, options);
};

interface DayButtonsProps {
onDateChange: (date: Date) => void;
}

const DayButtons: React.FC<DayButtonsProps> = ({ onDateChange }) => {
const days = getNextFiveDays();
const [selectedDay, setSelectedDay] = useState<Date>(days[0]);

const handleButtonClick = (day: Date) => {
setSelectedDay(day);
onDateChange(day);
};

return (
<div className="flex space-x-2 mt-4 justify-center">
{days.map((day, index) => (
<button
key={index}
onClick={() => handleButtonClick(day)}
className={`px-4 py-2 rounded border ${
selectedDay.toDateString() === day.toDateString()
? 'bg-red-500 text-white'
: 'bg-white border-red-500 text-red-500 hover:bg-red-500 hover:text-white'
}`}
>
{formatDate(day)}
</button>
))}
</div>
);
};

export default DayButtons;
47 changes: 47 additions & 0 deletions components/Footer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import React from 'react';
import Link from 'next/link';

const Footer: React.FC = () => {
return (
<footer className="bg-red-950 text-white py-8 px-10">
<div className="container mx-auto px-4">
<div className="flex flex-wrap justify-between mx-10">
<div className="w-full md:w-1/3 mb-6 md:mb-0">
<Link href="/" className="flex items-center space-x-3 rtl:space-x-reverse">
<img src="https://vietguys.biz/storage/case_study/pVLqwuM1fUAsly6tYNbJZ0gdhowi70RuHxeHJpz9.png" className="h-8" alt="CGV Logo" />
</Link>
<p className="text-gray-400 mt-4">CGV Cinemas Indonesia (CJ CGV Cinemas Indonesia)</p>
</div>

<div className="w-full md:w-1/3 mb-6 md:mb-0">
<h3 className="text-xl font-semibold mb-2">Quick Links</h3>
<ul>
<li className="mb-1"><Link href="/" className="text-gray-400 hover:text-white">Home</Link></li>
<li className="mb-1"><Link href="/movies" className="text-gray-400 hover:text-white">Movies</Link></li>
<li className="mb-1"><Link href="#" className="text-gray-400 hover:text-white">Cinemas</Link></li>
<li className="mb-1"><Link href="#" className="text-gray-400 hover:text-white">Contact Us</Link></li>
</ul>
</div>

<div className="w-full md:w-1/3">
<h3 className="text-xl font-semibold mb-2">Follow Us</h3>
<div className="flex space-x-4">
<a href="https://twitter.com/CGV_ID" className="text-gray-400 hover:text-white">
<svg className="w-6 h-6" fill="currentColor" viewBox="0 0 24 24"><path d="M22.54 6.42c.01.16.01.32.01.48 0 4.9-3.72 10.56-10.56 10.56-2.1 0-4.07-.61-5.72-1.67.29.03.59.04.88.04 1.75 0 3.36-.6 4.64-1.61a3.73 3.73 0 01-3.48-2.59c.25.05.5.08.77.08.37 0 .72-.05 1.06-.14a3.72 3.72 0 01-2.98-3.65v-.05c.5.28 1.07.45 1.67.47a3.72 3.72 0 01-1.66-3.09c0-.68.18-1.31.5-1.86a10.56 10.56 0 007.67 3.88c-.06-.27-.09-.55-.09-.84a3.72 3.72 0 016.44-2.55 7.43 7.43 0 002.36-.9 3.74 3.74 0 01-1.63 2.05 7.45 7.45 0 002.13-.58 7.94 7.94 0 01-1.86 1.94z"/></svg>
</a>
<a href="https://www.facebook.com/CGV.ID" className="text-gray-400 hover:text-white">
<svg className="w-6 h-6" fill="currentColor" viewBox="0 0 24 24"><path d="M19.39 2.01H4.61A2.6 2.6 0 002 4.61v14.78c0 1.43 1.18 2.6 2.61 2.6h7.93v-6.41h-2.14v-2.5h2.14v-1.84c0-2.11 1.29-3.27 3.19-3.27.91 0 1.68.07 1.91.1v2.21h-1.31c-1.02 0-1.22.49-1.22 1.2v1.59h2.44l-.32 2.5h-2.12V22h4.16c1.43 0 2.61-1.17 2.61-2.6V4.61c0-1.43-1.18-2.6-2.61-2.6z"/></svg>
</a>
</div>
</div>
</div>

<div className="mt-8 text-center text-gray-500">
<p>&copy; 2024 CGV Cinema. All rights reserved.</p>
</div>
</div>
</footer>
);
};

export default Footer;
28 changes: 28 additions & 0 deletions components/LoadingSpinner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import React, { useState, useEffect } from 'react';

const LoadingSpinner: React.FC = () => {
const [dots, setDots] = useState<string>('.');

useEffect(() => {
const interval = setInterval(() => {
setDots(prevDots => (prevDots.length < 3 ? prevDots + '.' : '.'));
}, 350);

return () => clearInterval(interval);
}, []);

return (
<div role="status" className="fixed top-0 left-0 w-full h-full flex flex-col items-center justify-center">
<div className="relative">
<svg aria-hidden="true" className="w-14 h-14 text-gray-200 animate-spin dark:text-gray-600 fill-red-500" viewBox="0 0 100 101" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z" fill="currentColor"/>
<path d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z" fill="currentFill"/>
</svg>
<span className="sr-only">Loading{dots}</span>
</div>
<span className="text-gray-500 mt-2 text-xl">Loading{dots}</span>
</div>
);
};

export default LoadingSpinner;
75 changes: 75 additions & 0 deletions components/MovieBanner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import React, { useState } from 'react';
import Link from 'next/link';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faChevronLeft, faChevronRight } from '@fortawesome/free-solid-svg-icons';
import movies from '@/functions/moviesData';

const splitTitle = (title: string) => {
const words = title.split(' ');
if (words.length > 3) {
return [words.slice(0, 3).join(' '), words.slice(3).join(' ')];
}
return [title];
};

const MovieBanner: React.FC = () => {
const [currentPage, setCurrentPage] = useState(0);

const nextPage = () => {
setCurrentPage((prev) => Math.min(prev + 1, movies.length - 4));
};

const prevPage = () => {
setCurrentPage((prev) => Math.max(prev - 1, 0));
};

const startIndex = currentPage;
const endIndex = startIndex + 4;
const visibleMovies = movies.slice(startIndex, endIndex);

return (
<div className="container mx-auto py-8 relative px-16">
<div className="grid sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
{visibleMovies.map((movie) => (
<div key={movie.id} className="max-w-lg mx-auto mb-8 flex flex-col justify-between h-full">
<div className="relative">
<img src={movie.posterUrl} alt={movie.title} className="w-full rounded-lg shadow-md" />
</div>
<div className="text-center mt-2 flex flex-col justify-between flex-grow">
<div>
{splitTitle(movie.title).map((line, index) => (
<h3 key={index} className="text-lg font-semibold">
{line}
</h3>
))}
</div>
<Link href={`/movies/schedule/${movie.id}`} legacyBehavior>
<a className=" bg-white hover:bg-red-500 hover:text-white border border-red-500 text-red-500 font-bold py-2 px-4 rounded text-sm max-w-[200px] mx-auto">
BOOK NOW
</a>
</Link>
</div>
</div>
))}
</div>
{currentPage > 0 && (
<button
onClick={prevPage}
className="absolute left-0 top-[40%] transform -translate-y-1/2 px-2 py-1 rounded-lg bg-gray-300 hover:bg-gray-400 mx-10"
>
<FontAwesomeIcon icon={faChevronLeft} />
</button>
)}
{endIndex < movies.length && (
<button
onClick={nextPage}
className="absolute right-0 top-[40%] transform -translate-y-1/2 px-2 py-1 rounded-lg bg-gray-300 hover:bg-gray-400 mx-10"
>
<FontAwesomeIcon icon={faChevronRight} />
</button>
)}
</div>
);
};

export default MovieBanner;
Loading
Loading