diff --git a/.github/workflows/labels.yaml b/.github/workflows/labels.yaml index 5326acd4..083f2648 100644 --- a/.github/workflows/labels.yaml +++ b/.github/workflows/labels.yaml @@ -9,7 +9,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/setup-node@v2 + - uses: actions/setup-node@v3 with: node-version: "20" - uses: SpaceyaTech/gh-action-open-source-labels@main diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index d5d21b58..dd6fe1aa 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -9,7 +9,7 @@ jobs: timeout-minutes: 60 runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Prepare .env file run: | rm -f .env && touch .env @@ -17,7 +17,7 @@ jobs: echo "VITE_SERVICE_ID=123fAkE" >> .env echo "VITE_TEMPLATE_ID=123fAkE" >> .env echo "VITE_PUBLIC_ID=123fAkE" >> .env - - uses: actions/setup-node@v3 + - uses: actions/setup-node@v4 with: node-version: "20.x" - name: Install dependencies diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index e2f990ce..fb30bfe7 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -8,8 +8,8 @@ jobs: validate: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 with: node-version: "20.x" - name: Install dependencies diff --git a/package.json b/package.json index 9d53f342..4b3288db 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "react-hot-toast": "^2.4.1", "react-icons": "^5.2.1", "react-lazy-load-image-component": "^1.6.0", + "react-phone-number-input": "^3.4.12", "react-photo-album": "^2.3.0", "react-router-dom": "^6.11.2", "react-share": "^5.1.0", @@ -54,8 +55,8 @@ }, "devDependencies": { "@playwright/test": "^1.44.1", - "@testing-library/react": "^16.0.0", "@tailwindcss/typography": "^0.5.13", + "@testing-library/react": "^16.0.0", "@types/node": "^20.14.1", "@types/react": "^18.0.28", "@types/react-dom": "^18.0.11", diff --git a/public/confirmation-card.svg b/public/confirmation-card.svg new file mode 100644 index 00000000..0eab0867 --- /dev/null +++ b/public/confirmation-card.svg @@ -0,0 +1,19 @@ + diff --git a/public/discount.svg b/public/discount.svg new file mode 100644 index 00000000..40c637f0 --- /dev/null +++ b/public/discount.svg @@ -0,0 +1,5 @@ + diff --git a/src/assets/images/icons/duration.svg b/src/assets/images/icons/duration.svg new file mode 100644 index 00000000..d530b3f2 --- /dev/null +++ b/src/assets/images/icons/duration.svg @@ -0,0 +1,3 @@ + diff --git a/src/assets/images/icons/index.js b/src/assets/images/icons/index.js index cfa4eef4..76cd0faa 100644 --- a/src/assets/images/icons/index.js +++ b/src/assets/images/icons/index.js @@ -4,6 +4,7 @@ import briefcase from "./briefcase.svg"; import calendar from "./calendar.svg"; import check from "./check.svg"; import dotpoints from "./dotpoints.svg"; +import duration from "./duration.svg"; import folder from "./folder.svg"; import sytLogoGreen from "./logo-green-bg.svg"; import sytLogoWhite from "./logo-white-bg.svg"; @@ -18,6 +19,7 @@ export { calendar, check, dotpoints, + duration, folder, sytLogoGreen, sytLogoWhite, diff --git a/src/assets/images/mastercraft/mastercraft-hero-footer.png b/src/assets/images/mastercraft/mastercraft-hero-footer.png index 0d2e126c..4d28d7dd 100644 Binary files a/src/assets/images/mastercraft/mastercraft-hero-footer.png and b/src/assets/images/mastercraft/mastercraft-hero-footer.png differ diff --git a/src/assets/images/mastercraft/mastercraft-hero.png b/src/assets/images/mastercraft/mastercraft-hero.png deleted file mode 100644 index 0f546051..00000000 Binary files a/src/assets/images/mastercraft/mastercraft-hero.png and /dev/null differ diff --git a/src/assets/images/mastercraft/mastercraft-hero.svg b/src/assets/images/mastercraft/mastercraft-hero.svg new file mode 100644 index 00000000..c6d71074 --- /dev/null +++ b/src/assets/images/mastercraft/mastercraft-hero.svg @@ -0,0 +1,149 @@ + diff --git a/src/assets/images/resources-page/android.png b/src/assets/images/resources-page/android.png new file mode 100644 index 00000000..fb06d2c7 Binary files /dev/null and b/src/assets/images/resources-page/android.png differ diff --git a/src/assets/images/resources-page/backend.png b/src/assets/images/resources-page/backend.png index 1ecbdb2a..bdd31f25 100644 Binary files a/src/assets/images/resources-page/backend.png and b/src/assets/images/resources-page/backend.png differ diff --git a/src/assets/images/resources-page/frontend.png b/src/assets/images/resources-page/frontend.png index f56bf7a6..da865683 100644 Binary files a/src/assets/images/resources-page/frontend.png and b/src/assets/images/resources-page/frontend.png differ diff --git a/src/assets/images/resources-page/icons/twitter-x.svg b/src/assets/images/resources-page/icons/twitter-x.svg new file mode 100644 index 00000000..9ce405e4 --- /dev/null +++ b/src/assets/images/resources-page/icons/twitter-x.svg @@ -0,0 +1,3 @@ + diff --git a/src/assets/images/resources-page/index.js b/src/assets/images/resources-page/index.js index e04e4d58..9fb77244 100644 --- a/src/assets/images/resources-page/index.js +++ b/src/assets/images/resources-page/index.js @@ -1,3 +1,4 @@ +export { default as android } from "./android.png"; export { default as backend } from "./backend.png"; export { default as UI } from "./bad-good-UI.png"; export { default as UIDesignEra } from "./UIDesignEra.png"; @@ -6,7 +7,7 @@ export { default as frontend } from "./frontend.png"; export { default as hero } from "./hero.png"; export { default as resourceHero } from "./resource-hero.png"; export { default as heroFooter } from "./hero-footer.png"; -export { default as productDesign } from "./product-design.png"; +export { default as productDesign } from "./productDesign.png"; export { default as podPoster } from "./podcast-poster.png"; export { default as youtube } from "./icons/youtube.svg"; diff --git a/src/assets/images/resources-page/productDesign.png b/src/assets/images/resources-page/productDesign.png new file mode 100644 index 00000000..61b3ae2a Binary files /dev/null and b/src/assets/images/resources-page/productDesign.png differ diff --git a/src/components/Header.jsx b/src/components/Header.jsx index 3956ef1a..707fa594 100644 --- a/src/components/Header.jsx +++ b/src/components/Header.jsx @@ -29,23 +29,28 @@ const navLinks = [ link: "Products", route: "/products", }, - { - id: 5, - link: "Blogs", - route: "/blogs", - }, - { - id: 6, - link: "Resources", - route: "/resources", - }, + // { + // id: 5, + // link: "Blogs", + // route: "/blogs", + // }, + // { + // id: 6, + // link: "Resources", + // route: "/resources", + // }, { id: 7, - link: "Shop", - route: "/shop", + link: "Mastercraft", + route: "/mastercraft", }, // { // id: 8, + // link: "Shop", + // route: "/shop", + // }, + // { + // id: 9, // link: "Donate", // route: "/donate", // }, diff --git a/src/components/index.js b/src/components/index.js index 4a8d5804..729a89cf 100644 --- a/src/components/index.js +++ b/src/components/index.js @@ -2,7 +2,8 @@ export { default as Button } from "./Button"; export { default as Caroussel } from "./Caroussel"; export { default as CartDrawer } from "./shop/CartDrawer"; export { default as Counter } from "./shop/Counter"; -export { default as CurriculumAccordion } from "./CurriculumAccordion"; +export { default as CurriculumAccordion } from "../pages/mastercraft/sections/CurriculumAccordion"; +export { default as MastercraftFAQAccordion } from "../pages/mastercraft-home/sections/MastercraftFAQAccordion"; export { default as FAQ } from "./FAQ"; export { default as Footer } from "./Footer"; export { default as GoBackBtn } from "./GoBackBtn"; diff --git a/src/hooks/Mutations/mastercraft/useCheckPaymentStatus.jsx b/src/hooks/Mutations/mastercraft/useCheckPaymentStatus.jsx new file mode 100644 index 00000000..fc330f56 --- /dev/null +++ b/src/hooks/Mutations/mastercraft/useCheckPaymentStatus.jsx @@ -0,0 +1,131 @@ +import { useQuery } from "@tanstack/react-query"; +import { useCallback, useEffect, useRef, useState } from "react"; +import privateAxios from "../../../api/privateAxios"; + +const useCheckPaymentStatus = (enrollmentId, options = {}) => { + const { + enabled = false, + pollingInterval = 5000, + maxAttempts = 3, + isOpen = true, + } = options; + + const [status, setStatus] = useState({ + isSuccess: false, + isError: false, + isPending: true, + message: "", + errorCode: null, + }); + + const attemptsRef = useRef(0); + + const { data, isError, isSuccess, refetch, isLoading } = useQuery({ + queryKey: ["paymentStatus", enrollmentId], + queryFn: async () => { + if (!enrollmentId) return null; + try { + const response = await privateAxios.get(`/callback/${enrollmentId}/`); + return response.data; + } catch (error) { + if (error.response?.status === 400) { + setStatus({ + isSuccess: false, + isError: true, + isPending: false, + message: error.response.data?.message || "Payment failed", + errorCode: error.response.status, + }); + + return { + error: true, + message: error.response.data?.message || "Payment failed", + errorCode: error.response.status, + }; + } + throw error; + } + }, + enabled: + Boolean(enrollmentId) && + enabled && + attemptsRef.current < maxAttempts && + isOpen, + refetchInterval: status.isPending ? pollingInterval : false, + refetchOnWindowFocus: false, + retry: (failureCount, error) => { + if (error.response?.status === 400) { + return false; + } + if (attemptsRef.current >= maxAttempts) { + return false; + } + return failureCount < maxAttempts; + }, + }); + + useEffect(() => { + if ((isSuccess || isError) && !isLoading && status.isPending) { + attemptsRef.current += 1; + } + + if (attemptsRef.current >= maxAttempts && status.isPending) { + setStatus({ + isSuccess: false, + isError: true, + isPending: false, + message: "Payment verification timed out. Please contact support.", + errorCode: "TIMEOUT", + }); + return; + } + + if (isSuccess && data) { + if (data.error) { + setStatus({ + isSuccess: false, + isError: true, + isPending: false, + message: data.message || "Payment failed", + errorCode: data.errorCode || "PAYMENT_FAILED", + }); + } else { + setStatus({ + isSuccess: true, + isError: false, + isPending: false, + message: data.message || "Payment successful", + errorCode: null, + }); + } + } + }, [data, isSuccess, isError, isLoading, status.isPending, maxAttempts]); + + const checkStatus = useCallback(() => { + if (!enrollmentId) return null; + attemptsRef.current = 0; + setStatus({ + isSuccess: false, + isError: false, + isPending: true, + message: "", + errorCode: null, + }); + return refetch(); + }, [enrollmentId, refetch]); + + useEffect(() => { + if (enrollmentId && enabled && isOpen) { + checkStatus(); + } + }, [checkStatus, enabled, enrollmentId, isOpen]); + + return { + ...status, + data, + checkStatus, + attemptsCount: attemptsRef.current, + }; +}; + +export default useCheckPaymentStatus; diff --git a/src/hooks/Mutations/mastercraft/useEnroll.jsx b/src/hooks/Mutations/mastercraft/useEnroll.jsx new file mode 100644 index 00000000..a359375a --- /dev/null +++ b/src/hooks/Mutations/mastercraft/useEnroll.jsx @@ -0,0 +1,49 @@ +/* eslint-disable no-alert */ +/* eslint-disable no-console */ +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import privateAxios from "../../../api/privateAxios"; + +const useEnroll = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async (formData) => { + const response = await privateAxios.post( + "/mastercraft-enrollment/", + formData, + { + headers: { + "Content-Type": "application/json", + }, + } + ); + return response.data; + }, + mutationKey: ["enrollment"], + onSuccess: (data) => { + queryClient.invalidateQueries({ queryKey: ["enrollment"] }); + + return data; + }, + onError: (error) => { + const profileErrors = error?.response?.data?.profile; + + if (profileErrors && typeof profileErrors === "object") { + Object.entries(profileErrors).forEach(([field, messages]) => { + if (Array.isArray(messages)) { + messages.forEach((message) => { + console.error(`${field}: ${message}`); + alert(`${field}: ${message}`); + }); + } + }); + } else if (error?.response?.data?.program_name) { + console.error("Program name error:", error.response.data.program_name); + } else { + console.error("Enrollment failed with an unknown error", error); + } + }, + }); +}; + +export default useEnroll; diff --git a/src/index.css b/src/index.css index 34ded2a8..a2049747 100644 --- a/src/index.css +++ b/src/index.css @@ -2,6 +2,7 @@ @import url("https://fonts.googleapis.com/css2?family=Poppins:wght@200;500;600;700;800;900&display=swap"); @import url("https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300..700&display=swap"); @import url("https://fonts.googleapis.com/css2?family=Open+Sans:ital,wght@0,300..800;1,300..800&display=swap"); +@import url("https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap"); @tailwind base; @tailwind components; @@ -73,6 +74,11 @@ border-bottom: 2px solid #f3f4f5; } +.PhoneInputInput { + background-color: transparent; + outline: none; +} + @media (min-width: 768px) { .scrollbar::-webkit-scrollbar { width: 6px; diff --git a/src/index.js b/src/index.js index b223d4de..81cf6eed 100644 --- a/src/index.js +++ b/src/index.js @@ -72,6 +72,9 @@ const InventoryReport = lazy( const OrdersPage = lazy(() => import("./pages/admin/shop/OrdersPage")); const Mastercraft = lazy(() => import("./pages/mastercraft/Mastercraft")); +const MastercraftEnroll = lazy( + () => import("./pages/mastercraft-enroll/MastercraftEnroll") +); export { AboutUs, @@ -103,6 +106,7 @@ export { Layout, LogIn, Mastercraft, + MastercraftEnroll, OrdersPage, ProductDisplay, Products, diff --git a/src/main.jsx b/src/main.jsx index 9bbe2f50..0b05b17e 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -11,6 +11,7 @@ import { SearchBlogProvider } from "./context/searchBlog"; import router from "./router"; import { ErrorBoundary } from "."; import "react-lazy-load-image-component/src/effects/blur.css"; +import "react-phone-number-input/style.css"; const queryClient = new QueryClient({ defaultOptions: { diff --git a/src/pages/landingPage/sections/OurEvents.jsx b/src/pages/landingPage/sections/OurEvents.jsx index 4b57995d..b963b14b 100644 --- a/src/pages/landingPage/sections/OurEvents.jsx +++ b/src/pages/landingPage/sections/OurEvents.jsx @@ -1,9 +1,10 @@ import PropTypes from "prop-types"; import { useEffect } from "react"; +import { FiArrowRightCircle } from "react-icons/fi"; import { LazyLoadImage } from "react-lazy-load-image-component"; -import { Link } from "react-router-dom"; +import { Link, useNavigate } from "react-router-dom"; import { error500svg } from "../../../assets/images/errorPages"; -import { Button, Loader } from "../../../components"; +import { Loader } from "../../../components"; import useTopEvents from "../../../hooks/Queries/eventsSection/useTopEvents"; import { calculateDistanceToDate, @@ -20,6 +21,8 @@ function OurEvents() { refetch: refetchTopEvents, } = useTopEvents(""); + const navigate = useNavigate(); + useEffect(() => { refetchTopEvents(); }, [refetchTopEvents]); @@ -44,9 +47,20 @@ function OurEvents() {
No events found!
++ No events found! +
++ Mastercraft Internship ● 8 weeks +
++ 121 have already enrolled +
++ Mastercraft Program ● 8 weeks +
++ You will receive a payment prompt on this number +
+Quickly grasp the essentials and get your feet wet with our introductory course and jump straight into real world projects to - enable you to understand the basics alongside their implmentation + enable you to understand the basics alongside their implmentation{" "}
Lay the groundwork for the coming weeks with weekly learning @@ -103,7 +105,7 @@ function Features() {
@@ -145,7 +147,9 @@ function Features() { -
Nurture your skills with first-hand experience, working diff --git a/src/pages/mastercraft-home/sections/HeroSection.jsx b/src/pages/mastercraft-home/sections/HeroSection.jsx index 21be91d5..655385c3 100644 --- a/src/pages/mastercraft-home/sections/HeroSection.jsx +++ b/src/pages/mastercraft-home/sections/HeroSection.jsx @@ -1,7 +1,7 @@ import React from "react"; import { LazyLoadImage } from "react-lazy-load-image-component"; -import heroImg from "../../../assets/images/mastercraft/mastercraft-hero.png"; +import heroImg from "../../../assets/images/mastercraft/mastercraft-hero.svg"; import img3 from "../../../assets/Landing Page Images/Ellipse 127.png"; import img2 from "../../../assets/Landing Page Images/Ellipse 128.png"; import img4 from "../../../assets/Landing Page Images/Ellipse 148.png"; @@ -10,13 +10,13 @@ import img from "../../../assets/Landing Page Images/Ellipse 159.png"; function HeroSection() { return (
- Learn from the best teachers in the Kenyan tech market, horning your - craft with our completely free and comprehensive learning programme. +
+ Join us for an intensive 8-week, team-based training program that + immerses you in a collaborative project while you get expert + mentorship.
- Trusted by 600+ students + 600+ graduates to date
+ {description} +
+ ) : ( +{description}
+ +{description2}
} ++ Mastercraft is what every techie getting ready to interview for their + first role needs. The program is tailored to sharpen your technical + skills to see you ace technical and behavioral interviews. I am + speaking as a mentor in the program +
+ ++ {/*
{question.description} -
+ */} ++ {subTitle && {subTitle} } + + {content} +
++ {subTitle && {subTitle} } + + {content} +
+- Each cohort is taught by a team of UI/UX professionals who are - thriving in the industry as professional product designers. The - teachers will be supported by mentors who are equally professionals - in product design. + We have carefully selected mentors, with whom you will meet for 1:1 + sessions and group sessions, to unblock you and guide you on best + practices.
Pay at once. Save Ksh 2000
+Pay before {pricing.offerDeadline}
- Our Product Design Masterclasses tend to fill up pretty quickly + Our Mastercraft program openings tend to fill up pretty quickly which is why we open them months in advance. Live Classes are 90 minutes on Thursday and Saturday from 9:00 pm - 10:00pm.
@@ -317,10 +349,10 @@ function Description() {- Jumpstart your UI/UX design career with out comprehensive 2-month - boot camp on user experience and visual design. -
+{description}
- Next cohort opens on 15th September + Next cohort opens on {nextCohortDate}
- UX Design -
-- UX Design -
-- Prototyping -
-- Product Design -
+ {tags?.map((tag) => ( ++ {tag} +
+ ))}+ Experience:{" "} + {experience} years +
++
+