first commit
This commit is contained in:
		
							
								
								
									
										112
									
								
								components/ContactForm.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										112
									
								
								components/ContactForm.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,112 @@ | ||||
| "use client" | ||||
| import React from 'react' | ||||
| import CustomButton from './CustomButton' | ||||
| import { useForm, SubmitHandler } from "react-hook-form" | ||||
| import { postMessage } from '@/utils' | ||||
| import { MessageProps } from '@/types' | ||||
| import { toast } from 'react-hot-toast'; | ||||
|  | ||||
| type Props = { | ||||
|     startLoading: () => void | ||||
|     stopLoading: () => void | ||||
| } | ||||
| const ContactForm = ({ startLoading, stopLoading }: Props) => { | ||||
|  | ||||
|  | ||||
|  | ||||
|     const { | ||||
|         register, | ||||
|         handleSubmit, | ||||
|         watch, | ||||
|         reset, | ||||
|         formState: { errors }, | ||||
|     } = useForm<MessageProps>() | ||||
|     const onSubmit: SubmitHandler<MessageProps> = async (data) => { | ||||
|         try { | ||||
|             startLoading; // Set loading state to true | ||||
|             const result = await postMessage(data); | ||||
|             if (result.success) { | ||||
|                 toast.success('已收到您的訊息,我們會盡快回覆您!'); | ||||
|               reset(); // Reset form fields | ||||
|                 // Reset form or perform any other actions on success | ||||
|             } else { | ||||
|                 toast.error('Failed to send message. Please try again.'); | ||||
|             } | ||||
|         } catch (error) { | ||||
|             console.error('Error sending message:', error); | ||||
|             toast.error('An error occurred. Please try again later.'); | ||||
|         } finally { | ||||
|             stopLoading; // Set loading state back to false | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|     return ( | ||||
|  | ||||
|         <div className="w-full flex flex-col h-auto items-center justify-center relative  " > | ||||
|             <div className="text-4xl sm:text-3xl lg:text-5xl text-center mt-[10vh]"> | ||||
|                 讓我們知道您的想法 | ||||
|             </div> | ||||
|             <div className="bg-[url('/images/014.png')] absolute bottom--10 inset-0 h-auto w-full bg-contain bg-bottom bg-no-repeat hidden lg:block"></div> | ||||
|  | ||||
|             <div className="font-[sans-serif] w-full md:h-[62vh] max-w-5xl relative bg-transparent lg:bg-white shadow-none lg:shadow-[0_2px_10px_-3px_rgba(6,81,237,0.3)] rounded-3xl overflow-hidden lg:mt-14 lg:mb-56 sm:my-15 max-md:my-0 sm:mx-20"> | ||||
|  | ||||
|  | ||||
|                 <div className="grid lg:grid-cols-10 "> | ||||
|  | ||||
|                     <form onSubmit={handleSubmit(onSubmit)} className="flex rounded-tl-3xl rounded-bl-3xl h-[62vh] lg:col-span-6 items-center justify-center"> | ||||
|  | ||||
|                         <div className="space-y-3 w-full mx-11"> | ||||
|                             <div className="grid md:grid-cols-2 space-y-3"> | ||||
|                                 <div className=" md:mr-3 mt-3"> | ||||
|                                     <p className='text-sm'>姓名</p> | ||||
|                                     <input type='text' | ||||
|                                         {...register("name", { required: true })} | ||||
|                                         className="w-full mt-3  max-lg:bg-gray-100 md:bg-gray-100 max-sm:bg-[#F6E2E3] rounded-md py-3 px-4 text-sm outline-[#EA004F] md:focus-within:bg-transparent border border-gray-200" /> | ||||
|                                 </div> | ||||
|                                 <div > | ||||
|                                     <p className='text-sm'>電話</p> | ||||
|                                     <input type='phone' | ||||
|                                         {...register("phone", { required: true })} | ||||
|                                         className="w-full mt-3 max-lg:bg-gray-100 md:bg-gray-100 max-sm:bg-[#F6E2E3] rounded-md py-3 px-4 text-sm outline-[#EA004F] md:focus-within:bg-transparent border border-gray-200" /> | ||||
|                                 </div> | ||||
|                             </div> | ||||
|  | ||||
|                             <div className="mt-5"> | ||||
|                                 <p className='text-sm'>電郵</p> | ||||
|                                 <input type='email' | ||||
|                                     {...register("email", { required: true })} | ||||
|                                     className="w-full mt-3 max-lg:bg-gray-100 md:bg-gray-100 max-sm:bg-[#F6E2E3] rounded-md py-3 px-4 text-sm outline-[#EA004F] md:focus-within:bg-transparent border border-gray-200" /> | ||||
|                             </div> | ||||
|                             <div className="mt-5"> | ||||
|                                 <p className='text-sm'>訊息</p> | ||||
|                                 <textarea | ||||
|                                     {...register("message", { required: true })} | ||||
|                                     className="w-full mt-3 h-36 max-lg:bg-gray-100 md:bg-gray-100 max-sm:bg-[#F6E2E3] rounded-md px-4 text-sm pt-3 outline-[#EA004F] md:focus-within:bg-transparent border border-gray-200"></textarea> | ||||
|                             </div> | ||||
|  | ||||
|                             <button | ||||
|                                 type="submit" | ||||
|                                 className={`middle none center rounded-full md:px-8 md:py-3 text-sm  max-sm:w-full py-3 px-8  bg-mainColor font-bold uppercase text-white shadow-md shadow-pink-500/20 transition-all hover:shadow-lg hover:shadow-pink-500/40`} | ||||
|                                 data-ripple-light="true" | ||||
|  | ||||
|                             > | ||||
|                                 {'發送'} | ||||
|                             </button> | ||||
|                         </div> | ||||
|                     </form> | ||||
|                     <div className="text-center  bg-[url('/images/contact.png')] bg-cover h-auto w-full lg:col-span-4 sm:hidden md:hidden lg:block"> | ||||
|                         {/* <img src="/images/contact.png"   /> */} | ||||
|                     </div> | ||||
|                 </div> | ||||
|             </div> | ||||
|  | ||||
|  | ||||
|         </div> | ||||
|     ) | ||||
| } | ||||
|  | ||||
| export default ContactForm | ||||
							
								
								
									
										124
									
								
								components/Course/Accordion.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										124
									
								
								components/Course/Accordion.tsx
									
									
									
									
									
										Normal 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 | ||||
							
								
								
									
										24
									
								
								components/Course/Banner.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								components/Course/Banner.tsx
									
									
									
									
									
										Normal 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 | ||||
							
								
								
									
										168
									
								
								components/Course/Collapse.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										168
									
								
								components/Course/Collapse.tsx
									
									
									
									
									
										Normal 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 | ||||
							
								
								
									
										20
									
								
								components/Course/Course.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								components/Course/Course.tsx
									
									
									
									
									
										Normal 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 | ||||
							
								
								
									
										158
									
								
								components/Course/CourseImagesSilder.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										158
									
								
								components/Course/CourseImagesSilder.tsx
									
									
									
									
									
										Normal 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 | ||||
							
								
								
									
										20
									
								
								components/Course/LongDesc.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								components/Course/LongDesc.tsx
									
									
									
									
									
										Normal 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 | ||||
							
								
								
									
										75
									
								
								components/Course/ScheduleCollapse.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								components/Course/ScheduleCollapse.tsx
									
									
									
									
									
										Normal 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 | ||||
							
								
								
									
										30
									
								
								components/Course/slick.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								components/Course/slick.css
									
									
									
									
									
										Normal 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; | ||||
|   } | ||||
							
								
								
									
										112
									
								
								components/CoursesSilder.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										112
									
								
								components/CoursesSilder.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,112 @@ | ||||
