diff --git a/app/app.config.tsx b/app/app.config.tsx new file mode 100644 index 0000000..c0a422e --- /dev/null +++ b/app/app.config.tsx @@ -0,0 +1,6 @@ +module.exports = { + colors:{ + + }, + api_link: "http://localhost/api/v1/", + } \ No newline at end of file diff --git a/app/courses/[slug]/page.tsx b/app/courses/[slug]/page.tsx new file mode 100644 index 0000000..7dfc217 --- /dev/null +++ b/app/courses/[slug]/page.tsx @@ -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 ( +
+ + +
+ ) +} \ No newline at end of file diff --git a/app/favicon.ico b/app/favicon.ico index 718d6fe..04fa867 100644 Binary files a/app/favicon.ico and b/app/favicon.ico differ diff --git a/app/globals.css b/app/globals.css index 13d40b8..9fb8c81 100644 --- a/app/globals.css +++ b/app/globals.css @@ -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 */ } +} \ No newline at end of file diff --git a/app/layout.tsx b/app/layout.tsx index a36cde0..6d6842a 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -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 ( + {children} + ); } + diff --git a/app/page.tsx b/app/page.tsx index 433c8aa..d02f73a 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -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 ( -
-
- Next.js logo -
    -
  1. - Get started by editing{" "} - - app/page.tsx - - . -
  2. -
  3. Save and see your changes instantly.
  4. -
- -
- - Vercel logomark - Deploy now - - - Read our docs - -
-
- +
+ +
); } + diff --git a/components/ContactForm.tsx b/components/ContactForm.tsx new file mode 100644 index 0000000..aefd944 --- /dev/null +++ b/components/ContactForm.tsx @@ -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() + const onSubmit: SubmitHandler = 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 ( + +
+
+ 讓我們知道您的想法 +
+
+ +
+ + +
+ +
+ +
+
+
+

姓名

+ +
+
+

電話

+ +
+
+ +
+

電郵

+ +
+
+

訊息

+ +
+ + +
+
+
+ {/* */} +
+
+
+ + +
+ ) +} + +export default ContactForm diff --git a/components/Course/Accordion.tsx b/components/Course/Accordion.tsx new file mode 100644 index 0000000..9b0db75 --- /dev/null +++ b/components/Course/Accordion.tsx @@ -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>((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 ( +
+ + + +

+ {"課程時間表"} +

+ { + fakeData.map((item, index) => { + return ( + + ) + }) + } +
+ +
+ ) +} + +export default Accordion diff --git a/components/Course/Banner.tsx b/components/Course/Banner.tsx new file mode 100644 index 0000000..0937142 --- /dev/null +++ b/components/Course/Banner.tsx @@ -0,0 +1,24 @@ +import React from 'react' +import { CoursesProps } from '@/types' +const Banner = ({ courseData }: { courseData: CoursesProps }) => { + console.log(courseData) + return ( +
+ +
+

+ {courseData?.title} +

+ +
+ {/*
*/} + kid2 + +
+ ) +} +export default Banner diff --git a/components/Course/Collapse.tsx b/components/Course/Collapse.tsx new file mode 100644 index 0000000..9b5dc90 --- /dev/null +++ b/components/Course/Collapse.tsx @@ -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 + + +} + +const Collapse: React.FC = ({ 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) => ( +
+
    {dots}
+
+ ), + 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) => ( +
+
    {dots}
+
+ ), + dotsClass: 'dots_custom' + }; + + + const [isOpen, setIsOpen] = useState(false) + const [height, setHeight] = useState(0) + const ref = useRef(null) + + useEffect(() => { + if (isOpen) setHeight(ref.current?.scrollHeight) + else setHeight(0) + }, [isOpen]) + + return ( + children && children.length > 0 ? ( +
+ +
+
+ + {info ? (
+ + {info_images && info_images.length > 0 && ( +
+
+ + {info_images.map((image, index) => ( +
+ {`Course +
+ ))} +
+
+ {/*
*/} +
+ + {info_images.map((image, index) => ( +
+
+ {`Course + {index !== currentSlide && ( +
+ )} +
+
+ ))} +
+ +
+
+ )} +
) + : + (
)} +
+ +
+
+
+ ) : ( +
+ ) + ) +} +export default Collapse diff --git a/components/Course/Course.tsx b/components/Course/Course.tsx new file mode 100644 index 0000000..6c9cd98 --- /dev/null +++ b/components/Course/Course.tsx @@ -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 ( +
+ + + + +
+
+ ) +} + +export default Course diff --git a/components/Course/CourseImagesSilder.tsx b/components/Course/CourseImagesSilder.tsx new file mode 100644 index 0000000..3f20327 --- /dev/null +++ b/components/Course/CourseImagesSilder.tsx @@ -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 ( + + ); +} + + +function SampleNextArrow(props: any) { + const { className, style, onClick } = props; + return ( + + ); +} + +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) => ( +
+
    {dots}
+
+ ), + dotsClass: 'dots_custom' + }; + + var settings2 = { + fade: true, + centerMode: true, + infinite: true, + dots: true, + speed: 500, + slidesToShow: 1, + slidesToScroll: 1, + nextArrow: , + prevArrow: , + beforeChange: (current: number, next: number) => setCurrentSlide(next), + appendDots: (dots: any) => ( +
+
    {dots}
+
+ ), + dotsClass: 'dots_custom' + }; + + + + + return ( + +
+ +
+ {courseData.images.length > 0 && ( +
+ + {courseData.images.map((image, index) => ( +
+ {`Course +
+ ))} +
+ +
+ )} +
+
+ {courseData.images.length > 0 && ( +
+ + {courseData.images.map((image, index) => { + return ( +
+
+ {`Course + {index !== currentSlide && ( +
+ )} +
+
+ ) + })} +
+
+ )} +
+
+ + + + ) +} + +export default CourseImagesSilder diff --git a/components/Course/LongDesc.tsx b/components/Course/LongDesc.tsx new file mode 100644 index 0000000..3447b3f --- /dev/null +++ b/components/Course/LongDesc.tsx @@ -0,0 +1,20 @@ +import React from 'react' +import { CoursesProps } from '@/types' +const LongDesc = ({ courseData }: { courseData: CoursesProps }) => { + return ( +
+
+

+ {courseData.title} +

+
+
+ +
+ ) +} + +export default LongDesc diff --git a/components/Course/ScheduleCollapse.tsx b/components/Course/ScheduleCollapse.tsx new file mode 100644 index 0000000..5369e04 --- /dev/null +++ b/components/Course/ScheduleCollapse.tsx @@ -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 = ({ title, rerganizedSchedule }) => { + + + const [isOpen, setIsOpen] = useState(false) + const [height, setHeight] = useState(0) + const ref = useRef(null) + + useEffect(() => { + if (isOpen) setHeight(ref.current?.scrollHeight) + else setHeight(0) + }, [isOpen]) + + return ( +
+ +
+
+ + + {rerganizedSchedule ? ( +
+ {rerganizedSchedule.schedules.map((schedule, index) => ( +
+

+ { moment.utc(schedule.date).format("MM月DD日")} +

+

{schedule.title}

+

{schedule.info1}

+

{schedule.info2}

+
+ ))} +
+ ) : ( +
+ )} + +
+
+
+ ) + +} +export default ScheduleCollapse diff --git a/components/Course/slick.css b/components/Course/slick.css new file mode 100644 index 0000000..b48ece5 --- /dev/null +++ b/components/Course/slick.css @@ -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; + } \ No newline at end of file diff --git a/components/CoursesSilder.tsx b/components/CoursesSilder.tsx new file mode 100644 index 0000000..dc7ac45 --- /dev/null +++ b/components/CoursesSilder.tsx @@ -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) => ( +
+
    {dots}
+
+ ), + dotsClass: 'dots_custom' + }; + console.log(courses) + return ( +
+ {courses.length > 0 ? ( +
+ + {courses.map((course, index) => { + return ( +
+
+
+ Course Image +
+
+
+

+ {course.title} +

+

+ {course.sort_description} +

+
+ +
+
+
+
+ +
+ Course Image +
+

+ {course.title} +

+

+ {course.sort_description} +

+ +
+
+
+ ) + })} +
+
+ ) : ( +
+ )} +
+ ) +} + +export default CoursesSilder; + diff --git a/components/CustomButton.tsx b/components/CustomButton.tsx new file mode 100644 index 0000000..e09ed05 --- /dev/null +++ b/components/CustomButton.tsx @@ -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 ( + + ) +} + +export default CustomButton diff --git a/components/Footer.tsx b/components/Footer.tsx new file mode 100644 index 0000000..6ff328d --- /dev/null +++ b/components/Footer.tsx @@ -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 ( +
+
+
+ + logo + +
+ + Copyright @ 2024 All In One All Rights Reserved. + +
+
+
window.open(settings.facebook)}> + +
+
window.open(settings.instagram)}> + +
+
window.open(settings.youtube)}> + +
+
+
+
+
+ + + ) +} + +export default Footer diff --git a/components/Hero1.tsx b/components/Hero1.tsx new file mode 100644 index 0000000..07c027b --- /dev/null +++ b/components/Hero1.tsx @@ -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([]); + const scrollRef = useRef(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 ( +
+ {/* Existing content */} + +
+
+
+ 發掘你的 +
+
+
+ 音樂之路 +
+
+
+
+
+ {[ + "學音樂能陶冶性情,專注,舒緩壓力,放鬆心情。實際上是否做到呢?", + "我們認為音樂不單單是「睇五線譜」", + "我們可以透過不同方式", + "例如聆聽,唱歌,身體律動等學習音樂,令學習不再是沉悶艱難", + "真正做到「陶冶性情」", + "One & ALL Music 提供全面的音樂課程", + "我們相信每位學生都各有天賦", + "「誰都可發光 只要找對地方」", + "只要運用合適的教學法,同學必定能發揮所長", + " ", + " ", + ].map((text, index) => ( +

+ {text} +

+ ))} +
+
+ +
+
+
+ 我們認為 +
+
+
+
+
+

+ {"音樂不單單是「睇五線譜」,我們可以透過不同方式,例如聆聽唱歌,身體律動等學習音樂,令學習不再是沉悶艱難,真正做到「陶治性情」。"} +

+

+ One & ALL Music 提供全面的音樂課程,我們相信每位學生都各有天赋,

「誰都可發光 只要找對地方」,只要運用合適的教學法,同學必定能發揮所長。 +

+
+
+ { + console.log('Button clicked'); + }} /> +
+ +
+
+ + + +
+ ); +} +export default Hero1 diff --git a/components/Hero2.tsx b/components/Hero2.tsx new file mode 100644 index 0000000..9d4c91d --- /dev/null +++ b/components/Hero2.tsx @@ -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 ( +
+
+
+ +
+
+ ) +} + +export default Hero2 diff --git a/components/Home/Home.tsx b/components/Home/Home.tsx new file mode 100644 index 0000000..d42be10 --- /dev/null +++ b/components/Home/Home.tsx @@ -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([]); + + 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 ( +
+ {loading ? ( +
+ Loading...
}> + + +
+ ) : ( +
+ + + Loading...
}> + + + + +
+
+ )} +
+ ) +} +export default Home \ No newline at end of file diff --git a/components/Loading.tsx b/components/Loading.tsx new file mode 100644 index 0000000..448caa8 --- /dev/null +++ b/components/Loading.tsx @@ -0,0 +1,17 @@ +import React from 'react' + +const Loading = () => { + return ( +
+ +
+
+
+
+
+ +
+ ) +} + +export default Loading diff --git a/components/Navbar/MobileNav.tsx b/components/Navbar/MobileNav.tsx new file mode 100644 index 0000000..13c90ee --- /dev/null +++ b/components/Navbar/MobileNav.tsx @@ -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 ( +
+ {/*overlay*/} +
+ + {/* nav links*/} +
+ +
+ +

+ 主頁 +

+ +
+
+
setDropdownOpen(!dropdownOpen)} + > +

