first commit

This commit is contained in:
2024-10-05 15:04:17 +08:00
parent 792a3d9db1
commit ee746cd61d
44 changed files with 2399 additions and 124 deletions

View File

@@ -0,0 +1,124 @@
"use client"
import React from 'react'
import Collapse from './Collapse'
import ScheduleCollapse from './ScheduleCollapse'
import { CoursesProps, ScheduleProps, RerganizedScheduleProps } from '@/types'
async function reorganizedSchedule(schedule: ScheduleProps[]) {
const sortedSchedule = schedule.sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime());
// Then, reorganize the sorted schedule
const reorganizedSchedule = sortedSchedule.reduce<Record<string, { yearAndMonth: string; schedule: ScheduleProps[] }>>((acc, item) => {
const date = new Date(item.date);
const yearMonth = `${date.getFullYear()}${String(date.getMonth() + 1).padStart(2, '0')}`;
if (!acc[yearMonth]) {
acc[yearMonth] = {
yearAndMonth: yearMonth,
schedule: []
};
}
acc[yearMonth].schedule.push(item);
return acc;
}, {});
return Object.values(reorganizedSchedule);
}
const Accordion = async ({ courseData }: { courseData: CoursesProps }) => {
const newSchedule = await reorganizedSchedule(courseData.schedule)
const fakeData = [
{
"yearAndMonth": "2024年10月",
"schedule": [
{
"info2": "A room, 14F, Sha Tin Market, N.T.",
"info1": "11:00-12:00, 13:00-14:00, 15:00-16:00, 17:00-18:00",
"id": "e19a7934-82fe-4027-aca6-57a419e9623e",
"title": "A班",
"date": "2024-10-05T01:30:00Z",
"course_id": "29ab193d-2927-459d-9277-5520771b2dd6"
},
{
"info2": "Room B, Sheung Shui Village, N.T.",
"info1": "11:00-12:00, 13:00-14:00, 15:00-16:00, 17:00-18:00",
"id": "dc49a1b6-1c1f-44d6-bc52-b757aeba7b5b",
"title": "B班",
"date": "2024-10-05T01:31:00Z",
"course_id": "29ab193d-2927-459d-9277-5520771b2dd6"
},
{
"info2": "Room B, Sheung Shui Village, N.T.",
"info1": "11:00-12:00, 13:00-14:00, 15:00-16:00, 17:00-18:00",
"id": "d4785a90-311b-4a91-8fb7-beb077245f4f",
"title": "A班",
"date": "2024-10-06T01:31:00Z",
"course_id": "29ab193d-2927-459d-9277-5520771b2dd6"
}
]
},
{
"yearAndMonth": "2024年11月",
"schedule": [
{
"info2": "Room B, Sheung Shui Village, N.T.",
"info1": "11:00-12:00, 13:00-14:00, 15:00-16:00, 17:00-18:00",
"id": "5c88b2a7-6b4d-455f-b341-1fec3173d208",
"title": "A班",
"date": "2024-11-06T01:31:00Z",
"course_id": "29ab193d-2927-459d-9277-5520771b2dd6"
}
]
},
{
"yearAndMonth": "2025年01月",
"schedule": [
{
"info2": "Room B, Sheung Shui Village, N.T.",
"info1": "11:00-12:00, 13:00-14:00, 15:00-16:00, 17:00-18:00",
"id": "8a30dec2-63ed-4237-8f5b-177c42f29dda",
"title": "B班",
"date": "2025-01-04T01:31:00Z",
"course_id": "29ab193d-2927-459d-9277-5520771b2dd6"
},
{
"info2": "Room B, Sheung Shui Village, N.T.",
"info1": "11:00-12:00, 13:00-14:00, 15:00-16:00, 17:00-18:00",
"id": "86ddf906-499b-4dbd-967d-cc4dddd9f27b",
"title": "A班",
"date": "2025-01-05T01:31:00Z",
"course_id": "29ab193d-2927-459d-9277-5520771b2dd6"
}
]
}
]
console.log(newSchedule)
return (
<div className='relative flex flex-col w-full h-auto items-center pb-60'>
<Collapse title='課程資訊' children={courseData.information} />
<Collapse title='課程內容' children={courseData.contant} info={true} info_images={courseData.info_images} />
<Collapse title='備註' children={courseData.remark} />
<p className='text-5xl text-center mt-48'>
{"課程時間表"}
</p>
{
fakeData.map((item, index) => {
return (
<ScheduleCollapse key={index} title={item.yearAndMonth} rerganizedSchedule={{ yearAndMonth: item.yearAndMonth, schedules: item.schedule }} />
)
})
}
<div className="bg-[url('/images/014.png')] absolute bottom-0 inset-0 h-auto w-full bg-contain bg-bottom bg-no-repeat -z-10"></div>
</div>
)
}
export default Accordion