| "use client"; | ||||
| import CustomButton from "./CustomButton"; | ||||
| import { useRef, useEffect, useState } from 'react'; | ||||
| import Image from 'next/image'; | ||||
| import { CoursesProps, CoursesArrayProps } from "@/types"; | ||||
| import CustomButtom from "./CustomButton"; | ||||
| import "slick-carousel/slick/slick.css"; | ||||
| import "slick-carousel/slick/slick-theme.css"; | ||||
| import Slider from "react-slick"; | ||||
| import './slick.css' | ||||
| import { GoDot } from "react-icons/go"; | ||||
| //npm i --save-dev @types/react-slick | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| const CoursesSilder = ({ courses }: { courses: CoursesProps[] }) => { | ||||
|   var settings = { | ||||
|     dots: true, | ||||
|     infinite: true, | ||||
|     speed: 500, | ||||
|     slidesToShow: 1, | ||||
|     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' | ||||
|   }; | ||||
|   console.log(courses) | ||||
|   return ( | ||||
|     <div> | ||||
|       {courses.length > 0 ? ( | ||||
|         <div className=" bg-[#F6E8E9] items-center h-[100vh] w-full pt-30 transition-all duration-200"> | ||||
|           <Slider className="w-full" {...settings}> | ||||
|             {courses.map((course, index) => { | ||||
|               return ( | ||||
|                 <div key={index} className=" bg-[#F6E8E9] h-auto pt-30 transition-all justify-items-center duration-20"> | ||||
|                   <div className="hidden lg:grid lg:grid-cols-2 w-full mb-28"> | ||||
|                     <div className="flex col-span-1 h-auto justify-self-end mr-20"> | ||||
|                       <img | ||||
|                         src={`http://localhost/${course.images[0].image}`} | ||||
|                         alt="Course Image" | ||||
|                         className="w-[50vh] h-[70vh] object-cover object-center rounded-3xl shadow-md" | ||||
|                         style={{ aspectRatio: '1 / 1' }} | ||||
|                       /> | ||||
|                     </div> | ||||
|                     <div className="flex col-span-1  h-auto justify-start items-center ml-20"> | ||||
|                       <div className="flex flex-col items-start w-[50vh]"> | ||||
|                         <p className="text-4xl"> | ||||
|                           {course.title} | ||||
|                         </p> | ||||
|                         <p className="text-base line-clamp-3 overflow-hidden mt-8"> | ||||
|                           {course.sort_description} | ||||
|                         </p> | ||||
|                         <div> | ||||
|                           <button | ||||
|                             className={`mt-16 middle none center rounded-full bg-[#D60050] h-12 w-28 text-base text-white shadow-md shadow-pink-500/20 transition-all hover:shadow-lg hover:shadow-pink-500/40`} | ||||
|                             data-ripple-light="true" | ||||
|                           > | ||||
|                             {"了解更多"} | ||||
|                           </button> | ||||
|                         </div> | ||||
|                       </div> | ||||
|                     </div> | ||||
|                   </div> | ||||
|  | ||||
|                   <div className=" relative w-[50vh] h-[70vh] max-sm:w-[36vh] max-sm:h-[60vh]  mt-20 mb-28 overflow-hidden rounded-3xl shadow-md sm:block md:block lg:hidden xl:hidden 2xl:hidden mx-auto"> | ||||
|                     <img | ||||
|                       src={`http://localhost/${course.images[0].image}`} | ||||
|                       alt="Course Image" | ||||
|                       className="absolute inset-0 w-full h-full object-cover object-center " | ||||
|                     /> | ||||
|                     <div className="absolute inset-0 flex flex-col items-center justify-center bg-black bg-opacity-50 text-white p-6"> | ||||
|                       <p className="text-2xl text-center mb-4"> | ||||
|                         {course.title} | ||||
|                       </p> | ||||
|                       <p className="text-base line-clamp-3 overflow-hidden text-center mb-8"> | ||||
|                         {course.sort_description} | ||||
|                       </p> | ||||
|                       <button | ||||
|                         className="rounded-full bg-white h-12 w-28 text-sm text-black shadow-md shadow-gray-400/20 transition-all hover:shadow-lg hover:shadow-gray-500/40" | ||||
|                         data-ripple-light="true" | ||||
|                       > | ||||
|                         {"了解更多"} | ||||
|                       </button> | ||||
|                     </div> | ||||
|                   </div> | ||||
|                 </div> | ||||
|               ) | ||||
|             })} | ||||
|           </Slider> | ||||
|         </div> | ||||
|       ) : ( | ||||
|         <div className="w-full h-10" /> | ||||
|       )} | ||||
|     </div> | ||||
|   ) | ||||
| } | ||||
|  | ||||
| export default CoursesSilder; | ||||
|  | ||||
							
								
								
									
										17
									
								
								components/CustomButton.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								components/CustomButton.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | ||||
