Skip to content

Commit 753b816

Browse files
author
Ahtesham Quraish
committed
add carousel for courses
1 parent 9b9e10a commit 753b816

File tree

6 files changed

+344
-21
lines changed

6 files changed

+344
-21
lines changed

frontends/ol-ckeditor/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99
"api": "workspace:*",
1010
"ckeditor5": "^42.0.0",
1111
"next": "^15.5.2",
12-
"ol-components": "0.0.0"
12+
"ol-components": "0.0.0",
13+
"swiper": "^12.0.3"
1314
},
1415
"peerDependencies": {
1516
"react": ">=18.0.0",
Lines changed: 65 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,86 @@
11
"use client"
22

3-
import React from "react"
3+
import React, { useEffect, useState } from "react"
44
import { useArticleDetail } from "api/hooks/articles"
55
import "ckeditor5/ckeditor5.css"
66
import "./styles.css"
77

8+
import { Swiper, SwiperSlide } from "swiper/react"
9+
import { Navigation, Pagination, Autoplay } from "swiper/modules"
10+
11+
import "swiper/css"
12+
import "swiper/css/navigation"
13+
import "swiper/css/pagination"
14+
815
export const ArticleDetail = ({ articleId }: { articleId: number }) => {
916
const id = Number(articleId)
1017
const { data, isLoading } = useArticleDetail(id)
1118

12-
if (isLoading) {
13-
return (
14-
<div className="article-detail-container">
15-
<div className="loader">Loading article...</div>
16-
</div>
17-
)
18-
}
19-
20-
if (!data) {
21-
return (
22-
<div className="article-detail-container">
23-
<p className="empty">Article not found.</p>
24-
</div>
25-
)
26-
}
19+
const [cleanHtml, setCleanHtml] = useState("")
20+
const [carouselItems, setCarouselItems] = useState<string[]>([])
21+
22+
useEffect(() => {
23+
if (!data?.html) return
24+
25+
const parser = new DOMParser()
26+
const doc = parser.parseFromString(data.html, "text/html")
27+
28+
const cards = Array.from(doc.querySelectorAll(".carousel-course-card"))
29+
30+
// ✅ Add new class to each card before storing HTML
31+
cards.forEach((el) => {
32+
el.classList.add("carousel-slide-item")
33+
})
34+
35+
setCarouselItems(cards.map((el) => el.outerHTML))
36+
cards.forEach((el) => el.remove())
37+
38+
setCleanHtml(doc.body.innerHTML)
39+
}, [data])
40+
41+
if (isLoading) return <div>Loading article...</div>
42+
if (!data) return <div>Article not found.</div>
2743

2844
return (
2945
<div className="article-detail-container">
3046
<h1 className="article-title">{data.title}</h1>
3147

32-
{/* Render article HTML */}
48+
{/* ✅ Swiper slider */}
49+
{carouselItems.length > 0 && (
50+
<div className="course-carousel-wrapper">
51+
<Swiper
52+
modules={[Navigation, Pagination, Autoplay]}
53+
spaceBetween={16}
54+
slidesPerView={1.2}
55+
navigation
56+
pagination={{ clickable: true }}
57+
autoplay={{
58+
delay: 2500,
59+
disableOnInteraction: false,
60+
}}
61+
breakpoints={{
62+
640: { slidesPerView: 2.2 },
63+
1024: { slidesPerView: 3.2 },
64+
}}
65+
>
66+
{carouselItems.map((item, idx) => (
67+
<SwiperSlide key={idx}>
68+
<div
69+
className="course-card-wrapper"
70+
dangerouslySetInnerHTML={{ __html: item }}
71+
/>
72+
</SwiperSlide>
73+
))}
74+
</Swiper>
75+
</div>
76+
)}
77+
3378
<div
3479
className="ck-content"
35-
dangerouslySetInnerHTML={{ __html: data.html }}
80+
dangerouslySetInnerHTML={{ __html: cleanHtml }}
3681
/>
3782
</div>
3883
)
3984
}
85+
86+
export default ArticleDetail

frontends/ol-ckeditor/src/components/CKEditorClient.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@ export const CKEditorClient: React.FC<CKEditorClientProps> = ({
3333
// --- Core editor and modal states
3434
// eslint-disable-next-line @typescript-eslint/no-explicit-any
3535
const [EditorModules, setEditorModules] = useState<any>(null)
36-
console.log("EditorModules====================:", value)
3736
const [data, setData] = useState(value || "")
3837
const [open, setOpen] = useState(false)
3938
const [isLoader, setIsLoader] = useState(false)
@@ -134,6 +133,12 @@ export const CKEditorClient: React.FC<CKEditorClientProps> = ({
134133
)
135134
const InsertCoursePlugin = createInsertCoursePlugin(CKEditorModules)
136135

136+
const { createInsertCourseCarouselPlugin } = await import(
137+
"./InsertCourseCarouselPlugin"
138+
)
139+
const InsertCourseCarouselPlugin =
140+
createInsertCourseCarouselPlugin(CKEditorModules)
141+
137142
setEditorModules({
138143
ClassicEditor,
139144
Essentials,
@@ -171,6 +176,7 @@ export const CKEditorClient: React.FC<CKEditorClientProps> = ({
171176
MediaFloatPlugin,
172177
DefaultImageStylePlugin,
173178
InsertCoursePlugin,
179+
InsertCourseCarouselPlugin,
174180
_CKEditorModules: CKEditorModules,
175181
})
176182
} catch (error) {
@@ -229,6 +235,7 @@ export const CKEditorClient: React.FC<CKEditorClientProps> = ({
229235
EditorModules.MediaFloatPlugin,
230236
EditorModules.DefaultImageStylePlugin,
231237
EditorModules.InsertCoursePlugin,
238+
EditorModules.InsertCourseCarouselPlugin,
232239
],
233240
toolbar: {
234241
items: [
@@ -264,6 +271,7 @@ export const CKEditorClient: React.FC<CKEditorClientProps> = ({
264271
"|",
265272
"insertCourse",
266273
"|",
274+
"insertCourseCarousel",
267275
],
268276
},
269277
alignment: {

frontends/ol-ckeditor/src/components/CkeditorArticle.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ const CkeditorArticle = ({ articleId }: { articleId: number }) => {
4848
})
4949
}
5050
// if (isLoading) return <div className="loading">Loading...</div>
51+
console.log("editorContent==================:", editorContent)
5152
return (
5253
<div className="article-wrapper">
5354
<h1 className="article-title">Create Article</h1>
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
// InsertCourseCarouselPlugin.js
2+
// NOTE: adapt import / export to your project module system if needed.
3+
4+
const MOCK_COURSES = [
5+
{
6+
id: "1",
7+
title: "Course A",
8+
image:
9+
"https://35904.cdn.cke-cs.com/A8zpYq0deQ8s5ZchTmUI/images/01c35ba4c768c8465ec1563610744ebc25567078532853aa.webp",
10+
description: "Intro to topic A",
11+
},
12+
{
13+
id: "2",
14+
title: "Course B",
15+
image:
16+
"https://35904.cdn.cke-cs.com/A8zpYq0deQ8s5ZchTmUI/images/fe330022e8ac9bdb0ddb483adb64448cd2d44b2fd976fd27.jpg",
17+
description: "Deep dive into B",
18+
},
19+
]
20+
21+
export function createInsertCourseCarouselPlugin(CKEditorModules) {
22+
const { Plugin, Command, ButtonView } = CKEditorModules
23+
24+
class InsertCarouselCommand extends Command {
25+
execute(courses = []) {
26+
const editor = this.editor
27+
28+
// ✅ Prevent double execution
29+
if (this._isExecuting) return
30+
this._isExecuting = true
31+
setTimeout(() => (this._isExecuting = false), 0)
32+
33+
editor.model.change((writer) => {
34+
const carousel = writer.createElement("courseCarousel")
35+
36+
for (const course of courses) {
37+
const item = writer.createElement("courseItem", {
38+
id: course.id,
39+
title: course.title,
40+
image: course.image,
41+
description: course.description,
42+
})
43+
console.log("Inserting course item:", course)
44+
writer.append(item, carousel)
45+
}
46+
47+
const root = editor.model.document.getRoot()
48+
const pos = writer.createPositionAt(root, "end")
49+
writer.insert(carousel, pos)
50+
51+
const paragraph = writer.createElement("paragraph")
52+
writer.insert(paragraph, writer.createPositionAfter(carousel))
53+
writer.setSelection(paragraph, "in")
54+
})
55+
}
56+
}
57+
58+
class InsertCourseCarouselPlugin extends Plugin {
59+
init() {
60+
const editor = this.editor
61+
62+
// -------- 1) Schema ----------
63+
editor.model.schema.register("courseCarousel", {
64+
allowWhere: "$block",
65+
isObject: true,
66+
isBlock: true,
67+
})
68+
69+
editor.model.schema.register("courseItem", {
70+
allowIn: "courseCarousel",
71+
allowAttributes: ["title", "image", "description"],
72+
})
73+
74+
// -------- 2) Editing downcast (model -> editing view) ----------
75+
// courseCarousel -> <div class="course-carousel"> ...cards...</div>
76+
77+
// ✅ Prevent double UI (placeholder only, no card here)
78+
79+
// -------- 3) Data downcast (model -> data/html) ----------
80+
// ✅ Editing downcast - SHOW ONLY A PREVIEW BOX, NO CARDS
81+
// ✅ Only preview in editor
82+
editor.conversion.for("editingDowncast").elementToElement({
83+
model: "courseCarousel",
84+
view: (modelElement, { writer: vWriter }) => {
85+
const count = [...modelElement.getChildren()].length
86+
87+
const container = vWriter.createContainerElement("div", {
88+
class: "course-carousel-editor-preview",
89+
style:
90+
"border:2px dashed #7aa2ff;padding:12px;margin:8px 0;background:#f7faff;border-radius:6px;",
91+
})
92+
93+
const label = vWriter.createContainerElement("div", {
94+
style: "font-size:13px;color:#3d6de4;",
95+
})
96+
97+
vWriter.insert(
98+
vWriter.createPositionAt(label, 0),
99+
vWriter.createText(`📚 Course Carousel (${count} items)`),
100+
)
101+
102+
vWriter.insert(vWriter.createPositionAt(container, "end"), label)
103+
return container
104+
},
105+
})
106+
107+
// ✅ Course items in editor = invisible anchors
108+
editor.conversion.for("editingDowncast").elementToElement({
109+
model: "courseItem",
110+
view: (modelElement, { writer: vWriter }) => {
111+
return vWriter.createEmptyElement("span", {
112+
"data-course-anchor": modelElement.getAttribute("title"),
113+
style: "display:none;",
114+
})
115+
},
116+
})
117+
118+
// -------- 4) Upcast (data/html -> model) ----------
119+
editor.conversion.for("upcast").elementToElement({
120+
view: { name: "div", classes: "course-carousel" },
121+
model: "courseCarousel",
122+
})
123+
124+
editor.conversion.for("upcast").elementToElement({
125+
view: {
126+
name: "div",
127+
classes: "carousel-course-card",
128+
},
129+
model: (viewElement, { writer: mWriter }) => {
130+
// ✅ Only accept cards that contain data attributes
131+
const title = viewElement.getAttribute("data-title")
132+
const image = viewElement.getAttribute("data-image")
133+
const description = viewElement.getAttribute("data-description")
134+
135+
if (!title || !image) {
136+
// ❌ This is a visual duplicate — ignore it
137+
return null
138+
}
139+
140+
return mWriter.createElement("courseItem", {
141+
title,
142+
image,
143+
description: description || "",
144+
})
145+
},
146+
})
147+
148+
// -------- 5) Command & UI button ----------
149+
editor.commands.add(
150+
"insertCourseCarousel",
151+
new InsertCarouselCommand(editor),
152+
)
153+
154+
editor.ui.componentFactory.add("insertCourseCarousel", (locale) => {
155+
const view = new ButtonView(locale)
156+
view.set({
157+
label: "Insert Courses",
158+
tooltip: true,
159+
withText: true,
160+
})
161+
162+
view.on("execute", (evt) => {
163+
evt.stop() // ✅ Prevent double execution from UI
164+
165+
const input = prompt("Enter course IDs (comma-separated, e.g. 1,2)")
166+
if (!input) return
167+
const ids = input.split(",").map((s) => s.trim())
168+
const courses = MOCK_COURSES.filter((c) => ids.includes(c.id))
169+
if (courses.length) {
170+
editor.execute("insertCourseCarousel", courses)
171+
}
172+
})
173+
174+
return view
175+
})
176+
}
177+
}
178+
179+
return InsertCourseCarouselPlugin
180+
}

0 commit comments

Comments
 (0)