Skillpath courses component - React code component for Framer

SkillpathCourses.tsx

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,
},
})
添加评论
点赞收藏
点踩分享查看原文
评论
?
参与讨论