diff --git a/package-lock.json b/package-lock.json index cfc4eb1..a3bc073 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "@mui/icons-material": "^5.15.11", "@mui/lab": "^5.0.0-alpha.169", "@mui/material": "^5.15.11", + "@react-spring/web": "^9.7.3", "@reduxjs/toolkit": "^2.2.2", "@types/axios": "^0.14.0", "@types/node": "^20.11.24", @@ -22,6 +23,7 @@ "@types/react-avatar-editor": "^13.0.2", "axios": "^1.6.8", "framer-motion": "^11.0.8", + "gsap": "^3.12.5", "markdown-to-jsx": "^7.4.1", "notistack": "^3.0.1", "nprogress": "^0.2.0", @@ -32,6 +34,7 @@ "react-helmet-async": "^2.0.4", "react-redux": "^9.1.0", "react-router-dom": "^6.22.2", + "react-use-measure": "^2.1.1", "redux-persist": "^6.0.0", "reselect": "^5.1.0", "simplebar-react": "^3.2.4", @@ -1687,6 +1690,66 @@ "url": "https://opencollective.com/popperjs" } }, + "node_modules/@react-spring/animated": { + "version": "9.7.3", + "resolved": "https://registry.npmjs.org/@react-spring/animated/-/animated-9.7.3.tgz", + "integrity": "sha512-5CWeNJt9pNgyvuSzQH+uy2pvTg8Y4/OisoscZIR8/ZNLIOI+CatFBhGZpDGTF/OzdNFsAoGk3wiUYTwoJ0YIvw==", + "dependencies": { + "@react-spring/shared": "~9.7.3", + "@react-spring/types": "~9.7.3" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/@react-spring/core": { + "version": "9.7.3", + "resolved": "https://registry.npmjs.org/@react-spring/core/-/core-9.7.3.tgz", + "integrity": "sha512-IqFdPVf3ZOC1Cx7+M0cXf4odNLxDC+n7IN3MDcVCTIOSBfqEcBebSv+vlY5AhM0zw05PDbjKrNmBpzv/AqpjnQ==", + "dependencies": { + "@react-spring/animated": "~9.7.3", + "@react-spring/shared": "~9.7.3", + "@react-spring/types": "~9.7.3" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-spring/donate" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/@react-spring/shared": { + "version": "9.7.3", + "resolved": "https://registry.npmjs.org/@react-spring/shared/-/shared-9.7.3.tgz", + "integrity": "sha512-NEopD+9S5xYyQ0pGtioacLhL2luflh6HACSSDUZOwLHoxA5eku1UPuqcJqjwSD6luKjjLfiLOspxo43FUHKKSA==", + "dependencies": { + "@react-spring/types": "~9.7.3" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/@react-spring/types": { + "version": "9.7.3", + "resolved": "https://registry.npmjs.org/@react-spring/types/-/types-9.7.3.tgz", + "integrity": "sha512-Kpx/fQ/ZFX31OtlqVEFfgaD1ACzul4NksrvIgYfIFq9JpDHFwQkMVZ10tbo0FU/grje4rcL4EIrjekl3kYwgWw==" + }, + "node_modules/@react-spring/web": { + "version": "9.7.3", + "resolved": "https://registry.npmjs.org/@react-spring/web/-/web-9.7.3.tgz", + "integrity": "sha512-BXt6BpS9aJL/QdVqEIX9YoUy8CE6TJrU0mNCqSoxdXlIeNcEBWOfIyE6B14ENNsyQKS3wOWkiJfco0tCr/9tUg==", + "dependencies": { + "@react-spring/animated": "~9.7.3", + "@react-spring/core": "~9.7.3", + "@react-spring/shared": "~9.7.3", + "@react-spring/types": "~9.7.3" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/@reduxjs/toolkit": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.2.2.tgz", @@ -2862,6 +2925,11 @@ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" }, + "node_modules/debounce": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz", + "integrity": "sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==" + }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -3519,6 +3587,11 @@ "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "dev": true }, + "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==" + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -4410,6 +4483,18 @@ "react-dom": ">=16.6.0" } }, + "node_modules/react-use-measure": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/react-use-measure/-/react-use-measure-2.1.1.tgz", + "integrity": "sha512-nocZhN26cproIiIduswYpV5y5lQpSQS1y/4KuvUCjSKmw7ZWIS/+g3aFnX3WdBkyuGUtTLif3UTqnLLhbDoQig==", + "dependencies": { + "debounce": "^1.2.1" + }, + "peerDependencies": { + "react": ">=16.13", + "react-dom": ">=16.13" + } + }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -6018,6 +6103,49 @@ "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==" }, + "@react-spring/animated": { + "version": "9.7.3", + "resolved": "https://registry.npmjs.org/@react-spring/animated/-/animated-9.7.3.tgz", + "integrity": "sha512-5CWeNJt9pNgyvuSzQH+uy2pvTg8Y4/OisoscZIR8/ZNLIOI+CatFBhGZpDGTF/OzdNFsAoGk3wiUYTwoJ0YIvw==", + "requires": { + "@react-spring/shared": "~9.7.3", + "@react-spring/types": "~9.7.3" + } + }, + "@react-spring/core": { + "version": "9.7.3", + "resolved": "https://registry.npmjs.org/@react-spring/core/-/core-9.7.3.tgz", + "integrity": "sha512-IqFdPVf3ZOC1Cx7+M0cXf4odNLxDC+n7IN3MDcVCTIOSBfqEcBebSv+vlY5AhM0zw05PDbjKrNmBpzv/AqpjnQ==", + "requires": { + "@react-spring/animated": "~9.7.3", + "@react-spring/shared": "~9.7.3", + "@react-spring/types": "~9.7.3" + } + }, + "@react-spring/shared": { + "version": "9.7.3", + "resolved": "https://registry.npmjs.org/@react-spring/shared/-/shared-9.7.3.tgz", + "integrity": "sha512-NEopD+9S5xYyQ0pGtioacLhL2luflh6HACSSDUZOwLHoxA5eku1UPuqcJqjwSD6luKjjLfiLOspxo43FUHKKSA==", + "requires": { + "@react-spring/types": "~9.7.3" + } + }, + "@react-spring/types": { + "version": "9.7.3", + "resolved": "https://registry.npmjs.org/@react-spring/types/-/types-9.7.3.tgz", + "integrity": "sha512-Kpx/fQ/ZFX31OtlqVEFfgaD1ACzul4NksrvIgYfIFq9JpDHFwQkMVZ10tbo0FU/grje4rcL4EIrjekl3kYwgWw==" + }, + "@react-spring/web": { + "version": "9.7.3", + "resolved": "https://registry.npmjs.org/@react-spring/web/-/web-9.7.3.tgz", + "integrity": "sha512-BXt6BpS9aJL/QdVqEIX9YoUy8CE6TJrU0mNCqSoxdXlIeNcEBWOfIyE6B14ENNsyQKS3wOWkiJfco0tCr/9tUg==", + "requires": { + "@react-spring/animated": "~9.7.3", + "@react-spring/core": "~9.7.3", + "@react-spring/shared": "~9.7.3", + "@react-spring/types": "~9.7.3" + } + }, "@reduxjs/toolkit": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.2.2.tgz", @@ -6775,6 +6903,11 @@ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" }, + "debounce": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz", + "integrity": "sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==" + }, "debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -7260,6 +7393,11 @@ "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "dev": true }, + "gsap": { + "version": "3.12.5", + "resolved": "https://registry.npmjs.org/gsap/-/gsap-3.12.5.tgz", + "integrity": "sha512-srBfnk4n+Oe/ZnMIOXt3gT605BX9x5+rh/prT2F1SsNJsU1XuMiP0E2aptW481OnonOGACZWBqseH5Z7csHxhQ==" + }, "has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -7874,6 +8012,14 @@ "prop-types": "^15.6.2" } }, + "react-use-measure": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/react-use-measure/-/react-use-measure-2.1.1.tgz", + "integrity": "sha512-nocZhN26cproIiIduswYpV5y5lQpSQS1y/4KuvUCjSKmw7ZWIS/+g3aFnX3WdBkyuGUtTLif3UTqnLLhbDoQig==", + "requires": { + "debounce": "^1.2.1" + } + }, "readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", diff --git a/package.json b/package.json index 8917389..34a2c60 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "@mui/icons-material": "^5.15.11", "@mui/lab": "^5.0.0-alpha.169", "@mui/material": "^5.15.11", + "@react-spring/web": "^9.7.3", "@reduxjs/toolkit": "^2.2.2", "@types/axios": "^0.14.0", "@types/node": "^20.11.24", @@ -25,6 +26,7 @@ "@types/react-avatar-editor": "^13.0.2", "axios": "^1.6.8", "framer-motion": "^11.0.8", + "gsap": "^3.12.5", "markdown-to-jsx": "^7.4.1", "notistack": "^3.0.1", "nprogress": "^0.2.0", @@ -35,6 +37,7 @@ "react-helmet-async": "^2.0.4", "react-redux": "^9.1.0", "react-router-dom": "^6.22.2", + "react-use-measure": "^2.1.1", "redux-persist": "^6.0.0", "reselect": "^5.1.0", "simplebar-react": "^3.2.4", diff --git a/public/asset/svg/tccp-vector-p.svg b/public/asset/svg/tccp-vector-p.svg new file mode 100644 index 0000000..9789504 --- /dev/null +++ b/public/asset/svg/tccp-vector-p.svg @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/app.tsx b/src/app.tsx index 17bbb82..14dbbde 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -1,7 +1,6 @@ import { Fragment, useEffect } from 'react' import { BrowserRouter } from 'react-router-dom' import { HelmetProvider } from 'react-helmet-async' -import { AppNavBar, AppFooter } from 'component' import { Fallback } from 'page' import { AuthProvider } from 'auth/auth-provider' import { SettingProvider } from 'component/setting' diff --git a/src/component/index.ts b/src/component/index.ts index 4e9d755..7a60b2f 100644 --- a/src/component/index.ts +++ b/src/component/index.ts @@ -17,3 +17,5 @@ export { default as Typography } from './typography/typography' export { default as Markdown } from './markdown/markdown' // meta export { default as Meta } from './meta/meta' +// loading screen +export { default as LoadingScreen } from './loading-screen/loading-screen' diff --git a/src/component/loading-screen/animated-brand-logo.tsx b/src/component/loading-screen/animated-brand-logo.tsx new file mode 100644 index 0000000..59be03f --- /dev/null +++ b/src/component/loading-screen/animated-brand-logo.tsx @@ -0,0 +1,68 @@ +const AnimatedBrandLogo = ({ worldRef }: { worldRef: React.RefObject }) => { + return ( +
+ + + + + + + + + + + + + + + + +
+ ) +} + +export default AnimatedBrandLogo diff --git a/src/component/loading-screen/index.ts b/src/component/loading-screen/index.ts index c29e9ad..b4a525a 100644 --- a/src/component/loading-screen/index.ts +++ b/src/component/loading-screen/index.ts @@ -1 +1,2 @@ export { default as LoadingScreen } from './loading-screen' +export { default as AnimatedBrandLogo } from './animated-brand-logo' diff --git a/src/component/loading-screen/loading-screen.tsx b/src/component/loading-screen/loading-screen.tsx index dcb53c8..501d434 100644 --- a/src/component/loading-screen/loading-screen.tsx +++ b/src/component/loading-screen/loading-screen.tsx @@ -1,93 +1,99 @@ +import React, { useEffect, useRef, useState } from 'react' import { m } from 'framer-motion' -import { useLocation } from 'react-router-dom' -import { alpha, styled } from '@mui/material/styles' -import { Box, LinearProgress } from '@mui/material' +import { gsap } from 'gsap' import { useResponsive } from 'hook' -import ProgressBar from './progress-bar' +import { useSpring, config, animated } from '@react-spring/web' +import useMeasure from 'react-use-measure' +import { Box, Typography, Stack } from '@mui/material' +import { AnimatedBrandLogo } from 'component/loading-screen' +import { KEY, TYPOGRAPHY } from 'constant' +import { SRoot } from './style' +import styles from './styles.module.css' +import { GLOBAL } from 'config' -const StyledRoot = styled('div')(({ theme }) => ({ - right: 0, - bottom: 0, - zIndex: 9998, - width: '100%', - height: '100%', - position: 'fixed', - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - backgroundColor: theme.palette.background.default -})) +const loadingPhrases = ['Loading...', 'Hang tight...', 'Just a moment...', 'Almost there...'] function LoadingScreen() { - const { pathname } = useLocation() + const [loadingProgress, setLoadingProgress] = useState(0) + const [phrase, setPhrase] = useState(loadingPhrases[0]) + const [open, toggle] = useState(false) + const [ref, { width }] = useMeasure() + const props = useSpring({ width }) + const props2 = useSpring({ + from: { x: 0, y: 0, z: 0 }, + to: { x: 1, y: 1, z: 1 } + }) + + const [parallaxProps, setParallaxProps] = useSpring(() => ({ + xy: [0, 0], + config: config.wobbly + })) + const isDesktop = useResponsive({ query: 'up', start: 'md', end: 'xl' }) - const size = 128 + const worldRef = useRef(null) + + const handleLoadingProgress = (progress: number) => { + setLoadingProgress(progress) + } + + useEffect(() => { + const world = worldRef.current as SVGSVGElement | null + const tl = gsap.timeline({ repeat: -1, yoyo: true }) + + tl.to(world, { rotation: 360, duration: 5, ease: 'power2.inOut' }).to(world, { rotationY: 360, duration: 3, ease: 'power2.inOut' }) + + return () => { + tl.kill() + } + }, []) + + useEffect(() => { + const interval = setInterval(() => { + setPhrase(loadingPhrases[Math.floor(Math.random() * loadingPhrases.length)]) + }, 2000) + + return () => clearInterval(interval) + }, []) + + const handleMouseMove = (event: any) => { + const { clientX, clientY } = event + const offsetX = (clientX - window.innerWidth / 2) / 50 + const offsetY = (clientY - window.innerHeight / 2) / 50 + setParallaxProps({ xy: [offsetX, offsetY] }) + } return ( - <> - - + - ( - <> - - - `solid 3px ${alpha(theme.palette.primary.dark, 0.24)}` - }} - /> + `translate3d(${x}px, ${y}px, 0)`) + }}> + + - `solid 8px ${alpha(theme.palette.primary.dark, 0.24)}` - }} - /> - - ) - - + + {GLOBAL.APP_NAME} + + + {phrase} + + + toggle(!open)}> + + {props.width.to((x) => x.toFixed(0))} + + + + ) } diff --git a/src/component/loading-screen/progress-bar.tsx b/src/component/loading-screen/progress-bar.tsx deleted file mode 100644 index 3b8fab9..0000000 --- a/src/component/loading-screen/progress-bar.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { useEffect, useMemo } from 'react' -import NProgress from 'nprogress' -import SProgressBar from './style' - -export default function ProgressBar() { - NProgress.configure({ - showSpinner: false, - }) - - useMemo(() => { - NProgress.start() - }, []) - - useEffect(() => { - NProgress.done() - }, []) - - return -} diff --git a/src/component/loading-screen/style.tsx b/src/component/loading-screen/style.tsx index d23c76b..40b674d 100644 --- a/src/component/loading-screen/style.tsx +++ b/src/component/loading-screen/style.tsx @@ -1,40 +1,27 @@ -import { useTheme } from '@mui/material/styles' -import GlobalStyles from '@mui/material/GlobalStyles' +import { m } from 'framer-motion' +import { styled } from '@mui/material/styles' +import { Box } from '@mui/material' -function SProgressBar() { - const theme = useTheme() +export const SBox = styled(Box)(({ theme }) => ({ + height: '100vh', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + backgroundColor: theme.palette.grey[900], + color: theme.palette.common.white +})) - const inputGlobalStyles = ( - - ) +export const SContainerBox = styled(Box)(({ theme }) => ({ + position: 'relative', + width: '300px', + height: '300px' +})) - return inputGlobalStyles -} - -export default SProgressBar +export const SRoot = styled(m.div)(({ theme }) => ({ + height: '100vh', + display: 'flex', + flexDirection: 'row', + justifyContent: 'center', + alignItems: 'center', + backgroundColor: theme.palette.background.default +})) diff --git a/src/component/loading-screen/styles.module.css b/src/component/loading-screen/styles.module.css new file mode 100644 index 0000000..33f2fe0 --- /dev/null +++ b/src/component/loading-screen/styles.module.css @@ -0,0 +1,41 @@ +.main { + position: relative; + width: 100px; + height: 30px; + cursor: pointer; + border-radius: 5px; + border: 2px solid #272727; + overflow: hidden; +} + +.fill { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: #ffd500; +} + +.content { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + color: #272727; +} + +.container { + display: flex; + align-items: center; + height: 100%; + justify-content: center; +} + +.no-animation { + animation: none !important; /* Disable animation */ +} diff --git a/src/route/element.tsx b/src/route/element.tsx index 1ec5f0d..d2e37d4 100644 --- a/src/route/element.tsx +++ b/src/route/element.tsx @@ -1,5 +1,5 @@ import { Suspense, lazy, FC, ComponentType } from 'react' -import { LoadingScreen } from 'component/loading-screen' +import { LoadingScreen } from 'component' import { MotionLazyContainer } from 'component/motion' const Loadable = (Component: () => Promise<{ default: ComponentType }>): FC => { diff --git a/src/section/bootcamp/view/view.tsx b/src/section/bootcamp/view/view.tsx index 8fc9661..aa30497 100644 --- a/src/section/bootcamp/view/view.tsx +++ b/src/section/bootcamp/view/view.tsx @@ -1,23 +1,8 @@ import { FC, useEffect } from 'react' import { useParams, Link } from 'react-router-dom' import { ICON_NAME, useIcon } from 'hook' -import { useGetBootcampQuery, useGetAllCourseByBootcampQuery } from 'store/slice' -import { - Box, - Grid, - Typography, - Container, - CardMedia, - Chip, - CardContent, - Divider, - Accordion, - AccordionSummary, - AccordionDetails, - List, - ListItem, - ListItemText -} from '@mui/material' +import { useGetBootcampQuery, useGetAllCourseByBootcampQuery, useGetCourseQuery } from 'store/slice' +import { Box, Grid, Typography, Container, CardMedia, Chip, CardContent, Divider } from '@mui/material' import { BackButton } from 'component' import { GSContainerGrid, SSpanBox } from 'theme/style' import { MotionLazyContainer } from 'component/motion' @@ -47,15 +32,16 @@ const BootcampViewSection: FC = () => { duration, phone, website, + course, slug, housing, jobAssistance, jobGuarantee } = bootcamp?.data || {} - const courseQueryResult = useGetAllCourseByBootcampQuery(_id) - - const { data: course, error: courseError, isLoading: courseLoading } = courseQueryResult || { data: null, error: null, isLoading: false } + // const courseQueryResult = useGetAllCourseByBootcampQuery(_id) + // const courseId = course && course.length !== 0 && course?.map((course: any) => course._id) + // const { data: course, error: courseError, isLoading: courseLoading } = useGetAllCourseByBootcampQuery(_id) const { Icon: PhoneIcon, iconSrc: phoneSrc } = useIcon(ICON_WEB_NAME.PHONE) const { Icon: RightIcon, iconSrc: rightSrc } = useIcon(ICON_WEB_NAME.CHEVRON_RIGHT) @@ -234,8 +220,8 @@ const BootcampViewSection: FC = () => { {LABEL.COURSE_COVERED} - {course?.data?.length > 0 ? ( - + {course?.length !== 0 && course ? ( + ) : ( {PLACEHOLDER.NO_COURSE_AVAILABLE} )}