| "use client" | ||||
| import React from 'react' | ||||
| import { CustomButtonProps } from '@/types' | ||||
| const CustomButton = ({ isDisabled, title, handleClick, text_and_button_size }: CustomButtonProps) => { | ||||
|     return ( | ||||
|         <button | ||||
|             className={`middle none center rounded-full bg-[#D60050] ${text_and_button_size} font-bold uppercase text-white shadow-md shadow-pink-500/20 transition-all hover:shadow-lg hover:shadow-pink-500/40`} | ||||
|             data-ripple-light="true" | ||||
|             disabled={isDisabled} | ||||
|             onClick={handleClick} | ||||
|         > | ||||
|             {title} | ||||
|         </button> | ||||
|     ) | ||||
| } | ||||
|  | ||||
| export default CustomButton | ||||
							
								
								
									
										43
									
								
								components/Footer.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								components/Footer.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,43 @@ | ||||
| "use client" | ||||
| import Link from 'next/link' | ||||
| import { FaFacebookF, FaYoutube } from "react-icons/fa"; | ||||
| import { RiInstagramFill } from "react-icons/ri"; | ||||
| import { SettingsProps } from '@/types'; | ||||
| import { IoMenu } from "react-icons/io5"; | ||||
| const Footer = ({ settings }: { settings: SettingsProps }) => { | ||||
|     return ( | ||||
|         <footer className="rounded-lg  py-8"> | ||||
|             <div className="container mx-auto px-4"> | ||||
|                 <div className="flex flex-col sm:flex-row items-center justify-between"> | ||||
|                     <Link className="mb-4 sm:mb-0 flex justify-center sm:justify-start" | ||||
|                         href="/"> | ||||
|                         <img src="/images/logo.png" width={170} height={170} alt="logo" /> | ||||
|                     </Link> | ||||
|                     <div className="text-center mb-4 sm:mb-0"> | ||||
|                         <span className="text-xs text-black"> | ||||
|                             Copyright @ 2024 <a href="https://flowbite.com/" className="hover:underline">All In One</a> All Rights Reserved. | ||||
|                         </span> | ||||
|                     </div> | ||||
|                     <div className="flex flex-row items-center space-x-4 mt-7 lg:mt-0 md:mt-0"> | ||||
|                         <div className="bg-[#DCCECF] rounded-full h-8 w-8 flex items-center justify-center hover:bg-mainColor" | ||||
|                             onClick={() => window.open(settings.facebook)}> | ||||
|                             <FaFacebookF className="w-4 h-4 cursor-pointer text-[#F6E5E9]" /> | ||||
|                         </div> | ||||
|                         <div className="bg-[#DCCECF] rounded-full h-8 w-8 flex items-center justify-center hover:bg-mainColor" | ||||
|                             onClick={() => window.open(settings.instagram)}> | ||||
|                             <RiInstagramFill className="w-4 h-4 cursor-pointer text-[#F6E5E9]" /> | ||||
|                         </div> | ||||
|                         <div className="bg-[#DCCECF] rounded-full h-8 w-8 flex items-center justify-center hover:bg-mainColor" | ||||
|                             onClick={() => window.open(settings.youtube)}> | ||||
|                             <FaYoutube className="w-4 h-4 cursor-pointer text-[#F6E5E9]" /> | ||||
|                         </div> | ||||
|                     </div> | ||||
|                 </div> | ||||
|             </div> | ||||
|         </footer> | ||||
|  | ||||
|  | ||||
|     ) | ||||
| } | ||||
|  | ||||
| export default Footer | ||||
							
								
								
									
										124
									
								
								components/Hero1.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										124
									
								
								components/Hero1.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,124 @@ | ||||
