@academy-sdk/sdk 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/atoms/index.cjs +318 -0
- package/dist/components/atoms/index.js +288 -0
- package/dist/components/index.cjs +1275 -0
- package/dist/components/index.js +1245 -0
- package/dist/components/molecules/index.cjs +334 -0
- package/dist/components/molecules/index.js +311 -0
- package/dist/components/organisms/index.cjs +855 -0
- package/dist/components/organisms/index.js +825 -0
- package/dist/components/pages/index.cjs +3306 -0
- package/dist/components/pages/index.js +3315 -0
- package/dist/contracts/index.cjs +52 -0
- package/dist/contracts/index.js +29 -0
- package/dist/hooks/index.cjs +165 -0
- package/dist/hooks/index.js +142 -0
- package/dist/index.cjs +630 -0
- package/dist/index.js +600 -0
- package/dist/types/index.cjs +18 -0
- package/dist/types/index.js +0 -0
- package/dist/utils/index.cjs +80 -0
- package/dist/utils/index.js +57 -0
- package/package.json +91 -0
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/components/molecules/index.ts
|
|
21
|
+
var molecules_exports = {};
|
|
22
|
+
__export(molecules_exports, {
|
|
23
|
+
CourseCard: () => CourseCard,
|
|
24
|
+
EmptyState: () => EmptyState,
|
|
25
|
+
LoadingSpinner: () => LoadingSpinner,
|
|
26
|
+
PageHeader: () => PageHeader,
|
|
27
|
+
Pagination: () => Pagination,
|
|
28
|
+
SearchInput: () => SearchInput
|
|
29
|
+
});
|
|
30
|
+
module.exports = __toCommonJS(molecules_exports);
|
|
31
|
+
|
|
32
|
+
// src/components/molecules/CourseCard.tsx
|
|
33
|
+
var import_react = require("react");
|
|
34
|
+
var import_lucide_react = require("lucide-react");
|
|
35
|
+
|
|
36
|
+
// src/components/utils.ts
|
|
37
|
+
var import_clsx = require("clsx");
|
|
38
|
+
var import_tailwind_merge = require("tailwind-merge");
|
|
39
|
+
function cn(...inputs) {
|
|
40
|
+
return (0, import_tailwind_merge.twMerge)((0, import_clsx.clsx)(inputs));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// src/components/molecules/CourseCard.tsx
|
|
44
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
45
|
+
function CourseCard({
|
|
46
|
+
id,
|
|
47
|
+
title,
|
|
48
|
+
thumbnail,
|
|
49
|
+
thumbnails,
|
|
50
|
+
totalLessons,
|
|
51
|
+
progress,
|
|
52
|
+
status,
|
|
53
|
+
isFree,
|
|
54
|
+
price = 0,
|
|
55
|
+
currency = "USD",
|
|
56
|
+
description,
|
|
57
|
+
showPrice = true,
|
|
58
|
+
showProgress = true,
|
|
59
|
+
category,
|
|
60
|
+
duration,
|
|
61
|
+
instructorName,
|
|
62
|
+
instructorAvatar,
|
|
63
|
+
originalPrice,
|
|
64
|
+
isBundle = false,
|
|
65
|
+
onClick,
|
|
66
|
+
className
|
|
67
|
+
}) {
|
|
68
|
+
const [imageError, setImageError] = (0, import_react.useState)(false);
|
|
69
|
+
const clampedProgress = Math.min(Math.max(progress, 0), 100);
|
|
70
|
+
const stripHtml = (html) => {
|
|
71
|
+
if (typeof document === "undefined") return html;
|
|
72
|
+
const tmp = document.createElement("div");
|
|
73
|
+
tmp.innerHTML = html;
|
|
74
|
+
return tmp.textContent || tmp.innerText || "";
|
|
75
|
+
};
|
|
76
|
+
const displayCategory = category || "Design";
|
|
77
|
+
const displayDuration = duration || "3 Month";
|
|
78
|
+
const displayInstructorName = instructorName || (description ? stripHtml(description).slice(0, 15) : "Instructor");
|
|
79
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
80
|
+
"div",
|
|
81
|
+
{
|
|
82
|
+
className: cn(
|
|
83
|
+
"group overflow-hidden rounded-2xl border border-gray-100 bg-white shadow-sm hover:shadow-lg transition-all duration-300 hover:-translate-y-1 h-full flex flex-col min-w-0 w-full cursor-pointer",
|
|
84
|
+
className
|
|
85
|
+
),
|
|
86
|
+
onClick,
|
|
87
|
+
children: [
|
|
88
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "relative h-[238.66px] w-full overflow-hidden mx-auto px-3 pt-3", children: [
|
|
89
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "relative w-full h-full rounded-xl overflow-hidden bg-gray-100", children: isBundle && thumbnails && thumbnails.length > 0 && !imageError ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "relative w-full h-full flex", children: [
|
|
90
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "relative h-full w-1/2", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
91
|
+
"img",
|
|
92
|
+
{
|
|
93
|
+
src: thumbnails[0],
|
|
94
|
+
alt: `${title} - image 1`,
|
|
95
|
+
className: "w-full h-full object-cover transition-transform duration-300 group-hover:scale-110",
|
|
96
|
+
onError: () => setImageError(true)
|
|
97
|
+
}
|
|
98
|
+
) }),
|
|
99
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex flex-col w-1/2 h-full", children: [
|
|
100
|
+
thumbnails[1] && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: cn("relative w-full overflow-hidden", thumbnails.length > 2 ? "h-1/2" : "h-full"), children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
101
|
+
"img",
|
|
102
|
+
{
|
|
103
|
+
src: thumbnails[1],
|
|
104
|
+
alt: `${title} - image 2`,
|
|
105
|
+
className: "w-full h-full object-cover transition-transform duration-300 group-hover:scale-110",
|
|
106
|
+
onError: () => setImageError(true)
|
|
107
|
+
}
|
|
108
|
+
) }),
|
|
109
|
+
thumbnails.length > 2 && thumbnails[2] && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "relative w-full h-1/2 overflow-hidden", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
110
|
+
"img",
|
|
111
|
+
{
|
|
112
|
+
src: thumbnails[2],
|
|
113
|
+
alt: `${title} - image 3`,
|
|
114
|
+
className: "w-full h-full object-cover transition-transform duration-300 group-hover:scale-110",
|
|
115
|
+
onError: () => setImageError(true)
|
|
116
|
+
}
|
|
117
|
+
) })
|
|
118
|
+
] })
|
|
119
|
+
] }) : thumbnail && !imageError ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
120
|
+
"img",
|
|
121
|
+
{
|
|
122
|
+
src: thumbnail,
|
|
123
|
+
alt: title,
|
|
124
|
+
className: "w-full h-full object-cover transition-transform duration-300 group-hover:scale-110",
|
|
125
|
+
onError: () => setImageError(true)
|
|
126
|
+
}
|
|
127
|
+
) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "w-full h-full flex items-center justify-center bg-gray-200", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_lucide_react.BookOpen, { className: "w-12 h-12 text-gray-400" }) }) }),
|
|
128
|
+
status && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "absolute right-6 top-5", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
129
|
+
"span",
|
|
130
|
+
{
|
|
131
|
+
className: `rounded-full px-3 py-1.5 text-xs font-medium text-white shadow-md ${status === "completed" ? "bg-green-500" : "bg-[rgb(var(--accent-primary))]"}`,
|
|
132
|
+
children: status
|
|
133
|
+
}
|
|
134
|
+
) })
|
|
135
|
+
] }),
|
|
136
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "p-5 space-y-3 flex-1 flex flex-col min-w-0", children: [
|
|
137
|
+
(displayCategory || displayDuration) && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex items-center justify-between text-xs text-gray-500", children: [
|
|
138
|
+
displayCategory && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex items-center gap-1.5", children: [
|
|
139
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_lucide_react.Grid2X2, { className: "w-3.5 h-3.5 text-gray-400" }),
|
|
140
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: displayCategory })
|
|
141
|
+
] }),
|
|
142
|
+
displayDuration && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex items-center gap-1.5", children: [
|
|
143
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_lucide_react.Clock, { className: "w-3.5 h-3.5 text-gray-400" }),
|
|
144
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: displayDuration })
|
|
145
|
+
] })
|
|
146
|
+
] }),
|
|
147
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("h3", { className: "text-base font-bold text-gray-900 line-clamp-2 leading-snug", children: title }),
|
|
148
|
+
description && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "text-sm text-gray-500 line-clamp-3 leading-relaxed", children: stripHtml(description) }),
|
|
149
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "flex-1" }),
|
|
150
|
+
showProgress && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "space-y-2", children: [
|
|
151
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "h-2 overflow-hidden rounded-full bg-gray-100", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
152
|
+
"div",
|
|
153
|
+
{
|
|
154
|
+
className: "h-full rounded-full bg-[rgb(var(--accent-primary))] transition-all duration-300",
|
|
155
|
+
style: { width: `${clampedProgress}%` }
|
|
156
|
+
}
|
|
157
|
+
) }),
|
|
158
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex items-center justify-between text-xs", children: [
|
|
159
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "text-gray-400" }),
|
|
160
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "text-gray-500", children: [
|
|
161
|
+
isBundle ? "Course" : "Lesson",
|
|
162
|
+
" ",
|
|
163
|
+
Math.ceil(clampedProgress / 100 * totalLessons),
|
|
164
|
+
" of ",
|
|
165
|
+
totalLessons
|
|
166
|
+
] })
|
|
167
|
+
] })
|
|
168
|
+
] }),
|
|
169
|
+
showPrice && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex items-center justify-between pt-3 border-t border-gray-100", children: [
|
|
170
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex items-center gap-2", children: [
|
|
171
|
+
instructorAvatar ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "w-7 h-7 rounded-full overflow-hidden flex-shrink-0", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("img", { src: instructorAvatar, alt: displayInstructorName, className: "w-full h-full object-cover" }) }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "w-7 h-7 rounded-full bg-gray-200 flex items-center justify-center flex-shrink-0", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "text-[10px] font-semibold text-gray-500", children: displayInstructorName.charAt(0).toUpperCase() }) }),
|
|
172
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "text-sm text-gray-600 font-medium truncate max-w-[80px]", children: displayInstructorName })
|
|
173
|
+
] }),
|
|
174
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex items-center gap-2", children: [
|
|
175
|
+
!isFree && originalPrice && originalPrice > price && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "text-sm text-gray-400 line-through", children: [
|
|
176
|
+
"$",
|
|
177
|
+
originalPrice
|
|
178
|
+
] }),
|
|
179
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "text-lg font-bold text-[rgb(var(--accent-primary))]", children: isFree ? "Free" : `$${price}` })
|
|
180
|
+
] })
|
|
181
|
+
] })
|
|
182
|
+
] })
|
|
183
|
+
]
|
|
184
|
+
}
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// src/components/molecules/Pagination.tsx
|
|
189
|
+
var import_lucide_react2 = require("lucide-react");
|
|
190
|
+
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
191
|
+
function Pagination({
|
|
192
|
+
currentPage,
|
|
193
|
+
totalPages,
|
|
194
|
+
total,
|
|
195
|
+
pageSize,
|
|
196
|
+
hasNextPage,
|
|
197
|
+
hasPreviousPage,
|
|
198
|
+
onPageChange,
|
|
199
|
+
maxVisiblePages = 5,
|
|
200
|
+
className
|
|
201
|
+
}) {
|
|
202
|
+
const renderPageNumbers = () => {
|
|
203
|
+
const pages = [];
|
|
204
|
+
let startPage = Math.max(1, currentPage - Math.floor(maxVisiblePages / 2));
|
|
205
|
+
let endPage = Math.min(totalPages, startPage + maxVisiblePages - 1);
|
|
206
|
+
if (endPage - startPage < maxVisiblePages - 1) {
|
|
207
|
+
startPage = Math.max(1, endPage - maxVisiblePages + 1);
|
|
208
|
+
}
|
|
209
|
+
for (let i = startPage; i <= endPage; i++) {
|
|
210
|
+
pages.push(
|
|
211
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
212
|
+
"button",
|
|
213
|
+
{
|
|
214
|
+
onClick: () => onPageChange(i),
|
|
215
|
+
className: cn(
|
|
216
|
+
"px-3 py-1 rounded cursor-pointer transition-colors",
|
|
217
|
+
i === currentPage ? "bg-theme-accent-primary text-white" : "text-theme-text-primary border border-theme-border-primary hover:bg-theme-bg-tertiary"
|
|
218
|
+
),
|
|
219
|
+
children: i
|
|
220
|
+
},
|
|
221
|
+
i
|
|
222
|
+
)
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
return pages;
|
|
226
|
+
};
|
|
227
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: cn("flex items-center justify-between border-t border-theme-border-primary pt-4", className), children: [
|
|
228
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "text-sm text-theme-text-secondary", children: [
|
|
229
|
+
"Showing ",
|
|
230
|
+
(currentPage - 1) * pageSize + 1,
|
|
231
|
+
" to ",
|
|
232
|
+
Math.min(currentPage * pageSize, total),
|
|
233
|
+
" of ",
|
|
234
|
+
total,
|
|
235
|
+
" results"
|
|
236
|
+
] }),
|
|
237
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "flex items-center gap-2", children: [
|
|
238
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
239
|
+
"button",
|
|
240
|
+
{
|
|
241
|
+
onClick: () => onPageChange(currentPage - 1),
|
|
242
|
+
disabled: !hasPreviousPage,
|
|
243
|
+
className: "p-2 rounded border border-theme-border-primary hover:bg-theme-bg-tertiary disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer transition-colors",
|
|
244
|
+
"aria-label": "Previous page",
|
|
245
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_lucide_react2.ChevronLeft, { className: "h-4 w-4" })
|
|
246
|
+
}
|
|
247
|
+
),
|
|
248
|
+
renderPageNumbers(),
|
|
249
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
250
|
+
"button",
|
|
251
|
+
{
|
|
252
|
+
onClick: () => onPageChange(currentPage + 1),
|
|
253
|
+
disabled: !hasNextPage,
|
|
254
|
+
className: "p-2 rounded border border-theme-border-primary hover:bg-theme-bg-tertiary disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer transition-colors",
|
|
255
|
+
"aria-label": "Next page",
|
|
256
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_lucide_react2.ChevronRight, { className: "h-4 w-4" })
|
|
257
|
+
}
|
|
258
|
+
)
|
|
259
|
+
] })
|
|
260
|
+
] });
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// src/components/molecules/PageHeader.tsx
|
|
264
|
+
var import_jsx_runtime3 = require("react/jsx-runtime");
|
|
265
|
+
function PageHeader({ title, description, actions, className }) {
|
|
266
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: cn("flex flex-col sm:flex-row sm:items-center justify-between gap-4 mb-6", className), children: [
|
|
267
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { children: [
|
|
268
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("h1", { className: "text-2xl font-bold text-theme-text-primary", children: title }),
|
|
269
|
+
description && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "text-sm text-theme-text-secondary mt-1", children: description })
|
|
270
|
+
] }),
|
|
271
|
+
actions && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "flex items-center gap-2", children: actions })
|
|
272
|
+
] });
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// src/components/molecules/SearchInput.tsx
|
|
276
|
+
var import_lucide_react3 = require("lucide-react");
|
|
277
|
+
var import_jsx_runtime4 = require("react/jsx-runtime");
|
|
278
|
+
function SearchInput({ value, onChange, placeholder = "Search...", className }) {
|
|
279
|
+
return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: cn("relative", className), children: [
|
|
280
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_lucide_react3.Search, { className: "absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-theme-text-muted" }),
|
|
281
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
282
|
+
"input",
|
|
283
|
+
{
|
|
284
|
+
type: "text",
|
|
285
|
+
value,
|
|
286
|
+
onChange: (e) => onChange(e.target.value),
|
|
287
|
+
placeholder,
|
|
288
|
+
className: "w-full pl-10 pr-10 py-2 text-sm rounded-lg border border-theme-border-primary bg-theme-bg-primary text-theme-text-primary placeholder:text-[rgb(var(--text-muted))] focus:outline-none focus:border-theme-accent-primary transition-colors"
|
|
289
|
+
}
|
|
290
|
+
),
|
|
291
|
+
value && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
292
|
+
"button",
|
|
293
|
+
{
|
|
294
|
+
onClick: () => onChange(""),
|
|
295
|
+
className: "absolute right-3 top-1/2 -translate-y-1/2 text-theme-text-muted hover:text-theme-text-primary cursor-pointer",
|
|
296
|
+
"aria-label": "Clear search",
|
|
297
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_lucide_react3.X, { className: "h-4 w-4" })
|
|
298
|
+
}
|
|
299
|
+
)
|
|
300
|
+
] });
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// src/components/molecules/EmptyState.tsx
|
|
304
|
+
var import_jsx_runtime5 = require("react/jsx-runtime");
|
|
305
|
+
function EmptyState({ icon, title, description, action, className }) {
|
|
306
|
+
return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: cn("flex flex-col items-center justify-center py-12 text-center", className), children: [
|
|
307
|
+
icon && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "mb-4 text-theme-text-muted", children: icon }),
|
|
308
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("h3", { className: "text-lg font-semibold text-theme-text-primary", children: title }),
|
|
309
|
+
description && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { className: "mt-2 text-sm text-theme-text-secondary max-w-md", children: description }),
|
|
310
|
+
action && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "mt-4", children: action })
|
|
311
|
+
] });
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// src/components/molecules/LoadingSpinner.tsx
|
|
315
|
+
var import_jsx_runtime6 = require("react/jsx-runtime");
|
|
316
|
+
var sizeClasses = {
|
|
317
|
+
sm: "h-4 w-4",
|
|
318
|
+
md: "h-8 w-8",
|
|
319
|
+
lg: "h-12 w-12"
|
|
320
|
+
};
|
|
321
|
+
function LoadingSpinner({ size = "md", className, text }) {
|
|
322
|
+
return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: cn("flex flex-col items-center justify-center gap-3", className), children: [
|
|
323
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
|
|
324
|
+
"div",
|
|
325
|
+
{
|
|
326
|
+
className: cn(
|
|
327
|
+
"animate-spin rounded-full border-2 border-theme-bg-tertiary border-t-theme-accent-primary",
|
|
328
|
+
sizeClasses[size]
|
|
329
|
+
)
|
|
330
|
+
}
|
|
331
|
+
),
|
|
332
|
+
text && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { className: "text-sm text-theme-text-secondary", children: text })
|
|
333
|
+
] });
|
|
334
|
+
}
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
// src/components/molecules/CourseCard.tsx
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import { BookOpen, Clock, Grid2X2 } from "lucide-react";
|
|
4
|
+
|
|
5
|
+
// src/components/utils.ts
|
|
6
|
+
import { clsx } from "clsx";
|
|
7
|
+
import { twMerge } from "tailwind-merge";
|
|
8
|
+
function cn(...inputs) {
|
|
9
|
+
return twMerge(clsx(inputs));
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// src/components/molecules/CourseCard.tsx
|
|
13
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
14
|
+
function CourseCard({
|
|
15
|
+
id,
|
|
16
|
+
title,
|
|
17
|
+
thumbnail,
|
|
18
|
+
thumbnails,
|
|
19
|
+
totalLessons,
|
|
20
|
+
progress,
|
|
21
|
+
status,
|
|
22
|
+
isFree,
|
|
23
|
+
price = 0,
|
|
24
|
+
currency = "USD",
|
|
25
|
+
description,
|
|
26
|
+
showPrice = true,
|
|
27
|
+
showProgress = true,
|
|
28
|
+
category,
|
|
29
|
+
duration,
|
|
30
|
+
instructorName,
|
|
31
|
+
instructorAvatar,
|
|
32
|
+
originalPrice,
|
|
33
|
+
isBundle = false,
|
|
34
|
+
onClick,
|
|
35
|
+
className
|
|
36
|
+
}) {
|
|
37
|
+
const [imageError, setImageError] = useState(false);
|
|
38
|
+
const clampedProgress = Math.min(Math.max(progress, 0), 100);
|
|
39
|
+
const stripHtml = (html) => {
|
|
40
|
+
if (typeof document === "undefined") return html;
|
|
41
|
+
const tmp = document.createElement("div");
|
|
42
|
+
tmp.innerHTML = html;
|
|
43
|
+
return tmp.textContent || tmp.innerText || "";
|
|
44
|
+
};
|
|
45
|
+
const displayCategory = category || "Design";
|
|
46
|
+
const displayDuration = duration || "3 Month";
|
|
47
|
+
const displayInstructorName = instructorName || (description ? stripHtml(description).slice(0, 15) : "Instructor");
|
|
48
|
+
return /* @__PURE__ */ jsxs(
|
|
49
|
+
"div",
|
|
50
|
+
{
|
|
51
|
+
className: cn(
|
|
52
|
+
"group overflow-hidden rounded-2xl border border-gray-100 bg-white shadow-sm hover:shadow-lg transition-all duration-300 hover:-translate-y-1 h-full flex flex-col min-w-0 w-full cursor-pointer",
|
|
53
|
+
className
|
|
54
|
+
),
|
|
55
|
+
onClick,
|
|
56
|
+
children: [
|
|
57
|
+
/* @__PURE__ */ jsxs("div", { className: "relative h-[238.66px] w-full overflow-hidden mx-auto px-3 pt-3", children: [
|
|
58
|
+
/* @__PURE__ */ jsx("div", { className: "relative w-full h-full rounded-xl overflow-hidden bg-gray-100", children: isBundle && thumbnails && thumbnails.length > 0 && !imageError ? /* @__PURE__ */ jsxs("div", { className: "relative w-full h-full flex", children: [
|
|
59
|
+
/* @__PURE__ */ jsx("div", { className: "relative h-full w-1/2", children: /* @__PURE__ */ jsx(
|
|
60
|
+
"img",
|
|
61
|
+
{
|
|
62
|
+
src: thumbnails[0],
|
|
63
|
+
alt: `${title} - image 1`,
|
|
64
|
+
className: "w-full h-full object-cover transition-transform duration-300 group-hover:scale-110",
|
|
65
|
+
onError: () => setImageError(true)
|
|
66
|
+
}
|
|
67
|
+
) }),
|
|
68
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-col w-1/2 h-full", children: [
|
|
69
|
+
thumbnails[1] && /* @__PURE__ */ jsx("div", { className: cn("relative w-full overflow-hidden", thumbnails.length > 2 ? "h-1/2" : "h-full"), children: /* @__PURE__ */ jsx(
|
|
70
|
+
"img",
|
|
71
|
+
{
|
|
72
|
+
src: thumbnails[1],
|
|
73
|
+
alt: `${title} - image 2`,
|
|
74
|
+
className: "w-full h-full object-cover transition-transform duration-300 group-hover:scale-110",
|
|
75
|
+
onError: () => setImageError(true)
|
|
76
|
+
}
|
|
77
|
+
) }),
|
|
78
|
+
thumbnails.length > 2 && thumbnails[2] && /* @__PURE__ */ jsx("div", { className: "relative w-full h-1/2 overflow-hidden", children: /* @__PURE__ */ jsx(
|
|
79
|
+
"img",
|
|
80
|
+
{
|
|
81
|
+
src: thumbnails[2],
|
|
82
|
+
alt: `${title} - image 3`,
|
|
83
|
+
className: "w-full h-full object-cover transition-transform duration-300 group-hover:scale-110",
|
|
84
|
+
onError: () => setImageError(true)
|
|
85
|
+
}
|
|
86
|
+
) })
|
|
87
|
+
] })
|
|
88
|
+
] }) : thumbnail && !imageError ? /* @__PURE__ */ jsx(
|
|
89
|
+
"img",
|
|
90
|
+
{
|
|
91
|
+
src: thumbnail,
|
|
92
|
+
alt: title,
|
|
93
|
+
className: "w-full h-full object-cover transition-transform duration-300 group-hover:scale-110",
|
|
94
|
+
onError: () => setImageError(true)
|
|
95
|
+
}
|
|
96
|
+
) : /* @__PURE__ */ jsx("div", { className: "w-full h-full flex items-center justify-center bg-gray-200", children: /* @__PURE__ */ jsx(BookOpen, { className: "w-12 h-12 text-gray-400" }) }) }),
|
|
97
|
+
status && /* @__PURE__ */ jsx("div", { className: "absolute right-6 top-5", children: /* @__PURE__ */ jsx(
|
|
98
|
+
"span",
|
|
99
|
+
{
|
|
100
|
+
className: `rounded-full px-3 py-1.5 text-xs font-medium text-white shadow-md ${status === "completed" ? "bg-green-500" : "bg-[rgb(var(--accent-primary))]"}`,
|
|
101
|
+
children: status
|
|
102
|
+
}
|
|
103
|
+
) })
|
|
104
|
+
] }),
|
|
105
|
+
/* @__PURE__ */ jsxs("div", { className: "p-5 space-y-3 flex-1 flex flex-col min-w-0", children: [
|
|
106
|
+
(displayCategory || displayDuration) && /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between text-xs text-gray-500", children: [
|
|
107
|
+
displayCategory && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5", children: [
|
|
108
|
+
/* @__PURE__ */ jsx(Grid2X2, { className: "w-3.5 h-3.5 text-gray-400" }),
|
|
109
|
+
/* @__PURE__ */ jsx("span", { children: displayCategory })
|
|
110
|
+
] }),
|
|
111
|
+
displayDuration && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5", children: [
|
|
112
|
+
/* @__PURE__ */ jsx(Clock, { className: "w-3.5 h-3.5 text-gray-400" }),
|
|
113
|
+
/* @__PURE__ */ jsx("span", { children: displayDuration })
|
|
114
|
+
] })
|
|
115
|
+
] }),
|
|
116
|
+
/* @__PURE__ */ jsx("h3", { className: "text-base font-bold text-gray-900 line-clamp-2 leading-snug", children: title }),
|
|
117
|
+
description && /* @__PURE__ */ jsx("p", { className: "text-sm text-gray-500 line-clamp-3 leading-relaxed", children: stripHtml(description) }),
|
|
118
|
+
/* @__PURE__ */ jsx("div", { className: "flex-1" }),
|
|
119
|
+
showProgress && /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
|
|
120
|
+
/* @__PURE__ */ jsx("div", { className: "h-2 overflow-hidden rounded-full bg-gray-100", children: /* @__PURE__ */ jsx(
|
|
121
|
+
"div",
|
|
122
|
+
{
|
|
123
|
+
className: "h-full rounded-full bg-[rgb(var(--accent-primary))] transition-all duration-300",
|
|
124
|
+
style: { width: `${clampedProgress}%` }
|
|
125
|
+
}
|
|
126
|
+
) }),
|
|
127
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between text-xs", children: [
|
|
128
|
+
/* @__PURE__ */ jsx("span", { className: "text-gray-400" }),
|
|
129
|
+
/* @__PURE__ */ jsxs("span", { className: "text-gray-500", children: [
|
|
130
|
+
isBundle ? "Course" : "Lesson",
|
|
131
|
+
" ",
|
|
132
|
+
Math.ceil(clampedProgress / 100 * totalLessons),
|
|
133
|
+
" of ",
|
|
134
|
+
totalLessons
|
|
135
|
+
] })
|
|
136
|
+
] })
|
|
137
|
+
] }),
|
|
138
|
+
showPrice && /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between pt-3 border-t border-gray-100", children: [
|
|
139
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
140
|
+
instructorAvatar ? /* @__PURE__ */ jsx("div", { className: "w-7 h-7 rounded-full overflow-hidden flex-shrink-0", children: /* @__PURE__ */ jsx("img", { src: instructorAvatar, alt: displayInstructorName, className: "w-full h-full object-cover" }) }) : /* @__PURE__ */ jsx("div", { className: "w-7 h-7 rounded-full bg-gray-200 flex items-center justify-center flex-shrink-0", children: /* @__PURE__ */ jsx("span", { className: "text-[10px] font-semibold text-gray-500", children: displayInstructorName.charAt(0).toUpperCase() }) }),
|
|
141
|
+
/* @__PURE__ */ jsx("span", { className: "text-sm text-gray-600 font-medium truncate max-w-[80px]", children: displayInstructorName })
|
|
142
|
+
] }),
|
|
143
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
144
|
+
!isFree && originalPrice && originalPrice > price && /* @__PURE__ */ jsxs("span", { className: "text-sm text-gray-400 line-through", children: [
|
|
145
|
+
"$",
|
|
146
|
+
originalPrice
|
|
147
|
+
] }),
|
|
148
|
+
/* @__PURE__ */ jsx("span", { className: "text-lg font-bold text-[rgb(var(--accent-primary))]", children: isFree ? "Free" : `$${price}` })
|
|
149
|
+
] })
|
|
150
|
+
] })
|
|
151
|
+
] })
|
|
152
|
+
]
|
|
153
|
+
}
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// src/components/molecules/Pagination.tsx
|
|
158
|
+
import { ChevronLeft, ChevronRight } from "lucide-react";
|
|
159
|
+
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
160
|
+
function Pagination({
|
|
161
|
+
currentPage,
|
|
162
|
+
totalPages,
|
|
163
|
+
total,
|
|
164
|
+
pageSize,
|
|
165
|
+
hasNextPage,
|
|
166
|
+
hasPreviousPage,
|
|
167
|
+
onPageChange,
|
|
168
|
+
maxVisiblePages = 5,
|
|
169
|
+
className
|
|
170
|
+
}) {
|
|
171
|
+
const renderPageNumbers = () => {
|
|
172
|
+
const pages = [];
|
|
173
|
+
let startPage = Math.max(1, currentPage - Math.floor(maxVisiblePages / 2));
|
|
174
|
+
let endPage = Math.min(totalPages, startPage + maxVisiblePages - 1);
|
|
175
|
+
if (endPage - startPage < maxVisiblePages - 1) {
|
|
176
|
+
startPage = Math.max(1, endPage - maxVisiblePages + 1);
|
|
177
|
+
}
|
|
178
|
+
for (let i = startPage; i <= endPage; i++) {
|
|
179
|
+
pages.push(
|
|
180
|
+
/* @__PURE__ */ jsx2(
|
|
181
|
+
"button",
|
|
182
|
+
{
|
|
183
|
+
onClick: () => onPageChange(i),
|
|
184
|
+
className: cn(
|
|
185
|
+
"px-3 py-1 rounded cursor-pointer transition-colors",
|
|
186
|
+
i === currentPage ? "bg-theme-accent-primary text-white" : "text-theme-text-primary border border-theme-border-primary hover:bg-theme-bg-tertiary"
|
|
187
|
+
),
|
|
188
|
+
children: i
|
|
189
|
+
},
|
|
190
|
+
i
|
|
191
|
+
)
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
return pages;
|
|
195
|
+
};
|
|
196
|
+
return /* @__PURE__ */ jsxs2("div", { className: cn("flex items-center justify-between border-t border-theme-border-primary pt-4", className), children: [
|
|
197
|
+
/* @__PURE__ */ jsxs2("div", { className: "text-sm text-theme-text-secondary", children: [
|
|
198
|
+
"Showing ",
|
|
199
|
+
(currentPage - 1) * pageSize + 1,
|
|
200
|
+
" to ",
|
|
201
|
+
Math.min(currentPage * pageSize, total),
|
|
202
|
+
" of ",
|
|
203
|
+
total,
|
|
204
|
+
" results"
|
|
205
|
+
] }),
|
|
206
|
+
/* @__PURE__ */ jsxs2("div", { className: "flex items-center gap-2", children: [
|
|
207
|
+
/* @__PURE__ */ jsx2(
|
|
208
|
+
"button",
|
|
209
|
+
{
|
|
210
|
+
onClick: () => onPageChange(currentPage - 1),
|
|
211
|
+
disabled: !hasPreviousPage,
|
|
212
|
+
className: "p-2 rounded border border-theme-border-primary hover:bg-theme-bg-tertiary disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer transition-colors",
|
|
213
|
+
"aria-label": "Previous page",
|
|
214
|
+
children: /* @__PURE__ */ jsx2(ChevronLeft, { className: "h-4 w-4" })
|
|
215
|
+
}
|
|
216
|
+
),
|
|
217
|
+
renderPageNumbers(),
|
|
218
|
+
/* @__PURE__ */ jsx2(
|
|
219
|
+
"button",
|
|
220
|
+
{
|
|
221
|
+
onClick: () => onPageChange(currentPage + 1),
|
|
222
|
+
disabled: !hasNextPage,
|
|
223
|
+
className: "p-2 rounded border border-theme-border-primary hover:bg-theme-bg-tertiary disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer transition-colors",
|
|
224
|
+
"aria-label": "Next page",
|
|
225
|
+
children: /* @__PURE__ */ jsx2(ChevronRight, { className: "h-4 w-4" })
|
|
226
|
+
}
|
|
227
|
+
)
|
|
228
|
+
] })
|
|
229
|
+
] });
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// src/components/molecules/PageHeader.tsx
|
|
233
|
+
import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
234
|
+
function PageHeader({ title, description, actions, className }) {
|
|
235
|
+
return /* @__PURE__ */ jsxs3("div", { className: cn("flex flex-col sm:flex-row sm:items-center justify-between gap-4 mb-6", className), children: [
|
|
236
|
+
/* @__PURE__ */ jsxs3("div", { children: [
|
|
237
|
+
/* @__PURE__ */ jsx3("h1", { className: "text-2xl font-bold text-theme-text-primary", children: title }),
|
|
238
|
+
description && /* @__PURE__ */ jsx3("p", { className: "text-sm text-theme-text-secondary mt-1", children: description })
|
|
239
|
+
] }),
|
|
240
|
+
actions && /* @__PURE__ */ jsx3("div", { className: "flex items-center gap-2", children: actions })
|
|
241
|
+
] });
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// src/components/molecules/SearchInput.tsx
|
|
245
|
+
import { Search, X } from "lucide-react";
|
|
246
|
+
import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
247
|
+
function SearchInput({ value, onChange, placeholder = "Search...", className }) {
|
|
248
|
+
return /* @__PURE__ */ jsxs4("div", { className: cn("relative", className), children: [
|
|
249
|
+
/* @__PURE__ */ jsx4(Search, { className: "absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-theme-text-muted" }),
|
|
250
|
+
/* @__PURE__ */ jsx4(
|
|
251
|
+
"input",
|
|
252
|
+
{
|
|
253
|
+
type: "text",
|
|
254
|
+
value,
|
|
255
|
+
onChange: (e) => onChange(e.target.value),
|
|
256
|
+
placeholder,
|
|
257
|
+
className: "w-full pl-10 pr-10 py-2 text-sm rounded-lg border border-theme-border-primary bg-theme-bg-primary text-theme-text-primary placeholder:text-[rgb(var(--text-muted))] focus:outline-none focus:border-theme-accent-primary transition-colors"
|
|
258
|
+
}
|
|
259
|
+
),
|
|
260
|
+
value && /* @__PURE__ */ jsx4(
|
|
261
|
+
"button",
|
|
262
|
+
{
|
|
263
|
+
onClick: () => onChange(""),
|
|
264
|
+
className: "absolute right-3 top-1/2 -translate-y-1/2 text-theme-text-muted hover:text-theme-text-primary cursor-pointer",
|
|
265
|
+
"aria-label": "Clear search",
|
|
266
|
+
children: /* @__PURE__ */ jsx4(X, { className: "h-4 w-4" })
|
|
267
|
+
}
|
|
268
|
+
)
|
|
269
|
+
] });
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// src/components/molecules/EmptyState.tsx
|
|
273
|
+
import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
274
|
+
function EmptyState({ icon, title, description, action, className }) {
|
|
275
|
+
return /* @__PURE__ */ jsxs5("div", { className: cn("flex flex-col items-center justify-center py-12 text-center", className), children: [
|
|
276
|
+
icon && /* @__PURE__ */ jsx5("div", { className: "mb-4 text-theme-text-muted", children: icon }),
|
|
277
|
+
/* @__PURE__ */ jsx5("h3", { className: "text-lg font-semibold text-theme-text-primary", children: title }),
|
|
278
|
+
description && /* @__PURE__ */ jsx5("p", { className: "mt-2 text-sm text-theme-text-secondary max-w-md", children: description }),
|
|
279
|
+
action && /* @__PURE__ */ jsx5("div", { className: "mt-4", children: action })
|
|
280
|
+
] });
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// src/components/molecules/LoadingSpinner.tsx
|
|
284
|
+
import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
285
|
+
var sizeClasses = {
|
|
286
|
+
sm: "h-4 w-4",
|
|
287
|
+
md: "h-8 w-8",
|
|
288
|
+
lg: "h-12 w-12"
|
|
289
|
+
};
|
|
290
|
+
function LoadingSpinner({ size = "md", className, text }) {
|
|
291
|
+
return /* @__PURE__ */ jsxs6("div", { className: cn("flex flex-col items-center justify-center gap-3", className), children: [
|
|
292
|
+
/* @__PURE__ */ jsx6(
|
|
293
|
+
"div",
|
|
294
|
+
{
|
|
295
|
+
className: cn(
|
|
296
|
+
"animate-spin rounded-full border-2 border-theme-bg-tertiary border-t-theme-accent-primary",
|
|
297
|
+
sizeClasses[size]
|
|
298
|
+
)
|
|
299
|
+
}
|
|
300
|
+
),
|
|
301
|
+
text && /* @__PURE__ */ jsx6("p", { className: "text-sm text-theme-text-secondary", children: text })
|
|
302
|
+
] });
|
|
303
|
+
}
|
|
304
|
+
export {
|
|
305
|
+
CourseCard,
|
|
306
|
+
EmptyState,
|
|
307
|
+
LoadingSpinner,
|
|
308
|
+
PageHeader,
|
|
309
|
+
Pagination,
|
|
310
|
+
SearchInput
|
|
311
|
+
};
|