first commit

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

6
app/app.config.tsx Normal file
View File

@ -0,0 +1,6 @@
module.exports = {
colors:{
},
api_link: "http://localhost/api/v1/",
}

View File

@ -0,0 +1,43 @@
import React from 'react'
import ResponsiveNav from "@/components/Navbar/ResponsiveNav";
import { fetchCourses, fetchCourse, fetchSettings } from "@/utils";
import { CoursesProps } from "@/types";
import Course from '@/components/Course/Course';
import dynamic from 'next/dynamic'
const DynamicComponent = dynamic(() => import('@/components/Course/Course'), {
ssr: false,
})
async function getCourses() {
const courses = await fetchCourses();
return courses;
}
async function getCourse(slug: string) {
const course = await fetchCourse(slug);
return course;
}
async function getSettings() {
const settings = await fetchSettings();
return settings;
}
export const metadata = {
title: "All In One",
description: "**",
}
export default async function Page({ params }: { params: { slug: string } }) {
const courses = await getCourses();
const settings = await getSettings();
//const course = await getCourse(params.slug);
const course = courses.find(course => course.id === params.slug);
if (!course) {
throw new Error(`Course with slug ${params.slug} not found`);
}
return (
<div className='bg-[#F6E8E8]'>
<ResponsiveNav courses={courses} settings={settings} />
<DynamicComponent course={course} />
</div>
)
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 5.9 KiB

View File

@ -2,26 +2,45 @@
@tailwind components;
@tailwind utilities;
:root {
--background: #ffffff;
--foreground: #171717;
}
@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
@layer components {
.nav__link {
@apply relative text-base font-medium w-fit hover:text-[#D60050];
}
}
.custom-position {
object-position: 60% -3px;
}
.mainBgColor {
background-color: #D60050;
}
.mainColor{
color: #D60050;
}
.dots {
li{
margin: 0 10px;
}
body {
color: var(--foreground);
background: var(--background);
font-family: Arial, Helvetica, sans-serif;
button::before {
font-size: 2rem !important;
color: #D60050 !important;
}
}
body.overflow-hidden {
overflow: hidden;
}
}
@layer utilities {
.text-balance {
text-wrap: balance;
/* Hide scrollbar for Chrome, Safari and Opera */
.no-scrollbar::-webkit-scrollbar {
display: none;
}
/* Hide scrollbar for IE, Edge and Firefox */
.no-scrollbar {
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
}
}

View File

@ -1,7 +1,10 @@
import type { Metadata } from "next";
import localFont from "next/font/local";
import "./globals.css";
import ResponsiveNav from "../components/Navbar/ResponsiveNav";
import { fetchCourses } from "@/utils";
import { TailSpin } from "react-loader-spinner";
import { Toaster } from "react-hot-toast";
const geistSans = localFont({
src: "./fonts/GeistVF.woff",
variable: "--font-geist-sans",
@ -19,6 +22,7 @@ export const metadata: Metadata = {
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
@ -26,10 +30,13 @@ export default function RootLayout({
return (
<html lang="en">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
className={`${geistSans.variable} ${geistMono.variable} antialiased bg-[#F6E5E9]`}
>
<Toaster position="bottom-center" />
{children}
</body>
</html>
);
}

View File

@ -1,101 +1,33 @@
import Image from "next/image";
import Home from "../components/Home/Home";
import { fetchCourses, fetchSettings} from "../utils/index";
import { CoursesProps } from "@/types";
import ResponsiveNav from "../components/Navbar/ResponsiveNav";
type Props = {}
export default function Home() {
async function getCourses() {
const courses = await fetchCourses();
return courses;
}
async function getSettings(){
const settings = await fetchSettings();
return settings;
}
export const metadata = {
title: "All In One",
description: "**",
}
export default async function HomePage({ }: Props) {
const courses = await getCourses();
const settings = await getSettings();
return (
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]">
<main className="flex flex-col gap-8 row-start-2 items-center sm:items-start">
<Image
className="dark:invert"
src="https://nextjs.org/icons/next.svg"
alt="Next.js logo"
width={180}
height={38}
priority
/>
<ol className="list-inside list-decimal text-sm text-center sm:text-left font-[family-name:var(--font-geist-mono)]">
<li className="mb-2">
Get started by editing{" "}
<code className="bg-black/[.05] dark:bg-white/[.06] px-1 py-0.5 rounded font-semibold">
app/page.tsx
</code>
.
</li>
<li>Save and see your changes instantly.</li>
</ol>
<div className="flex gap-4 items-center flex-col sm:flex-row">
<a
className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5"
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
className="dark:invert"
src="https://nextjs.org/icons/vercel.svg"
alt="Vercel logomark"
width={20}
height={20}
/>
Deploy now
</a>
<a
className="rounded-full border border-solid border-black/[.08] dark:border-white/[.145] transition-colors flex items-center justify-center hover:bg-[#f2f2f2] dark:hover:bg-[#1a1a1a] hover:border-transparent text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:min-w-44"
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
Read our docs
</a>
</div>
</main>
<footer className="row-start-3 flex gap-6 flex-wrap items-center justify-center">
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="https://nextjs.org/icons/file.svg"
alt="File icon"
width={16}
height={16}
/>
Learn
</a>
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="https://nextjs.org/icons/window.svg"
alt="Window icon"
width={16}
height={16}
/>
Examples
</a>
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="https://nextjs.org/icons/globe.svg"
alt="Globe icon"
width={16}
height={16}
/>
Go to nextjs.org
</a>
</footer>
<div>
<ResponsiveNav courses={courses} settings={settings} />
<Home courses={courses} settings={settings} />
</div>
);
}

112
components/ContactForm.tsx Normal file
View 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

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;
}

View 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;

View 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
View 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
View 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
View 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
View 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
View 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

View 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
View 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

View 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

View 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
View 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
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;
}

6
components/utils.ts Normal file
View File

@ -0,0 +1,6 @@
import { ClassValue, clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}

497
package-lock.json generated
View File

@ -8,14 +8,34 @@
"name": "webfrontend",
"version": "0.1.0",
"dependencies": {
"animate.css": "^4.1.1",
"clsx": "^2.1.1",
"dotenv": "^16.4.5",
"embla-carousel-react": "^8.3.0",
"framer-motion": "^11.7.0",
"gsap": "^3.12.5",
"keen-slider": "^6.8.6",
"moment": "^2.30.1",
"next": "14.2.13",
"nextjs-cors": "^2.2.0",
"react": "^18",
"react-dom": "^18"
"react-animate-on-scroll": "^2.1.9",
"react-dom": "^18",
"react-hook-form": "^7.53.0",
"react-hot-toast": "^2.4.1",
"react-icons": "^5.3.0",
"react-loader-spinner": "^6.1.6",
"react-scroll-motion": "^0.3.3",
"react-slick": "^0.30.2",
"slick-carousel": "^1.8.1",
"tailwind-merge": "^2.5.2"
},
"devDependencies": {
"@types/node": "^20",
"@types/react": "^18",
"@types/react-animate-on-scroll": "^2.1.8",
"@types/react-dom": "^18",
"@types/react-slick": "^0.23.13",
"eslint": "^8",
"eslint-config-next": "14.2.13",
"postcss": "^8",
@ -36,6 +56,27 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@emotion/is-prop-valid": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.2.tgz",
"integrity": "sha512-uNsoYd37AFmaCdXlg6EYD1KaPOaRWRByMCYzbKUX4+hhMfrxdVSelShywL4JVaAeM/eHUOSprYBQls+/neX3pw==",
"license": "MIT",
"dependencies": {
"@emotion/memoize": "^0.8.1"
}
},
"node_modules/@emotion/memoize": {
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz",
"integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==",
"license": "MIT"
},
"node_modules/@emotion/unitless": {
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.1.tgz",
"integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==",
"license": "MIT"
},
"node_modules/@eslint-community/eslint-utils": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz",
@ -518,6 +559,16 @@
"csstype": "^3.0.2"
}
},
"node_modules/@types/react-animate-on-scroll": {
"version": "2.1.8",
"resolved": "https://registry.npmjs.org/@types/react-animate-on-scroll/-/react-animate-on-scroll-2.1.8.tgz",
"integrity": "sha512-Lyd1hb1aY9T0bOUL3VE7bKuOlAv2nBziJg5piDLqW+jxzy5jCa/nIftsOpYxZ0+Sdo0wFXuI6tpLo6B0Q288IQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/react": "*"
}
},
"node_modules/@types/react-dom": {
"version": "18.3.0",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.0.tgz",
@ -528,6 +579,22 @@
"@types/react": "*"
}
},
"node_modules/@types/react-slick": {
"version": "0.23.13",
"resolved": "https://registry.npmjs.org/@types/react-slick/-/react-slick-0.23.13.tgz",
"integrity": "sha512-bNZfDhe/L8t5OQzIyhrRhBr/61pfBcWaYJoq6UDqFtv5LMwfg4NsVDD2J8N01JqdAdxLjOt66OZEp6PX+dGs/A==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/react": "*"
}
},
"node_modules/@types/stylis": {
"version": "4.2.5",
"resolved": "https://registry.npmjs.org/@types/stylis/-/stylis-4.2.5.tgz",
"integrity": "sha512-1Xve+NMN7FWjY14vLoY5tL3BVEQ/n42YLwaqJIPYhotZ9uBHt87VceMwWQpzmdEt2TNXIorIFG+YeCUUW7RInw==",
"license": "MIT"
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.7.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.7.0.tgz",
@ -791,6 +858,12 @@
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/animate.css": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/animate.css/-/animate.css-4.1.1.tgz",
"integrity": "sha512-+mRmCTv6SbCmtYJCN4faJMNFVNN5EuCTTprDTAo7YzIGji2KADmakjVA3+8mVDkZ2Bf09vayB35lSQIex2+QaQ==",
"license": "MIT"
},
"node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
@ -1158,6 +1231,15 @@
"node": ">= 6"
}
},
"node_modules/camelize": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz",
"integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001663",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001663.tgz",
@ -1233,12 +1315,27 @@
"node": ">= 6"
}
},
"node_modules/classnames": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz",
"integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==",
"license": "MIT"
},
"node_modules/client-only": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
"license": "MIT"
},
"node_modules/clsx": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@ -1276,6 +1373,19 @@
"dev": true,
"license": "MIT"
},
"node_modules/cors": {
"version": "2.8.5",
"resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
"integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
"license": "MIT",
"dependencies": {
"object-assign": "^4",
"vary": "^1"
},
"engines": {
"node": ">= 0.10"
}
},
"node_modules/cross-spawn": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
@ -1291,6 +1401,26 @@
"node": ">= 8"
}
},
"node_modules/css-color-keywords": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz",
"integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==",
"license": "ISC",
"engines": {
"node": ">=4"
}
},
"node_modules/css-to-react-native": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz",
"integrity": "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==",
"license": "MIT",
"dependencies": {
"camelize": "^1.0.0",
"css-color-keywords": "^1.0.0",
"postcss-value-parser": "^4.0.2"
}
},
"node_modules/cssesc": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
@ -1308,7 +1438,6 @@
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
"dev": true,
"license": "MIT"
},
"node_modules/damerau-levenshtein": {
@ -1493,6 +1622,18 @@
"node": ">=6.0.0"
}
},
"node_modules/dotenv": {
"version": "16.4.5",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz",
"integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://dotenvx.com"
}
},
"node_modules/eastasianwidth": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
@ -1500,6 +1641,34 @@
"dev": true,
"license": "MIT"
},
"node_modules/embla-carousel": {
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.3.0.tgz",
"integrity": "sha512-Ve8dhI4w28qBqR8J+aMtv7rLK89r1ZA5HocwFz6uMB/i5EiC7bGI7y+AM80yAVUJw3qqaZYK7clmZMUR8kM3UA==",
"license": "MIT"
},
"node_modules/embla-carousel-react": {
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/embla-carousel-react/-/embla-carousel-react-8.3.0.tgz",
"integrity": "sha512-P1FlinFDcIvggcErRjNuVqnUR8anyo8vLMIH8Rthgofw7Nj8qTguCa2QjFAbzxAUTQTPNNjNL7yt0BGGinVdFw==",
"license": "MIT",
"dependencies": {
"embla-carousel": "8.3.0",
"embla-carousel-reactive-utils": "8.3.0"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.1 || ^18.0.0"
}
},
"node_modules/embla-carousel-reactive-utils": {
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/embla-carousel-reactive-utils/-/embla-carousel-reactive-utils-8.3.0.tgz",
"integrity": "sha512-EYdhhJ302SC4Lmkx8GRsp0sjUhEN4WyFXPOk0kGu9OXZSRMmcBlRgTvHcq8eKJE1bXWBsOi1T83B+BSSVZSmwQ==",
"license": "MIT",
"peerDependencies": {
"embla-carousel": "8.3.0"
}
},
"node_modules/emoji-regex": {
"version": "9.2.2",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
@ -1521,6 +1690,12 @@
"node": ">=10.13.0"
}
},
"node_modules/enquire.js": {
"version": "2.1.6",
"resolved": "https://registry.npmjs.org/enquire.js/-/enquire.js-2.1.6.tgz",
"integrity": "sha512-/KujNpO+PT63F7Hlpu4h3pE3TokKRHN26JYmQpPyjkRD/N57R7bPDNojMXdi7uveAKjYB7yQnartCxZnFWr0Xw==",
"license": "MIT"
},
"node_modules/es-abstract": {
"version": "1.23.3",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.3.tgz",
@ -2322,6 +2497,31 @@
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/framer-motion": {
"version": "11.7.0",
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.7.0.tgz",
"integrity": "sha512-m+1E3mMzDIQ5DsVghMvXyC+jSkZSm5RHBLA2gHa/LczcXwW6JbQK4Uz48LsuCTGV8bZFVUezcauHj3M33tY/5w==",
"license": "MIT",
"dependencies": {
"tslib": "^2.4.0"
},
"peerDependencies": {
"@emotion/is-prop-valid": "*",
"react": "^18.0.0",
"react-dom": "^18.0.0"
},
"peerDependenciesMeta": {
"@emotion/is-prop-valid": {
"optional": true
},
"react": {
"optional": true
},
"react-dom": {
"optional": true
}
}
},
"node_modules/fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
@ -2529,6 +2729,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/goober": {
"version": "2.1.14",
"resolved": "https://registry.npmjs.org/goober/-/goober-2.1.14.tgz",
"integrity": "sha512-4UpC0NdGyAFqLNPnhCT2iHpza2q+RAY3GV85a/mRPdzyPQMsj0KmMMuetdIkzWRbJ+Hgau1EZztq8ImmiMGhsg==",
"license": "MIT",
"peerDependencies": {
"csstype": "^3.0.10"
}
},
"node_modules/gopd": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
@ -2555,6 +2764,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/gsap": {
"version": "3.12.5",
"resolved": "https://registry.npmjs.org/gsap/-/gsap-3.12.5.tgz",
"integrity": "sha512-srBfnk4n+Oe/ZnMIOXt3gT605BX9x5+rh/prT2F1SsNJsU1XuMiP0E2aptW481OnonOGACZWBqseH5Z7csHxhQ==",
"license": "Standard 'no charge' license: https://gsap.com/standard-license. Club GSAP members get more: https://gsap.com/licensing/. Why GreenSock doesn't employ an MIT license: https://gsap.com/why-license/"
},
"node_modules/has-bigints": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz",
@ -3196,6 +3411,13 @@
"jiti": "bin/jiti.js"
}
},
"node_modules/jquery": {
"version": "3.7.1",
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz",
"integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==",
"license": "MIT",
"peer": true
},
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@ -3236,6 +3458,15 @@
"dev": true,
"license": "MIT"
},
"node_modules/json2mq": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/json2mq/-/json2mq-0.2.0.tgz",
"integrity": "sha512-SzoRg7ux5DWTII9J2qkrZrqV1gt+rTaoufMxEzXbS26Uid0NwaJd123HcoB80TgubEppxxIGdNxCx50fEoEWQA==",
"license": "MIT",
"dependencies": {
"string-convert": "^0.2.0"
}
},
"node_modules/json5": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
@ -3265,6 +3496,12 @@
"node": ">=4.0"
}
},
"node_modules/keen-slider": {
"version": "6.8.6",
"resolved": "https://registry.npmjs.org/keen-slider/-/keen-slider-6.8.6.tgz",
"integrity": "sha512-dcEQ7GDBpCjUQA8XZeWh3oBBLLmyn8aoeIQFGL/NTVkoEOsmlnXqA4QykUm/SncolAZYGsEk/PfUhLZ7mwMM2w==",
"license": "MIT"
},
"node_modules/keyv": {
"version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
@ -3342,6 +3579,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/lodash.debounce": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
"integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==",
"license": "MIT"
},
"node_modules/lodash.merge": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
@ -3349,6 +3592,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/lodash.throttle": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz",
"integrity": "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==",
"license": "MIT"
},
"node_modules/loose-envify": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
@ -3425,6 +3674,15 @@
"node": ">=16 || 14 >=14.17"
}
},
"node_modules/moment": {
"version": "2.30.1",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz",
"integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==",
"license": "MIT",
"engines": {
"node": "*"
}
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@ -3547,6 +3805,18 @@
"node": "^10 || ^12 || >=14"
}
},
"node_modules/nextjs-cors": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/nextjs-cors/-/nextjs-cors-2.2.0.tgz",
"integrity": "sha512-FZu/A+L59J4POJNqwXYyCPDvsLDeu5HjSBvytzS6lsrJeDz5cmnH45zV+VoNic0hjaeER9xGaiIjZIWzEHnxQg==",
"license": "MIT",
"dependencies": {
"cors": "^2.8.5"
},
"peerDependencies": {
"next": "^8.1.1-canary.54 || ^9.0.0 || ^10.0.0-0 || ^11.0.0 || ^12.0.0 || ^13.0.0 || ^14.0.0"
}
},
"node_modules/normalize-path": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
@ -3561,7 +3831,6 @@
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
@ -4039,7 +4308,6 @@
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
"dev": true,
"license": "MIT"
},
"node_modules/prelude-ls": {
@ -4056,7 +4324,6 @@
"version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
"dev": true,
"license": "MIT",
"dependencies": {
"loose-envify": "^1.4.0",
@ -4107,6 +4374,20 @@
"node": ">=0.10.0"
}
},
"node_modules/react-animate-on-scroll": {
"version": "2.1.9",
"resolved": "https://registry.npmjs.org/react-animate-on-scroll/-/react-animate-on-scroll-2.1.9.tgz",
"integrity": "sha512-E4PZLX6RDLLn+/iIMhnQrC1xU74ixGcCQ5/TBX8fBsaO+SnaU9VFoZLvIfUqVf3mH5HUNzO8wAqA11niot5Obw==",
"license": "MIT",
"dependencies": {
"lodash.throttle": "^4.1.1",
"prop-types": "^15.5.9"
},
"peerDependencies": {
"classnames": "^2.2.5",
"react": ">= 15.4.1 < 19.0.0-0"
}
},
"node_modules/react-dom": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
@ -4120,13 +4401,103 @@
"react": "^18.3.1"
}
},
"node_modules/react-hook-form": {
"version": "7.53.0",
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.53.0.tgz",
"integrity": "sha512-M1n3HhqCww6S2hxLxciEXy2oISPnAzxY7gvwVPrtlczTM/1dDadXgUxDpHMrMTblDOcm/AXtXxHwZ3jpg1mqKQ==",
"license": "MIT",
"engines": {
"node": ">=18.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/react-hook-form"
},
"peerDependencies": {
"react": "^16.8.0 || ^17 || ^18 || ^19"
}
},
"node_modules/react-hot-toast": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.4.1.tgz",
"integrity": "sha512-j8z+cQbWIM5LY37pR6uZR6D4LfseplqnuAO4co4u8917hBUvXlEqyP1ZzqVLcqoyUesZZv/ImreoCeHVDpE5pQ==",
"license": "MIT",
"dependencies": {
"goober": "^2.1.10"
},
"engines": {
"node": ">=10"
},
"peerDependencies": {
"react": ">=16",
"react-dom": ">=16"
}
},
"node_modules/react-icons": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.3.0.tgz",
"integrity": "sha512-DnUk8aFbTyQPSkCfF8dbX6kQjXA9DktMeJqfjrg6cK9vwQVMxmcA3BfP4QoiztVmEHtwlTgLFsPuH2NskKT6eg==",
"license": "MIT",
"peerDependencies": {
"react": "*"
}
},
"node_modules/react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
"dev": true,
"license": "MIT"
},
"node_modules/react-loader-spinner": {
"version": "6.1.6",
"resolved": "https://registry.npmjs.org/react-loader-spinner/-/react-loader-spinner-6.1.6.tgz",
"integrity": "sha512-x5h1Jcit7Qn03MuKlrWcMG9o12cp9SNDVHVJTNRi9TgtGPKcjKiXkou4NRfLAtXaFB3+Z8yZsVzONmPzhv2ErA==",
"license": "MIT",
"dependencies": {
"react-is": "^18.2.0",
"styled-components": "^6.1.2"
},
"engines": {
"node": ">= 12"
},
"peerDependencies": {
"react": "^16.0.0 || ^17.0.0 || ^18.0.0",
"react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/react-loader-spinner/node_modules/react-is": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
"integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
"license": "MIT"
},
"node_modules/react-scroll-motion": {
"version": "0.3.3",
"resolved": "https://registry.npmjs.org/react-scroll-motion/-/react-scroll-motion-0.3.3.tgz",
"integrity": "sha512-Q33prpcdWDfQaHVmED7bo/EReUEBHfL6mOfvMGFP3FhDxTKddIoocMcd3bT9qw2KQ/fqNdJNADf7aiHWSLkXuQ==",
"license": "MIT",
"peerDependencies": {
"react": "^18.0.11",
"react-dom": "^18.0.11"
}
},
"node_modules/react-slick": {
"version": "0.30.2",
"resolved": "https://registry.npmjs.org/react-slick/-/react-slick-0.30.2.tgz",
"integrity": "sha512-XvQJi7mRHuiU3b9irsqS9SGIgftIfdV5/tNcURTb5LdIokRA5kIIx3l4rlq2XYHfxcSntXapoRg/GxaVOM1yfg==",
"license": "MIT",
"dependencies": {
"classnames": "^2.2.5",
"enquire.js": "^2.1.6",
"json2mq": "^0.2.0",
"lodash.debounce": "^4.0.8",
"resize-observer-polyfill": "^1.5.0"
},
"peerDependencies": {
"react": "^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0",
"react-dom": "^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/read-cache": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
@ -4191,6 +4562,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/resize-observer-polyfill": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz",
"integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==",
"license": "MIT"
},
"node_modules/resolve": {
"version": "1.22.8",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz",
@ -4396,6 +4773,12 @@
"node": ">= 0.4"
}
},
"node_modules/shallowequal": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz",
"integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==",
"license": "MIT"
},
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@ -4451,6 +4834,15 @@
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/slick-carousel": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/slick-carousel/-/slick-carousel-1.8.1.tgz",
"integrity": "sha512-XB9Ftrf2EEKfzoQXt3Nitrt/IPbT+f1fgqBdoxO3W/+JYvtEOW6EgxnWfr9GH6nmULv7Y2tPmEX3koxThVmebA==",
"license": "MIT",
"peerDependencies": {
"jquery": ">=1.8.0"
}
},
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
@ -4481,6 +4873,12 @@
"node": ">=10.0.0"
}
},
"node_modules/string-convert": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/string-convert/-/string-convert-0.2.1.tgz",
"integrity": "sha512-u/1tdPl4yQnPBjnVrmdLo9gtuLvELKsAoRapekWggdiQNvvvum+jYF329d84NAa660KQw7pB2n36KrIKVoXa3A==",
"license": "MIT"
},
"node_modules/string-width": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
@ -4702,6 +5100,68 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/styled-components": {
"version": "6.1.13",
"resolved": "https://registry.npmjs.org/styled-components/-/styled-components-6.1.13.tgz",
"integrity": "sha512-M0+N2xSnAtwcVAQeFEsGWFFxXDftHUD7XrKla06QbpUMmbmtFBMMTcKWvFXtWxuD5qQkB8iU5gk6QASlx2ZRMw==",
"license": "MIT",
"dependencies": {
"@emotion/is-prop-valid": "1.2.2",
"@emotion/unitless": "0.8.1",
"@types/stylis": "4.2.5",
"css-to-react-native": "3.2.0",
"csstype": "3.1.3",
"postcss": "8.4.38",
"shallowequal": "1.1.0",
"stylis": "4.3.2",
"tslib": "2.6.2"
},
"engines": {
"node": ">= 16"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/styled-components"
},
"peerDependencies": {
"react": ">= 16.8.0",
"react-dom": ">= 16.8.0"
}
},
"node_modules/styled-components/node_modules/postcss": {
"version": "8.4.38",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz",
"integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==",
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/postcss"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"nanoid": "^3.3.7",
"picocolors": "^1.0.0",
"source-map-js": "^1.2.0"
},
"engines": {
"node": "^10 || ^12 || >=14"
}
},
"node_modules/styled-components/node_modules/tslib": {
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
"integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==",
"license": "0BSD"
},
"node_modules/styled-jsx": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.1.tgz",
@ -4725,6 +5185,12 @@
}
}
},
"node_modules/stylis": {
"version": "4.3.2",
"resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.2.tgz",
"integrity": "sha512-bhtUjWd/z6ltJiQwg0dUfxEJ+W+jdqQd8TbWLWyeIJHlnsqmGLRFFd8e5mA0AZi/zx90smXRlN66YMTcaSFifg==",
"license": "MIT"
},
"node_modules/sucrase": {
"version": "3.35.0",
"resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz",
@ -4774,6 +5240,16 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/tailwind-merge": {
"version": "2.5.2",
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.5.2.tgz",
"integrity": "sha512-kjEBm+pvD+6eAwzJL2Bi+02/9LFLal1Gs61+QB7HvTfQQ0aXwC5LGT8PEt1gS0CWKktKe6ysPTAy3cBC5MeiIg==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/dcastil"
}
},
"node_modules/tailwindcss": {
"version": "3.4.13",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.13.tgz",
@ -5061,6 +5537,15 @@
"dev": true,
"license": "MIT"
},
"node_modules/vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
"integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",