| "use client"; | ||||
|  | ||||
| import "animate.css/animate.compat.css" | ||||
| import ScrollAnimation from 'react-animate-on-scroll'; | ||||
| import { useRef, useEffect, useState } from 'react'; | ||||
| import TextGradientScroll from './TextGradientScrollContext'; | ||||
| import Image from 'next/image'; | ||||
| import CustomButton from "./CustomButton"; | ||||
|  | ||||
| const Hero1 = () => { | ||||
|     const [paragraphOpacities, setParagraphOpacities] = useState<number[]>([]); | ||||
|     const scrollRef = useRef<HTMLDivElement | null>(null); | ||||
|  | ||||
|     useEffect(() => { | ||||
|         const handleScroll = () => { | ||||
|             if (scrollRef.current) { | ||||
|                 const container = scrollRef.current; | ||||
|                 const isAtBottom = Math.abs(container.scrollHeight - container.scrollTop - container.clientHeight) < 5; | ||||
|  | ||||
|  | ||||
|                 if (isAtBottom) { | ||||
|                     // Set all paragraphs to full opacity when at the bottom | ||||
|                     setParagraphOpacities(new Array(11).fill(1)); | ||||
|                 } else { | ||||
|                     const paragraphs = container.querySelectorAll('p'); | ||||
|                     const newOpacities = Array.from(paragraphs).map(p => { | ||||
|                         const rect = p.getBoundingClientRect(); | ||||
|                         const yPosition = rect.top - container.getBoundingClientRect().top; | ||||
|                         return yPosition > 30 ? 0 : 1; | ||||
|                     }); | ||||
|                     setParagraphOpacities(newOpacities); | ||||
|                 } | ||||
|             } | ||||
|         }; | ||||
|         setParagraphOpacities(new Array(9).fill(0)); | ||||
|         if (scrollRef.current) { | ||||
|             scrollRef.current.addEventListener('scroll', handleScroll); | ||||
|             setTimeout(handleScroll, 100); | ||||
|  | ||||
|         } | ||||
|  | ||||
|         return () => { | ||||
|             if (scrollRef.current) { | ||||
|                 scrollRef.current.removeEventListener('scroll', handleScroll); | ||||
|             } | ||||
|         }; | ||||
|     }, []); | ||||
|  | ||||
|  | ||||
|     return ( | ||||
|         <div className=" relative flex flex-col items-center bg-gradient-to-b from-[#F4D7D7] to-[#e2b2c4]  max-sm:h-[94vh] max-lg:h-[170] md:h-[170vh] z-[10] w-full transition-all duration-200  " > | ||||
|             {/* Existing content */} | ||||
|  | ||||
|             <div > | ||||
|                 <div className="flex flex-row items-center justify-center mt-[30vh]"> | ||||
|                     <div className='max-lg:text-6xl max-sm:text-3xl md:text-6xl'> | ||||
|                         發掘你的 | ||||
|                     </div> | ||||
|                     <div className=" bg-[url('/images/kid.png')]  max-lg:h-[60px]  max-lg:w-[140px] max-sm:h-[40px] max-sm:w-[95px] md:h-[60px] md:w-[140px] bg-cover  bg-no-repeat rounded-full " /> | ||||
|                     <div className='max-lg:text-6xl max-sm:text-3xl md:text-6xl'> | ||||
|                         音樂之路 | ||||
|                     </div> | ||||
|                 </div> | ||||
|             </div> | ||||
|             <div className="max-lg: w-[61vh] h-[15vh] max-sm:w-[40vh]  mt-[5vh] items-center"> | ||||
|                 <div className="flex-grow  h-[10vh]  overflow-y-auto items-center no-scrollbar" snap-mandatory snap-y ref={scrollRef}> | ||||
|                     {[ | ||||
|                         "學音樂能陶冶性情,專注,舒緩壓力,放鬆心情。實際上是否做到呢?", | ||||
|                         "我們認為音樂不單單是「睇五線譜」", | ||||
|                         "我們可以透過不同方式", | ||||
|                         "例如聆聽,唱歌,身體律動等學習音樂,令學習不再是沉悶艱難", | ||||
|                         "真正做到「陶冶性情」", | ||||
|                         "One & ALL Music 提供全面的音樂課程", | ||||
|                         "我們相信每位學生都各有天賦", | ||||
|                         "「誰都可發光 只要找對地方」", | ||||
|                         "只要運用合適的教學法,同學必定能發揮所長", | ||||
|                         " ", | ||||
|                         " ", | ||||
|                     ].map((text, index) => ( | ||||
|                         <p | ||||
|                             key={index} | ||||
|                             className={`text-black max-lg:text-2xl max-sm:text-lg md:text-2xl text-center ${paragraphOpacities[index] === 0 ? 'opacity-25' : ''}`} | ||||
|                         > | ||||
|                             {text} | ||||
|                         </p> | ||||
|                     ))} | ||||
|                 </div> | ||||
|             </div> | ||||
|  | ||||
|             <div className="mt-[7vh] max-sm:hidden max-lg:block md:block"> | ||||
|                 <div className="flex flex-row items-center justify-center mt-[30vh] "> | ||||
|                     <div className='max-lg:text-6xl max-sm:text-4xl md:text-6xl'> | ||||
|                         我們認為 | ||||
|                     </div> | ||||
|                     <div className=" bg-[url('/images/piano2.png')]  max-lg:h-[60px]  max-lg:w-[140px] max-sm:h-[40px] max-sm:w-[95px] md:h-[60px] md:w-[140px] bg-cover  bg-no-repeat rounded-full " /> | ||||
|                 </div> | ||||
|             </div> | ||||
|             <div className="w-[61vh] h-[19vh] mt-[5vh] items-center max-sm:hidden max-lg:block md:block"> | ||||
|                 <p | ||||
|                     className={`text-black text-2xl text-center `} | ||||
|                 > | ||||
|                     {"音樂不單單是「睇五線譜」,我們可以透過不同方式,例如聆聽唱歌,身體律動等學習音樂,令學習不再是沉悶艱難,真正做到「陶治性情」。"} | ||||
|                 </p> | ||||
|                 <p | ||||
|                     className={`text-black text-2xl text-center mt-[5vh]`} | ||||
|                 > | ||||
|                     One & ALL Music 提供全面的音樂課程,我們相信每位學生都各有天赋,<br></br>「誰都可發光 只要找對地方」,只要運用合適的教學法,同學必定能發揮所長。 | ||||
|                 </p> | ||||
|             </div> | ||||
|             <div className="max-lg:mt-[20vh] md:mt-[20vh] max-sm:mt-[5vh]"> | ||||
|                 <CustomButton text_and_button_size={" max-lg:px-14 max-lg:py-5 max-lg:text-md max-sm:px-8 max-sm:py-3 max-sm:text-sm md:px-14 md:py-5 md:text-md"} title={'關於我們更多'} handleClick={() => { | ||||
|                     console.log('Button clicked'); | ||||
|                 }} /> | ||||
|             </div> | ||||
|  | ||||
|             <div className="absolute bottom-0  bg-[url('/images/pianobackground2.png')]  max-lg:h-[30vh] max-sm:h-[15vh] md:h-[30vh] w-full bg-cover bg-center bg-no-repeat" style={{ opacity: 0.7 }}> | ||||
|             </div> | ||||
|  | ||||
|  | ||||
|  | ||||
|         </div > | ||||
|     ); | ||||
| } | ||||
| export default Hero1 | ||||
							
								
								
									
										33
									
								
								components/Hero2.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								components/Hero2.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,33 @@ | ||||
