Skillpath courses component - React code component for Framer
| import { useState, useEffect } from "react" | |
| import { addPropertyControls, ControlType } from "framer" | |
| export default function SkillpathCourses(props) { | |
| const { accentColor, cardRadius } = props | |
| // States | |
| const [courses, setCourses] = useState(null) | |
| const [courseError, setCourseError] = useState(false) | |
| const [countryCode, setCountryCode] = useState(null) | |
| const [countryError, setCountryError] = useState(false) | |
| const [countryLoading, setCountryLoading] = useState(true) | |
| // ---- Fetch function (used on mount + retry) ---- | |
| function fetchData() { | |
| setCourseError(false) | |
| setCountryError(false) | |
| setCourses(null) | |
| setCountryCode(null) | |
| setCountryLoading(true) | |
| // Course API | |
| fetch("https://syncsphere-hiv6.onrender.com/assignment/course-data") | |
| .then((response) => { | |
| if (!response.ok) throw new Error("Course request failed") | |
| return response.json() | |
| }) | |
| .then((data) => setCourses(data)) | |
| .catch(() => setCourseError(true)) | |
| // Country API | |
| fetch("https://syncsphere-hiv6.onrender.com/assignment/country-code") | |
| .then((response) => { | |
| if (!response.ok) throw new Error("Country request failed") | |
| return response.json() | |
| }) | |
| .then((data) => { | |
| setCountryCode(data.country_code) | |
| setCountryLoading(false) | |
| }) | |
| .catch(() => { | |
| setCountryError(true) | |
| setCountryLoading(false) | |
| }) | |
| } | |
| useEffect(() => { | |
| fetchData() | |
| }, []) | |
| // Price formatting | |
| function formatPrice(course) { | |
| if (countryCode === "IN" || countryError) { | |
| const rupees = course.pricePaise / 100 | |
| return `₹${rupees.toLocaleString("en-IN")}` | |
| } | |
| const dollars = course.priceUsdCents / 100 | |
| return `${dollars.toLocaleString("en-US")}` | |
| } | |
| // Loading state | |
| if (courses === null && !courseError) { | |
| return ( | |
| Loading courses... | |
| </div> | |
| ) | |
| } | |
| // Error state with Retry button | |
| if (courseError) { | |
| return ( | |
| Couldn't load courses right now. | |
| <button | |
| onClick={fetchData} | |
| style={{ | |
| marginTop: 12, | |
| padding: "8px 16px", | |
| borderRadius: 6, | |
| border: "none", | |
| backgroundColor: accentColor, | |
| color: "white", | |
| cursor: "pointer", | |
| }} | |
| > | |
| Retry | |
| </button> | |
| </div> | |
| ) | |
| } | |
| // Empty results | |
| if (courses.length === 0) { | |
| return ( | |
| No courses available at the moment. | |
| </div> | |
| ) | |
| } | |
| // Country still loading | |
| if (countryLoading) { | |
| return ( | |
| Loading pricing... | |
| </div> | |
| ) | |
| } | |
| // Success | |
| return ( | |
| {countryError && ( | |
| <div | |
| style={{ | |
| marginBottom: 16, | |
| textAlign: "center", | |
| fontSize: 12, | |
| color: "#777", | |
| }} | |
| > | |
| Country detection failed. Showing INR pricing. | |
| </div> | |
| )} | |
| {courses.map((course) => ( | |
| <div | |
| key={course.mangoId} | |
| style={{ | |
| padding: 20, | |
| border: "1px solid #e5e5e5", | |
| borderRadius: cardRadius, | |
| background: "#ffffff", | |
| display: "flex", | |
| flexDirection: "column", | |
| minHeight: 220, | |
| boxSizing: "border-box", | |
| }} | |
| > | |
| <div | |
| style={{ | |
| fontSize: 12, | |
| fontWeight: 600, | |
| color: accentColor, | |
| marginBottom: 10, | |
| }} | |
| > | |
| {course.mainCategory} | |
| </div> | |
| <h3 | |
| style={{ margin: 0, fontSize: 20, lineHeight: 1.3 }} | |
| > | |
| {course.courseName} | |
| </h3> | |
| <p | |
| style={{ | |
| margin: "10px 0", | |
| fontSize: 14, | |
| lineHeight: 1.5, | |
| color: "#666", | |
| display: "-webkit-box", | |
| WebkitLineClamp: 2, | |
| WebkitBoxOrient: "vertical", | |
| overflow: "hidden", | |
| }} | |
| > | |
| {course.description} | |
| </p> | |
| <div | |
| style={{ | |
| marginTop: "auto", | |
| fontSize: 20, | |
| fontWeight: 700, | |
| color: accentColor, | |
| }} | |
| > | |
| {formatPrice(course)} | |
| </div> | |
| {course.refundable && ( | |
| <div | |
| style={{ | |
| marginTop: 10, | |
| fontSize: 12, | |
| color: "#218c4b", | |
| fontWeight: 600, | |
| }} | |
| > | |
| ✓ Refundable | |
| </div> | |
| )} | |
| </div> | |
| ))} | |
| </div> | |
| .skillpath-grid { | |
| display: grid; | |
| grid-template-columns: repeat(3, minmax(0, 1fr)); | |
| gap: 20px; | |
| } | |
| `}</style> | |
| </div> | |
| ) | |
| } | |
| // Property Controls | |
| addPropertyControls(SkillpathCourses, { | |
| accentColor: { | |
| type: ControlType.Color, | |
| title: "Accent Color", | |
| defaultValue: "#0055FF", | |
| }, | |
| cardRadius: { | |
| type: ControlType.Number, | |
| title: "Card Radius", | |
| defaultValue: 12, | |
| min: 0, | |
| max: 30, | |
| step: 1, | |
| }, | |
| }) |
评论
?
参与讨论