View File

@ -9,18 +9,38 @@
"lint": "next lint"
},
"dependencies": {
"animate.css": "^4.1.1",
"clsx": "^2.1.1",
"dotenv": "^16.4.5",
"embla-carousel-react": "^8.3.0",
"framer-motion": "^11.7.0",
"gsap": "^3.12.5",
"keen-slider": "^6.8.6",
"moment": "^2.30.1",
"next": "14.2.13",
"nextjs-cors": "^2.2.0",
"react": "^18",
"react-animate-on-scroll": "^2.1.9",
"react-dom": "^18",
"next": "14.2.13"
"react-hook-form": "^7.53.0",
"react-hot-toast": "^2.4.1",
"react-icons": "^5.3.0",
"react-loader-spinner": "^6.1.6",
"react-scroll-motion": "^0.3.3",
"react-slick": "^0.30.2",
"slick-carousel": "^1.8.1",
"tailwind-merge": "^2.5.2"
},
"devDependencies": {
"typescript": "^5",
"@types/node": "^20",
"@types/react": "^18",
"@types/react-animate-on-scroll": "^2.1.8",
"@types/react-dom": "^18",
"@types/react-slick": "^0.23.13",
"eslint": "^8",
"eslint-config-next": "14.2.13",
"postcss": "^8",
"tailwindcss": "^3.4.1",
"eslint": "^8",
"eslint-config-next": "14.2.13"
"typescript": "^5"
}
}