| "use client"; | ||||
| import CustomButton from "./CustomButton"; | ||||
| import { useRef, useEffect, useState } from 'react'; | ||||
| import Image from 'next/image'; | ||||
| import { CoursesProps, SettingsProps } from "@/types"; | ||||
|  | ||||
|  | ||||
| function convertToEmbedLink(watchLink: string): string { | ||||
|     const videoIdMatch = watchLink.match(/(?:v=|\/)([a-zA-Z0-9_-]{11})/); | ||||
|     if (videoIdMatch && videoIdMatch[1]) { | ||||
|         const videoId = videoIdMatch[1]; | ||||
|         return `https://www.youtube.com/embed/${videoId}`; | ||||
|     } | ||||
|     return watchLink; // Return original link if conversion fails | ||||
| } | ||||
|  | ||||
| const Hero2 = ({ settings }: { settings: SettingsProps }) => { | ||||
|     return ( | ||||
|         <div className="relative flex flex-col items-center bg-[#F6E8E9] h-auto z-[10] w-full pt-30 transition-all duration-200"> | ||||
|             <div className="flex w-full h-auto justify-center items-center relative"> | ||||
|                 <div className="bg-[url('/images/014.png')] absolute bottom-0 inset-0 h-auto w-full bg-contain bg-bottom bg-no-repeat"></div> | ||||
|                 <iframe | ||||
|                     className="rounded-2xl w-[100vh] aspect-video justify-center relative z-10 lg:m-[15vh] max-sm:m-[5vh] md:m-[10vh]" | ||||
|                     src={convertToEmbedLink(settings.youtube_link)} | ||||
|                     allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" | ||||
|                     allowFullScreen | ||||
|                 ></iframe> | ||||
|             </div> | ||||
|         </div> | ||||
|     ) | ||||
| } | ||||
|  | ||||
| export default Hero2 | ||||
							
								
								
									
										53
									
								
								components/Home/Home.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								components/Home/Home.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,53 @@ | ||||
| "use client"; | ||||
|  | ||||
| import { useState, useEffect, lazy, Suspense } from "react"; | ||||
| import { Audio } from "react-loader-spinner"; | ||||
| const Hero1 = lazy(() => import('../Hero1')); | ||||
| import Hero2 from '../Hero2' | ||||
| import CoursesSilder from "../CoursesSilder"; | ||||
| import ContactForm from '../ContactForm' | ||||
| import Loading from '../Loading' | ||||
| import Footer from '../Footer' | ||||
| import { fetchCourses } from '../../utils/index' | ||||
| import { CoursesArrayProps, CoursesProps, SettingsProps } from "@/types"; | ||||
| import Whatsapp from "../Whatsapp"; | ||||
| const Home = ({ courses, settings }: { courses: CoursesProps[], settings: SettingsProps }) => { | ||||
|   const [loading, setLoading] = useState(true); | ||||
|   // const [courses, setCourses] = useState<CoursesProps[]>([]); | ||||
|  | ||||
|   useEffect(() => { | ||||
|     const loadCourses = async () => { | ||||
|       setLoading(true); | ||||
|       await new Promise(resolve => setTimeout(resolve, 300)); // 0.5 second delay | ||||
|       setLoading(false); | ||||
|     }; loadCourses(); | ||||
|   }, []); | ||||
|  | ||||
|   const startLoading = () => setLoading(true); | ||||
|   const stopLoading = () => setLoading(false); | ||||
|  | ||||
|   console.log(courses); | ||||
|   return ( | ||||
|     <div className='bg-[#F2D5D5]'> | ||||
|       {loading ? ( | ||||
|         <div className="flex justify-center items-center h-screen"> | ||||
|           <Suspense fallback={<div>Loading...</div>}> | ||||
|             <Loading /> | ||||
|           </Suspense> | ||||
|         </div> | ||||
|       ) : ( | ||||
|         <div> | ||||
|           <Hero1 /> | ||||
|           <Hero2 settings={settings} /> | ||||
|           <Suspense fallback={<div>Loading...</div>}> | ||||
|             <CoursesSilder courses={courses} /> | ||||
|           </Suspense> | ||||
|           <ContactForm startLoading={startLoading} stopLoading={stopLoading} /> | ||||
|           <Whatsapp settings={settings} /> | ||||
|           <Footer settings={settings} /> | ||||
|         </div> | ||||
|       )} | ||||
|     </div> | ||||
|   ) | ||||
| } | ||||
| export default Home | ||||
							
								
								
									
										17
									
								
								components/Loading.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								components/Loading.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | ||||
| import React from 'react' | ||||
|  | ||||
| const Loading = () => { | ||||
|     return ( | ||||
|         <div className="flex items-center justify-center min-h-screen p-5 min-w-screen"> | ||||
|  | ||||
|             <div className="flex space-x-2 animate-pulse"> | ||||
|                 <div className="w-3 h-3 bg-[#D60050] rounded-full"></div> | ||||
|                 <div className="w-3 h-3 bg-[#D60050] rounded-full"></div> | ||||
|                 <div className="w-3 h-3 bg-[#D60050] rounded-full"></div> | ||||
|             </div> | ||||
|  | ||||
|         </div> | ||||
|     ) | ||||
| } | ||||
|  | ||||
| export default Loading | ||||
							
								
								
									
										85
									
								
								components/Navbar/MobileNav.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										85
									
								
								components/Navbar/MobileNav.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,85 @@ | ||||