View File

@@ -0,0 +1,24 @@
import React from 'react'
import { CoursesProps } from '@/types'
const Banner = ({ courseData }: { courseData: CoursesProps }) => {
console.log(courseData)
return (
<div className='relative flex w-full h-[40vh] lg:h-[70vh] bg-gradient-to-r from-[#FDB2B8] to-[#FEB3BA] items-center'>
<div className='flex-col flex lg:ml-48 ml-4 z-10'>
<p className='text-black lg:text-5xl text-3xl font-bold text-center'>
{courseData?.title}
</p>
<img src={"/images/line.png"} className='lg:w-[35vh] w-[20vh] h-auto object-cover mt-2' />
</div>
{/* <div className='absolute bottom-0 right-0 max-sm:-right-32 md:-right-32 lg:right-32 bg-[url("/images/kid2.png")] bg-cover bg-center bg-no-repeat h-full w-[80%]'></div> */}
<img
src={"/images/kid2.png"}
alt="kid2"
className="absolute bottom-0 right-0 lg:right-32 object-cover object-left h-full lg:w-auto :w-40"
/>
</div>
)
}
export default Banner

View File

@@ -0,0 +1,168 @@
import React, { useState, useRef, useEffect } from 'react'
import { cn } from '../utils'
import { VscAdd, VscChromeMinimize } from "react-icons/vsc";
import { imageProps, } from '@/types';
import "slick-carousel/slick/slick.css";
import "slick-carousel/slick/slick-theme.css";
import Slider from "react-slick";
import './slick.css'
interface CollapseProps {
title: string
children?: string
className?: string
info?: boolean
info_images?: Array<imageProps>
}
const Collapse: React.FC<CollapseProps> = ({ title, children, className, info, info_images, }) => {
const [currentSlide, setCurrentSlide] = useState(0);
var settings2 = {
arrows: false,
infinite: true,
//centerPadding: "30px",
dots: true,
speed: 500,
slidesToShow: 3,
slidesToScroll: 1,
appendDots: (dots: any) => (
<div
style={{
width: '100%',
position: 'absolute',
bottom: '24px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}
>
<ul> {dots} </ul>
</div>
),
dotsClass: 'dots_custom'
};
var settings1 = {
arrows: false,
className: "center",
centerMode: true,
infinite: true,
centerPadding: "30px",
dots: true,
speed: 500,
slidesToShow: 1,
slidesToScroll: 1,
beforeChange: (current: number, next: number) => setCurrentSlide(next),
appendDots: (dots: any) => (
<div
style={{
width: '100%',
position: 'absolute',
bottom: '24px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}
>
<ul> {dots} </ul>
</div>
),
dotsClass: 'dots_custom'
};
const [isOpen, setIsOpen] = useState(false)
const [height, setHeight] = useState<number | undefined>(0)
const ref = useRef<HTMLDivElement>(null)
useEffect(() => {
if (isOpen) setHeight(ref.current?.scrollHeight)
else setHeight(0)
}, [isOpen])
return (
children && children.length > 0 ? (
<div className={cn('mt-6 rounded-3xl ', className)}>
<button
className={`flex justify-between items-center w-full p-6 text-left ${isOpen ? "rounded-t-lg bg-mainColor" : "rounded-lg bg-[#F2D6D5] "}`}
onClick={() => setIsOpen(!isOpen)}
>
<span className={`text-xl font-bold ml-3 ${isOpen ? "text-white " : " text-black"}`}>{title}</span>
{isOpen ? (
<VscChromeMinimize className='text-white mr-3' />
) : (
<VscAdd className='text-black mr-3' />
)}
</button>
<div
ref={ref}
style={{ height: height, }}
className="flex relative overflow-hidden transition-[height] duration-300 ease-in-out "
>
<div className='relative flex flex-col bg-[#FAF6F5] rounded-b-lg w-[65vw] max-sm:w-[90vw]'>
{info ? (<div className='relative justify-between items-center '>
{info_images && info_images.length > 0 && (
<div className="flex flex-col relative w-ful h-auto ">
<div className='relative justify-between items-center hidden lg:flex pt-10'>
<Slider className=" w-full h-[17vw] px-12 " {...settings2}>
{info_images.map((image, index) => (
<div key={index} className="relative w-full h-[13vw] px-1 transition-all justify-items-center ">
<img
src={`http://localhost/${image.image}`}
alt={`Course Image ${index + 1}`}
className="w-full h-full object-cover object-center "
/>
</div>
))}
</Slider>
</div>
{/* <div className='relative flex sm:block md:block lg:hidden xl:hidden 2xl:hidden'> */}
<div className='sm:block md:block lg:hidden xl:hidden 2xl:hidden pt-10'>
<Slider {...settings1}>
{info_images.map((image, index) => (
<div key={index} className="px-4">
<div key={index} className="relative h-[50vw] transition-all justify-items-center mb-16">
<img
src={`http://localhost/${image.image}`}
alt={`Course Image ${index + 1}`}
className="w-full h-full object-cover object-center "
/>
{index !== currentSlide && (
<div
className="absolute inset-0 bg-white bg-opacity-50 transition-opacity duration-300 "
style={{
objectFit: 'cover',
objectPosition: 'center'
}}
></div>
)}
</div>
</div>
))}
</Slider>
</div>
</div>
)}
</div>)
:
(<div></div>)}
<div
className='px-12 py-14 [&_*]:!text-[1.5rem] [&_*]:!text-gray-600 [&_*]:!bg-transparent'
dangerouslySetInnerHTML={{ __html: children || '' }}
/>
</div>
</div>
</div>
) : (
<div></div>
)
)
}
export default Collapse

View File

@@ -0,0 +1,20 @@
import React from 'react'
import Banner from './Banner'
import LongDesc from './LongDesc'
import CourseImagesSilder from './CourseImagesSilder'
import Accordion from './Accordion'
import { CoursesProps } from '@/types'
import Footer from '../Footer'
const Course = ({ course }: { course: CoursesProps }) => {
return (
<div className='bg-[#F6E8E8]'>
<Banner courseData={course} />
<LongDesc courseData={course} />
<CourseImagesSilder courseData={course} />
<Accordion courseData={course} />
<Footer />
</div>
)
}
export default Course

View File

@@ -0,0 +1,158 @@
"use client"
import React, { useState } from 'react'
import "slick-carousel/slick/slick.css";
import "slick-carousel/slick/slick-theme.css";
import Slider from "react-slick";
import './slick.css'
import { CoursesProps, CoursesArrayProps } from "@/types";
import { IoIosArrowForward, IoIosArrowBack } from "react-icons/io";
const SamplePrevArrow = (props: any) => {
const { className, style, onClick } = props;
return (
<div
id='prev'
onClick={onClick}
className="group flex h-12 w-12 rounded-full bg-[#F2D6D5] justify-center items-center absolute top-1/2 -translate-x-1/2 -left-8 transform -translate-y-1/2 z-10 cursor-pointer hover:bg-[#D60050]"
>
<IoIosArrowBack className='text-black mr-1 group-hover:text-white transition-colors duration-200' size={30} />
</div>
);
}
function SampleNextArrow(props: any) {
const { className, style, onClick } = props;
return (
<div
id='next'
onClick={onClick}
className="group flex h-12 w-12 rounded-full bg-[#F2D6D5] justify-center items-center absolute top-1/2 -translate-x-1/2 -right-20 transform -translate-y-1/2 z-10 cursor-pointer hover:bg-[#D60050]"
>
<IoIosArrowForward className='text-black ml-1 group-hover:text-white transition-colors duration-200' size={30} />
</div>
);
}
const CourseImagesSilder = ({ courseData }: { courseData: CoursesProps }) => {
const [currentSlide, setCurrentSlide] = useState(0);
var settings = {
arrows: false,
className: "center",
centerMode: true,
infinite: true,
centerPadding: "30px",
dots: true,
speed: 500,
slidesToShow: 1,
slidesToScroll: 1,
beforeChange: (current: number, next: number) => setCurrentSlide(next),
appendDots: (dots: any) => (
<div
style={{
width: '100%',
position: 'absolute',
bottom: '24px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}
>
<ul> {dots} </ul>
</div>
),
dotsClass: 'dots_custom'
};
var settings2 = {
fade: true,
centerMode: true,
infinite: true,
dots: true,
speed: 500,
slidesToShow: 1,
slidesToScroll: 1,
nextArrow: <SampleNextArrow to="next" />,
prevArrow: <SamplePrevArrow to="prev" />,
beforeChange: (current: number, next: number) => setCurrentSlide(next),
appendDots: (dots: any) => (
<div
style={{
width: '100%',
position: 'absolute',
bottom: '24px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}
>
<ul> {dots} </ul>
</div>
),
dotsClass: 'dots_custom'
};
return (
<div className='w-full h-auto my-20'>
<div className='relative mx-[15vw] justify-between items-center hidden lg:flex'>
{courseData.images.length > 0 && (
<div className="w-full ">
<Slider className="w-full h-[40vw]" {...settings2}>
{courseData.images.map((image, index) => (
<div key={index} className="relative w-full h-[35vw] transition-all justify-items-center mb-28 ">
<img
src={`http://localhost/${image.image}`}
alt={`Course Image ${index + 1}`}
className="w-full h-full object-cover object-center rounded-xl "
/>
</div>
))}
</Slider>
</div>
)}
</div>
<div className='relative flex justify-between items-center sm:block md:block lg:hidden xl:hidden 2xl:hidden'>
{courseData.images.length > 0 && (
<div className="w-full">
<Slider {...settings}>
{courseData.images.map((image, index) => {
return (
<div key={index} className="px-4">
<div key={index} className="relative h-[38vw] transition-all justify-items-center mb-16">
<img
src={`http://localhost/${image.image}`}
alt={`Course Image ${index + 1}`}
className="w-full h-full object-cover object-center rounded-xl"
/>
{index !== currentSlide && (
<div
className="absolute inset-0 bg-white bg-opacity-50 transition-opacity duration-300 rounded-xl"
style={{
objectFit: 'cover',
objectPosition: 'center'
}}
></div>
)}
</div>
</div>
)
})}
</Slider>
</div>
)}
</div>
</div>
)
}
export default CourseImagesSilder

View File

@@ -0,0 +1,20 @@
import React from 'react'
import { CoursesProps } from '@/types'
const LongDesc = ({ courseData }: { courseData: CoursesProps }) => {
return (
<div className='w-full h-auto lg:mt-64 mt-20'>
<div className='flex flex-col items-center justify-center mx-[5vw] lg:mx-[20vw]'>
<p className='text-5xl text-center'>
{courseData.title}
</p>
<div
className='mt-10 [&_*]:!text-[1.5rem] [&_*]:!text-gray-600 [&_*]:!text-center'
dangerouslySetInnerHTML={{ __html: courseData.long_description }}
/>
</div>
</div >
)
}
export default LongDesc

View File

@@ -0,0 +1,75 @@
import React, { useState, useRef, useEffect } from 'react'
import { cn } from '../utils'
import { VscAdd, VscChromeMinimize } from "react-icons/vsc";
import { ScheduleProps } from '@/types';
import moment from 'moment';
interface CollapseProps {
title: string
rerganizedSchedule?: {
yearAndMonth: string
schedules: ScheduleProps[]
}
}
const ScheduleCollapse: React.FC<CollapseProps> = ({ title, rerganizedSchedule }) => {
const [isOpen, setIsOpen] = useState(false)
const [height, setHeight] = useState<number | undefined>(0)
const ref = useRef<HTMLDivElement>(null)
useEffect(() => {
if (isOpen) setHeight(ref.current?.scrollHeight)
else setHeight(0)
}, [isOpen])
return (
<div className={cn('mt-6 rounded-3xl ')}>
<button
className={`flex justify-between items-center w-full p-6 text-left ${isOpen ? "rounded-t-lg bg-[#D60050]" : "rounded-lg bg-[#F2D6D5] "}`}
onClick={() => setIsOpen(!isOpen)}
>
<span className={`text-xl font-bold ml-3 ${isOpen ? "text-white " : " text-black"}`}>{title}</span>
{isOpen ? (
<VscChromeMinimize className='text-white mr-3' />
) : (
<VscAdd className='text-black mr-3' />
)}
</button>
<div
ref={ref}
style={{ height: height, }}
className="flex relative overflow-hidden transition-[height] duration-300 ease-in-out "
>
<div className='relative flex flex-col bg-[#FAF6F5] rounded-b-lg w-[65vw] max-sm:w-[90vw]'>
{rerganizedSchedule ? (
<div className='px-12 py-14'>
{rerganizedSchedule.schedules.map((schedule, index) => (
<div key={index} className='mb-4'>
<h3 className='text-xl font-bold'>
{ moment.utc(schedule.date).format("MM月DD日")}
</h3>
<p className='text-gray-600'>{schedule.title}</p>
<p className='text-gray-600'>{schedule.info1}</p>
<p className='text-gray-600'>{schedule.info2}</p>
</div>
))}
</div>
) : (
<div
/>
)}
</div>
</div>
</div>
)
}
export default ScheduleCollapse

View File

@@ -0,0 +1,30 @@
.dots_custom {
display: inline-block;
vertical-align: middle;
margin: auto 0;
padding: 0;
}
.dots_custom li {
list-style: none;
cursor: pointer;
display: inline-block;
margin: 0 6px;
padding: 0;
}
.dots_custom li button {
border: none;
background: #d1d1d1;
color: transparent;
cursor: pointer;
display: block;
height: 8px;
width: 8px;
border-radius: 100%;
padding: 0;
}
.dots_custom li.slick-active button {
background-color: #D60050;
}