BIN
public/images/014.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

BIN
public/images/contact.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 534 KiB

BIN
public/images/kid.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

BIN
public/images/kid2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 648 KiB

BIN
public/images/line.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
public/images/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

BIN
public/images/piano2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 794 KiB

7
public/themes.ts Normal file
View File

@ -0,0 +1,7 @@
export const colors = {
mainColor: "#D60050"
};
export default {
colors
};

View File

@ -1,4 +1,4 @@
import type { Config } from "tailwindcss";
import type { Config, } from "tailwindcss";
const config: Config = {
content: [
@ -9,6 +9,7 @@ const config: Config = {
theme: {
extend: {
colors: {
mainColor: "#D60050",
background: "var(--background)",
foreground: "var(--foreground)",
},

77
types/index.ts Normal file
View File

@ -0,0 +1,77 @@
import { MouseEventHandler } from "react";
export interface SettingsProps {
address: string;
google_map_api_key: string;
latitude: number;
longitude: number;
phone: string;
email: string;
facebook: string;
instagram: string;
youtube: string;
youtube_link: string;
whatsapp: string;
id: string;
}
export interface MessageProps {
name: string,
phone: string,
email: string,
message: string,
}
export interface CustomButtonProps {
isDisabled?: boolean;
// btnType?: "button" | "submit";
//containerStyles?: string;
//textStyles?: string;
title: string;
text_and_button_size: string;
//rightIcon?: string;
handleClick?: MouseEventHandler<HTMLButtonElement>;
}
export interface imageProps {
image: string,
course_id: string,
index: number,
id: string
}
export interface CoursesProps {
id: string,
title: string,
images: Array<imageProps>,
info_images: Array<imageProps>,
schedule: Array<ScheduleProps>,
sort_description: string,
long_description: string,
information: string,
contant: string,
remark: string,
created_at: string,
}
export interface ScheduleProps {
id: string,
course_id: string,
title: string,
info1: string,
info2: string,
date: string,
}
export interface RerganizedScheduleProps {
yearAndMonth: string,
schedules: Array<ScheduleProps>
}
export interface CoursesArrayProps {
courses: Array<CoursesProps>
}

112
utils/index.ts Normal file
View File

@ -0,0 +1,112 @@
import { CoursesProps, CoursesArrayProps, SettingsProps, MessageProps } from "@/types";
export async function postMessage(data: MessageProps) {
const headers: HeadersInit = {
"Accept": "application/json",
"Content-Type": "application/json",
};
const body = JSON.stringify(data);
const url = `http://localhost/api/v1/messages/`;
try {
const response = await fetch(url, {
method: "POST",
headers,
body,
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const contentType = response.headers.get("content-type");
if (contentType && contentType.indexOf("application/json") !== -1) {
return { success: true }
} else {
const text = await response.text();
throw new Error(`Expected JSON, got ${contentType}: ${text}`);
}
} catch (error) {
console.error("Error in postMessage:", error);
return { success: false };
}
}
export async function fetchCourses() {
const headers: HeadersInit = {
accept: "application/json"
};
const url = `${process.env.api_url}course/?skip=0&limit=100`;
console.log('Fetching from URL:', url);
try {
const response = await fetch(url, { headers });
console.log('Response status:', response.status);
const result = await response.json();
console.log('Fetched data:', result);
const coursesArray: CoursesProps[] = result.data.map((course: CoursesProps) => ({
...course,
images: course.images.sort((a: any, b: any) => a.index - b.index),
info_images: course?.info_images?.sort((a: any, b: any) => a.index - b.index),
}));
return coursesArray;
} catch (error) {
console.error('Fetch error:', error);
throw error;
}
}
export async function fetchCourse(id: string) {
const headers: HeadersInit = {
accept: "application/json"
};
const url = `${process.env.api_url}course/${id}`;
console.log('Fetching from URL:', url);
try {
const response = await fetch(url, { headers });
console.log('Response status:', response.status);
var result: CoursesProps = await response.json();
console.log('Fetched data:', result);
if (result.info_images && Array.isArray(result.info_images)) {
result.info_images.sort((a, b) => a.index - b.index);
}
return result;
} catch (error) {
console.error('Fetch error:', error);
throw error;
}
}
export async function fetchSettings() {
const headers: HeadersInit = {
accept: "application/json"
};
const url = `${process.env.api_url}setting`;
console.log('Fetching from URL:', url);
try {
const response = await fetch(url, { headers });
console.log('Response status:', response.status);
const result: SettingsProps = await response.json();
console.log('Fetched data:', result);
return result;
}
catch (error) {
console.error('Fetch error:', error);
throw error;
}
}