| "use client" | ||||
| import React, { useEffect, useState } from 'react' | ||||
|  | ||||
| import Link from 'next/link' | ||||
| import { FiChevronDown, FiChevronUp } from 'react-icons/fi'; | ||||
| import { CoursesProps } from '@/types' | ||||
| //define props type | ||||
| type Props = { | ||||
|     showNav: boolean; | ||||
|     closeNav: () => void | ||||
|     courses: CoursesProps[] | ||||
| } | ||||
|  | ||||
| const MobileNav = ({ closeNav, showNav, courses }: Props) => { | ||||
|     const [dropdownOpen, setDropdownOpen] = useState(false); | ||||
|     useEffect(() => { | ||||
|         if (showNav) { | ||||
|             document.body.classList.add('overflow-hidden'); | ||||
|         } else { | ||||
|             document.body.classList.remove('overflow-hidden'); | ||||
|         } | ||||
|  | ||||
|         return () => { | ||||
|             document.body.classList.remove('overflow-hidden'); | ||||
|         }; | ||||
|     }, [showNav]); | ||||
|  | ||||
|     const navOpen = showNav ? 'translate-x-0' : 'translate-x-[-100%]' | ||||
|     const navClose = !showNav ? 'translate-x-[-100%]' : 'translate-x-0' | ||||
|  | ||||
|     return ( | ||||
|         <div> | ||||
|             {/*overlay*/} | ||||
|             <div className={`fixed ${navOpen} ${navClose} transform transition-all duration-500 top-[7vh] inset-x-0 bottom-0 z-[1000]`}> | ||||
|  | ||||
|                 {/* nav links*/} | ||||
|                 <div className={`text-white ${navOpen} fixed flex flex-col h-full w-[80%] sm:w-[60%] bg-[#F9E7E9] z-[10000] `}> | ||||
|  | ||||
|                     <div className='w-full h-16 flex justify-start items-center border-b-[1.5px] border-[#F5DADF]'> | ||||
|                         <Link key={"01"} href={"#"} > | ||||
|                             <p className="text-xl ml-6 text-black"> | ||||
|                                 主頁 | ||||
|                             </p> | ||||
|                         </Link> | ||||
|                     </div> | ||||
|                     <div className={`w-full flex flex-col ${dropdownOpen ? "border-b-[0px]" : "border-b-[1.5px]"}  border-[#F5DADF]`}> | ||||
|                         <div | ||||
|                             className='h-16 flex justify-between items-center px-6 cursor-pointer' | ||||
|                             onClick={() => setDropdownOpen(!dropdownOpen)} | ||||
|                         > | ||||
|                             <p className="text-xl text-black">課程</p> | ||||
|                             {dropdownOpen ? <FiChevronUp className='text-black' size={30}/> : <FiChevronDown className='text-black' size={30}/>} | ||||
|                         </div> | ||||
|                         {dropdownOpen && ( | ||||
|                             <div> | ||||
|                                 {courses?.map((course) => ( | ||||
|                                     <div className='bg-[#F5DADF] w-full h-16 flex justify-start items-center border-b-[1.5px] border-[#F9E7E9]'> | ||||
|                                         <Link key={course.id} href={`/courses/${course.id}`}> | ||||
|                                             <p className="text-lg text-black ml-8 "> | ||||
|                                                 {course.title} | ||||
|                                             </p> | ||||
|                                         </Link> | ||||
|                                     </div> | ||||
|                                 ))} | ||||
|                             </div> | ||||
|                         )} | ||||
|                     </div> | ||||
|  | ||||
|                     <div className='w-full h-16 flex justify-start items-center border-b-[1.5px] border-[#F5DADF]'> | ||||
|                         <Link key={"03"} href={"#"} > | ||||
|                             <p className="text-xl ml-6 text-black"> | ||||
|                                 關於我們 | ||||
|                             </p> | ||||
|                         </Link> | ||||
|                     </div> | ||||
|  | ||||
|  | ||||
|                 </div> | ||||
|             </div> | ||||
|         </div> | ||||
|     ) | ||||
| } | ||||
|  | ||||
| export default MobileNav | ||||
|  | ||||
							
								
								
									
										134
									
								
								components/Navbar/Nav.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										134
									
								
								components/Navbar/Nav.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,134 @@ | ||||