課程

+ {dropdownOpen ? : } +
+ {dropdownOpen && ( +
+ {courses?.map((course) => ( +
+ +

+ {course.title} +

+ +
+ ))} +
+ )} +
+ +
+ +

+ 關於我們 +

+ +
+ + +
+
+
+ ) +} + +export default MobileNav + diff --git a/components/Navbar/Nav.tsx b/components/Navbar/Nav.tsx new file mode 100644 index 0000000..b94759b --- /dev/null +++ b/components/Navbar/Nav.tsx @@ -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 ( +
+
+
+ + logo + +
+
+ +

+ {"主頁"} +

+ +
+ + {dropdownOpen && ( +
+
+ {courses.map((course) => ( + + {course.title} + + ))} +
+
+ )} +
+ +

+ {"關於我們"} +

+ +
+
+
+ {/*button*/} +
+
+
window.open(settings.facebook)} > + +
+
window.open(settings.instagram)} > + +
+ +
window.open(settings.youtube)} > + +
+
+ {/*burger*/} + {showNav ? ( + + ) : ( + + )} +
+ +
+
+ ) +} +export default Nav diff --git a/components/Navbar/ResponsiveNav.tsx b/components/Navbar/ResponsiveNav.tsx new file mode 100644 index 0000000..44c7c72 --- /dev/null +++ b/components/Navbar/ResponsiveNav.tsx @@ -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 ( +
+
+ ) +} +export default ResponsiveNav diff --git a/components/TextGradientScrollContext.tsx b/components/TextGradientScrollContext.tsx new file mode 100644 index 0000000..0e8a58d --- /dev/null +++ b/components/TextGradientScrollContext.tsx @@ -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; + range: number[]; +}; + +type WordType = { + children: React.ReactNode; + progress: MotionValue; + range: number[]; +}; + +type CharType = { + children: React.ReactNode; + progress: MotionValue; + range: number[]; +}; + +type TextGradientScrollContextType = { + textOpacity?: TextOpacityEnum; + type?: ViewTypeEnum; +}; + +const TextGradientScrollContext = createContext( + {} +); + +function useGradientScroll() { + const context = useContext(TextGradientScrollContext); + return context; +} + +export default function TextGradientScroll({ + text, + className, + type = "letter", + textOpacity = "soft", +}: TextGradientScrollType) { + const ref = useRef(null); + const { scrollYProgress } = useScroll({ + target: ref, + offset: ["start center", "end center"], + }); + + const words = text.split(" "); + + return ( + +

+ {words.map((word, i) => { + const start = i / words.length; + const end = start + 1 / words.length; + return type === "word" ? ( + + {word} + + ) : ( + + {word} + + ); + })} +

+
+ ); +} + +const Word = ({ children, progress, range }: WordType) => { + const opacity = useTransform(progress, range, [0, 1]); + + return ( + + {children} + + {children} + + + ); +}; + +const Letter = ({ children, progress, range }: LetterType) => { + if (typeof children === "string") { + const amount = range[1] - range[0]; + const step = amount / children.length; + + return ( + + {children.split("").map((char: string, i: number) => { + const start = range[0] + i * step; + const end = range[0] + (i + 1) * step; + return ( + + {char} + + ); + })} + + ); + } +}; + +const Char = ({ children, progress, range }: CharType) => { + const opacity = useTransform(progress, range, [0, 1]); + const { textOpacity } = useGradientScroll(); + + return ( + + + {children} + + + {children} + + + ); +}; diff --git a/components/Whatsapp.tsx b/components/Whatsapp.tsx new file mode 100644 index 0000000..597c984 --- /dev/null +++ b/components/Whatsapp.tsx @@ -0,0 +1,18 @@ +import React from 'react' +import { IoLogoWhatsapp } from "react-icons/io"; +import { SettingsProps } from '@/types'; +const Whatsapp = ({ settings }: { settings: SettingsProps }) => { + return ( +
+ +
+ ) +} + +export default Whatsapp diff --git a/components/slick.css b/components/slick.css new file mode 100644 index 0000000..b48ece5 --- /dev/null +++ b/components/slick.css @@ -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; + } \ No newline at end of file diff --git a/components/utils.ts b/components/utils.ts new file mode 100644 index 0000000..300f9c1 --- /dev/null +++ b/components/utils.ts @@ -0,0 +1,6 @@ +import { ClassValue, clsx } from 'clsx'; +import { twMerge } from 'tailwind-merge'; + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 2a38cb4..2761522 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 546fa53..2fd555a 100644 --- a/package.json +++ b/package.json @@ -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" } } diff --git a/public/images/014.png b/public/images/014.png new file mode 100644 index 0000000..4cd1ad0 Binary files /dev/null and b/public/images/014.png differ diff --git a/public/images/contact.png b/public/images/contact.png new file mode 100644 index 0000000..d83ba9d Binary files /dev/null and b/public/images/contact.png differ diff --git a/public/images/kid.png b/public/images/kid.png new file mode 100644 index 0000000..e1b752d Binary files /dev/null and b/public/images/kid.png differ diff --git a/public/images/kid2.png b/public/images/kid2.png new file mode 100644 index 0000000..2f27ec0 Binary files /dev/null and b/public/images/kid2.png differ diff --git a/public/images/line.png b/public/images/line.png new file mode 100644 index 0000000..06558cb Binary files /dev/null and b/public/images/line.png differ diff --git a/public/images/logo.png b/public/images/logo.png new file mode 100644 index 0000000..141fea0 Binary files /dev/null and b/public/images/logo.png differ diff --git a/public/images/piano2.png b/public/images/piano2.png new file mode 100644 index 0000000..a2cd934 Binary files /dev/null and b/public/images/piano2.png differ diff --git a/public/images/pianobackground.jpg b/public/images/pianobackground.jpg new file mode 100644 index 0000000..d6e85cf Binary files /dev/null and b/public/images/pianobackground.jpg differ diff --git a/public/images/pianobackground2.png b/public/images/pianobackground2.png new file mode 100644 index 0000000..b76aff2 Binary files /dev/null and b/public/images/pianobackground2.png differ diff --git a/public/themes.ts b/public/themes.ts new file mode 100644 index 0000000..d0fef51 --- /dev/null +++ b/public/themes.ts @@ -0,0 +1,7 @@ +export const colors = { + mainColor: "#D60050" +}; + +export default { + colors +}; diff --git a/tailwind.config.ts b/tailwind.config.ts index d43da91..34b1568 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -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)", }, diff --git a/types/index.ts b/types/index.ts new file mode 100644 index 0000000..9e5fb53 --- /dev/null +++ b/types/index.ts @@ -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; +} + +export interface imageProps { + image: string, + course_id: string, + index: number, + id: string +} + +export interface CoursesProps { + id: string, + title: string, + images: Array, + info_images: Array, + schedule: Array, + 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 +} + +export interface CoursesArrayProps { + courses: Array +} diff --git a/utils/index.ts b/utils/index.ts new file mode 100644 index 0000000..1b47827 --- /dev/null +++ b/utils/index.ts @@ -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; + } +} \ No newline at end of file