| "use client" | ||||
| import React, { useState, useEffect } from 'react' | ||||
| import Image from 'next/image' | ||||
| import Link from 'next/link' | ||||
| import { FiChevronDown } from "react-icons/fi"; | ||||
| import { HiBars3BottomRight } from 'react-icons/hi2' | ||||
| import { FaFacebookF, FaYoutube } from "react-icons/fa"; | ||||
| import { RiInstagramFill } from "react-icons/ri"; | ||||
| import { IoMenu, IoClose } from "react-icons/io5"; | ||||
| import { CoursesProps, SettingsProps } from '@/types' | ||||
| import { colors } from '@/public/themes' | ||||
| //define props type | ||||
| type Props = { | ||||
|     openNav: () => void | ||||
|     showNav: boolean | ||||
|     courses: CoursesProps[] | ||||
|     settings: SettingsProps | ||||
| } | ||||
| const Nav = ({ openNav, courses, showNav, settings }: Props) => { | ||||
|     const [navBg, setNavBg] = useState(false) | ||||
|     const [dropdownOpen, setDropdownOpen] = useState(false); | ||||
|  | ||||
|     useEffect(() => { | ||||
|         const handler = () => { | ||||
|             if (window.scrollY >= 90) { | ||||
|                 setNavBg(true) | ||||
|             } else if (window.scrollY <= 90) { | ||||
|                 setNavBg(false) | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         window.addEventListener("scroll", handler) | ||||
|         return () => { | ||||
|             window.removeEventListener("scroll", handler) | ||||
|         } | ||||
|     }, []); | ||||
|  | ||||
|     return ( | ||||
|         <div className={`bg-[#F6E5E9] max-sm:bg-[#FFF9F9] h-[7vh] z-[10] w-full transition-all duration-200 `}> | ||||
|             <div className="flex items-center justify-between w-[80%] sm:w-[80%] x1:w-[80%] mx-auto"> | ||||
|                 <div className='flex items-center space-x-10'> | ||||
|                     <Link href={"/"}> | ||||
|                         <Image | ||||
|  | ||||
|                             src="/images/logo.png" | ||||
|                             alt='logo' | ||||
|                             width={170} | ||||
|                             height={170} | ||||
|                             className="m1-[-1.5rem] sm:m1-0 mr-4" | ||||
|                         /> | ||||
|                     </Link> | ||||
|                     <div className="flex items-center space-x-15"> | ||||
|                         <div className='hidden lg:flex items-center space-x-8'> | ||||
|                             <Link key={"01"} href={"/"} > | ||||
|                                 <p className="nav__link"> | ||||
|                                     {"主頁"} | ||||
|                                 </p> | ||||
|                             </Link> | ||||
|                             <div key={"02"} className="relative"> | ||||
|                                 <button | ||||
|                                     className="nav__link flex items-center" | ||||
|                                     onClick={() => setDropdownOpen(!dropdownOpen)} | ||||
|                                 > | ||||
|                                     {"課程"} | ||||
|                                     <FiChevronDown className='text-gray-400' /> | ||||
|                                 </button> | ||||
|                                 {dropdownOpen && ( | ||||
|                                     <div className="absolute left-0 top-full mt-1 w-48 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 z-50"> | ||||
|                                         <div className="py-1" role="menu" aria-orientation="vertical" aria-labelledby="options-menu"> | ||||
|                                             {courses.map((course) => ( | ||||
|                                                 <a | ||||
|                                                     key={course.id} | ||||
|                                                     href={`/courses/${course.id}`} | ||||
|                                                     className={`block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 hover:text-mainColor`} | ||||
|                                                     role="menuitem" | ||||
|                                                 > | ||||
|                                                     {course.title} | ||||
|                                                 </a> | ||||
|                                             ))} | ||||
|                                         </div> | ||||
|                                     </div> | ||||
|                                 )} | ||||
|                             </div> | ||||
|                             <Link key={"03"} href={"#"} > | ||||
|                                 <p className="nav__link"> | ||||
|                                     {"關於我們"} | ||||
|                                 </p> | ||||
|                             </Link> | ||||
|                         </div> | ||||
|                     </div> | ||||
|                 </div> | ||||
|                 {/*button*/} | ||||
|                 <div className="flex items-center space-x-5 "> | ||||
|                     <div className="flex items-center space-x-2 max-sm:hidden"> | ||||
|                         <div className="bg-[#DCCECF] rounded-full h-8 w-8 flex items-center justify-center hover:bg-mainColor" | ||||
|                             onClick={() =>  window.open(settings.facebook)} > | ||||
|                             <FaFacebookF | ||||
|                                 className="w-4 h-4 cursor-pointer text-[#F6E5E9]" | ||||
|                             /> | ||||
|                         </div> | ||||
|                         <div className="bg-[#DCCECF] rounded-full h-8 w-8 flex items-center justify-center hover:bg-mainColor" | ||||
|                             onClick={() => window.open(settings.instagram)} > | ||||
|                             <RiInstagramFill | ||||
|                                 className="w-4 h-4 cursor-pointer text-[#F6E5E9]" | ||||
|                             /> | ||||
|                         </div> | ||||
|  | ||||
|                         <div className="bg-[#DCCECF] rounded-full h-8 w-8 flex items-center justify-center hover:bg-mainColor" | ||||
|                             onClick={() => window.open(settings.youtube)} > | ||||
|                             <FaYoutube | ||||
|                                 className="w-4 h-4 cursor-pointer text-[#F6E5E9]" | ||||
|                             /> | ||||
|                         </div> | ||||
|                     </div> | ||||
|                     {/*burger*/} | ||||
|                     {showNav ? ( | ||||
|                         <IoClose | ||||
|                             onClick={openNav} | ||||
|                             className="w-8 h-8 cursor-pointer text-mainColor lg:hidden ml-2" | ||||
|                         /> | ||||
|                     ) : ( | ||||
|                         <IoMenu | ||||
|                             id='menubutton' | ||||
|                             onClick={openNav} | ||||
|                             className="w-8 h-8 cursor-pointer text-mainColor lg:hidden ml-2" | ||||
|                         /> | ||||
|                     )} | ||||
|                 </div> | ||||
|  | ||||
|             </div> | ||||
|         </div> | ||||
|     ) | ||||
| } | ||||
| export default Nav | ||||
							
								
								
									
										20
									
								
								components/Navbar/ResponsiveNav.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								components/Navbar/ResponsiveNav.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,20 @@ | ||||
| "use client" | ||||
| import React, {useState} from 'react' | ||||
| import MobileNav from './MobileNav' | ||||
| import Nav from './Nav' | ||||
| import { CoursesProps,SettingsProps } from '@/types' | ||||
|  | ||||
| const ResponsiveNav = ({ courses, settings }: { courses: CoursesProps[], settings: SettingsProps }) => { | ||||
|   const [showNav, setShowNav] = useState(false) | ||||
|   const toggleNavHandler = () => setShowNav(!showNav) | ||||
|   const showNavHandler = () => setShowNav(true) | ||||
|   const closeNavHandler = () => setShowNav(false) | ||||
|  | ||||
|   return ( | ||||
|     <div> | ||||
|       <Nav openNav={toggleNavHandler} showNav={showNav}  courses={courses} settings={settings} /> | ||||
|       <MobileNav showNav={showNav} closeNav={closeNavHandler} courses={courses}  /> | ||||
|     </div> | ||||
|   ) | ||||
| } | ||||
| export default ResponsiveNav | ||||
							
								
								
									
										143
									
								
								components/TextGradientScrollContext.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										143
									
								
								components/TextGradientScrollContext.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,143 @@ | ||||
| "use client"; | ||||
|  | ||||
| import React, { createContext, useContext, useRef } from "react"; | ||||
| import { useScroll, useTransform, motion, MotionValue } from "framer-motion"; | ||||
| import { cn } from "./utils"; | ||||
|  | ||||
| type TextOpacityEnum = "none" | "soft" | "medium"; | ||||
| type ViewTypeEnum = "word" | "letter"; | ||||
|  | ||||
| type TextGradientScrollType = { | ||||
|   text: string; | ||||
|   type?: ViewTypeEnum; | ||||
|   className?: string; | ||||
|   textOpacity?: TextOpacityEnum; | ||||
| }; | ||||
|  | ||||
| type LetterType = { | ||||
|   children: React.ReactNode | string; | ||||
|   progress: MotionValue<number>; | ||||
|   range: number[]; | ||||
| }; | ||||
|  | ||||
| type WordType = { | ||||
|   children: React.ReactNode; | ||||
|   progress: MotionValue<number>; | ||||
|   range: number[]; | ||||
| }; | ||||
|  | ||||
| type CharType = { | ||||
|   children: React.ReactNode; | ||||
|   progress: MotionValue<number>; | ||||
|   range: number[]; | ||||
| }; | ||||
|  | ||||
| type TextGradientScrollContextType = { | ||||
|   textOpacity?: TextOpacityEnum; | ||||
|   type?: ViewTypeEnum; | ||||
| }; | ||||
|  | ||||
| const TextGradientScrollContext = createContext<TextGradientScrollContextType>( | ||||
|   {} | ||||
| ); | ||||
|  | ||||
| function useGradientScroll() { | ||||
|   const context = useContext(TextGradientScrollContext); | ||||
|   return context; | ||||
| } | ||||
|  | ||||
| export default function TextGradientScroll({ | ||||
|   text, | ||||
|   className, | ||||
|   type = "letter", | ||||
|   textOpacity = "soft", | ||||
| }: TextGradientScrollType) { | ||||
|   const ref = useRef<HTMLParagraphElement>(null); | ||||
|   const { scrollYProgress } = useScroll({ | ||||
|     target: ref, | ||||
|     offset: ["start center", "end center"], | ||||
|   }); | ||||
|  | ||||
|   const words = text.split(" "); | ||||
|  | ||||
|   return ( | ||||
|     <TextGradientScrollContext.Provider value={{ textOpacity, type }}> | ||||
|       <p ref={ref} className={cn("relative flex m-0 flex-wrap", className)}> | ||||
|         {words.map((word, i) => { | ||||
|           const start = i / words.length; | ||||
|           const end = start + 1 / words.length; | ||||
|           return type === "word" ? ( | ||||
|             <Word key={i} progress={scrollYProgress} range={[start, end]}> | ||||
|               {word} | ||||
|             </Word> | ||||
|           ) : ( | ||||
|             <Letter key={i} progress={scrollYProgress} range={[start, end]}> | ||||
|               {word} | ||||
|             </Letter> | ||||
|           ); | ||||
|         })} | ||||
|       </p> | ||||
|     </TextGradientScrollContext.Provider> | ||||
|   ); | ||||
| } | ||||
|  | ||||
| const Word = ({ children, progress, range }: WordType) => { | ||||
|   const opacity = useTransform(progress, range, [0, 1]); | ||||
|  | ||||
|   return ( | ||||
|     <span className="relative me-2 mt-2"> | ||||
|       <span style={{ position: "absolute", opacity: 0.1 }}>{children}</span> | ||||
|       <motion.span style={{ transition: "all .5s", opacity: opacity }}> | ||||
|         {children} | ||||
|       </motion.span> | ||||
|     </span> | ||||
|   ); | ||||
| }; | ||||
|  | ||||
| const Letter = ({ children, progress, range }: LetterType) => { | ||||
|   if (typeof children === "string") { | ||||
|     const amount = range[1] - range[0]; | ||||
|     const step = amount / children.length; | ||||
|  | ||||
|     return ( | ||||
|       <span className="relative me-2 mt-2"> | ||||
|         {children.split("").map((char: string, i: number) => { | ||||
|           const start = range[0] + i * step; | ||||
|           const end = range[0] + (i + 1) * step; | ||||
|           return ( | ||||
|             <Char key={`c_${i}`} progress={progress} range={[start, end]}> | ||||
|               {char} | ||||
|             </Char> | ||||
|           ); | ||||
|         })} | ||||
|       </span> | ||||
|     ); | ||||
|   } | ||||
| }; | ||||
|  | ||||
| const Char = ({ children, progress, range }: CharType) => { | ||||
|   const opacity = useTransform(progress, range, [0, 1]); | ||||
|   const { textOpacity } = useGradientScroll(); | ||||
|  | ||||
|   return ( | ||||
|     <span> | ||||
|       <span | ||||
|         className={cn("absolute", { | ||||
|           "opacity-0": textOpacity == "none", | ||||
|           "opacity-10": textOpacity == "soft", | ||||
|           "opacity-30": textOpacity == "medium", | ||||
|         })} | ||||
|       > | ||||
|         {children} | ||||
|       </span> | ||||
|       <motion.span | ||||
|         style={{ | ||||
|           transition: "all .5s", | ||||
|           opacity: opacity, | ||||
|         }} | ||||
|       > | ||||
|         {children} | ||||
|       </motion.span> | ||||
|     </span> | ||||
|   ); | ||||
| }; | ||||
							
								
								
									
										18
									
								
								components/Whatsapp.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								components/Whatsapp.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | ||||
| import React from 'react' | ||||
| import { IoLogoWhatsapp } from "react-icons/io"; | ||||
| import { SettingsProps } from '@/types'; | ||||
| const Whatsapp = ({ settings }: { settings: SettingsProps }) => { | ||||
|     return ( | ||||
|         <div className=" relative"> | ||||
|             <button className=" z-20 text-white flex flex-col shrink-0 grow-0 justify-around  | ||||
|                   fixed bottom-0 right-0 right-5 rounded-lg | ||||
|                   mr-1 mb-5 lg:mr-5 lg:mb-5 xl:mr-10 xl:mb-10" | ||||
|                   onClick={()=>window.open(`https://wa.me/${settings.whatsapp}`)}> | ||||
|                 <IoLogoWhatsapp size={50} className='text-[#D60050]'/> | ||||
|  | ||||
|             </button> | ||||
|         </div> | ||||
|     ) | ||||
| } | ||||
|  | ||||
| export default Whatsapp | ||||
							
								
								
									
										30
									
								
								components/slick.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								components/slick.css
									
									
									
									
									
										Normal 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; | ||||
|   } | ||||
							
								
								
									
										6
									
								
								components/utils.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								components/utils.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | ||||
| import { ClassValue, clsx } from 'clsx'; | ||||
| import { twMerge } from 'tailwind-merge'; | ||||
|  | ||||
| export function cn(...inputs: ClassValue[]) { | ||||
|   return twMerge(clsx(inputs)); | ||||
| } | ||||
		Reference in New Issue
	
	Block a user