@fydemy/cms 1.0.1 → 1.0.2

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/index.js CHANGED
@@ -104,8 +104,10 @@ var init_rate_limit = __esm({
104
104
  // src/index.ts
105
105
  var index_exports = {};
106
106
  __export(index_exports, {
107
+ AdminDashboard: () => AdminDashboard,
107
108
  GitHubStorage: () => GitHubStorage,
108
109
  LocalStorage: () => LocalStorage,
110
+ Login: () => Login,
109
111
  MAX_FILE_SIZE: () => MAX_FILE_SIZE,
110
112
  MAX_PASSWORD_LENGTH: () => MAX_PASSWORD_LENGTH,
111
113
  MAX_USERNAME_LENGTH: () => MAX_USERNAME_LENGTH,
@@ -816,6 +818,1060 @@ async function handleUpload(request) {
816
818
  }
817
819
  }
818
820
 
821
+ // src/ui/AdminDashboard.tsx
822
+ var import_react = require("react");
823
+ var import_jsx_runtime = require("react/jsx-runtime");
824
+ function AdminDashboard() {
825
+ const [entries, setEntries] = (0, import_react.useState)([]);
826
+ const [currentPath, setCurrentPath] = (0, import_react.useState)("");
827
+ const [selectedFile, setSelectedFile] = (0, import_react.useState)(null);
828
+ const [fields, setFields] = (0, import_react.useState)({});
829
+ const [loading, setLoading] = (0, import_react.useState)(false);
830
+ const [message, setMessage] = (0, import_react.useState)("");
831
+ const [newFileName, setNewFileName] = (0, import_react.useState)("");
832
+ const [uploading, setUploading] = (0, import_react.useState)(false);
833
+ (0, import_react.useEffect)(() => {
834
+ loadDirectory(currentPath);
835
+ }, [currentPath]);
836
+ const loadDirectory = async (path4) => {
837
+ try {
838
+ const url = path4 ? `/api/cms/list/${path4}` : "/api/cms/list";
839
+ const response = await fetch(url);
840
+ const data = await response.json();
841
+ setEntries(data.entries || []);
842
+ } catch (error) {
843
+ console.error("Failed to load directory:", error);
844
+ }
845
+ };
846
+ const loadFile = async (filePath) => {
847
+ try {
848
+ const response = await fetch(`/api/cms/content/${filePath}`);
849
+ const data = await response.json();
850
+ setSelectedFile(filePath);
851
+ const loadedFields = {};
852
+ if (data.content !== void 0) {
853
+ loadedFields["content"] = { type: "markdown", value: data.content };
854
+ }
855
+ Object.entries(data.data || {}).forEach(([key, value]) => {
856
+ loadedFields[key] = {
857
+ type: detectFieldType(value),
858
+ value
859
+ };
860
+ });
861
+ setFields(loadedFields);
862
+ } catch (error) {
863
+ setMessage("Failed to load file");
864
+ }
865
+ };
866
+ const detectFieldType = (value) => {
867
+ if (typeof value === "number") return "number";
868
+ if (typeof value === "string") {
869
+ if (value.match(/^\d{4}-\d{2}-\d{2}/)) return "date";
870
+ if (value.startsWith("/uploads/") || value.startsWith("http"))
871
+ return "image";
872
+ if (value.length > 100) return "markdown";
873
+ }
874
+ return "text";
875
+ };
876
+ const saveFile = async () => {
877
+ if (!selectedFile) return;
878
+ setLoading(true);
879
+ setMessage("");
880
+ try {
881
+ const frontmatter = {};
882
+ let content = "";
883
+ Object.entries(fields).forEach(([key, field]) => {
884
+ if (key === "content") {
885
+ content = field.value;
886
+ } else {
887
+ frontmatter[key] = field.value;
888
+ }
889
+ });
890
+ const response = await fetch(`/api/cms/content/${selectedFile}`, {
891
+ method: "POST",
892
+ headers: { "Content-Type": "application/json" },
893
+ body: JSON.stringify({ data: frontmatter, content })
894
+ });
895
+ if (response.ok) {
896
+ setMessage("\u2705 File saved successfully!");
897
+ setTimeout(() => setMessage(""), 3e3);
898
+ } else {
899
+ const errorData = await response.json().catch(() => ({}));
900
+ setMessage(
901
+ `\u274C Failed to save file: ${errorData.error || response.statusText}`
902
+ );
903
+ }
904
+ } catch (error) {
905
+ setMessage(
906
+ `\u274C Error saving file: ${error instanceof Error ? error.message : "Unknown error"}`
907
+ );
908
+ } finally {
909
+ setLoading(false);
910
+ }
911
+ };
912
+ const createFile = async () => {
913
+ if (!newFileName) return;
914
+ const fileName = newFileName.endsWith(".md") ? newFileName : `${newFileName}.md`;
915
+ const fullPath = currentPath ? `${currentPath}/${fileName}` : fileName;
916
+ setLoading(true);
917
+ setMessage("");
918
+ try {
919
+ const response = await fetch(`/api/cms/content/${fullPath}`, {
920
+ method: "POST",
921
+ headers: { "Content-Type": "application/json" },
922
+ body: JSON.stringify({
923
+ data: {},
924
+ content: ""
925
+ })
926
+ });
927
+ if (response.ok) {
928
+ setMessage("\u2705 File created!");
929
+ setNewFileName("");
930
+ loadDirectory(currentPath);
931
+ loadFile(fullPath);
932
+ } else {
933
+ setMessage("\u274C Failed to create file");
934
+ }
935
+ } catch (error) {
936
+ setMessage("\u274C Error creating file");
937
+ } finally {
938
+ setLoading(false);
939
+ }
940
+ };
941
+ const duplicateFile = async () => {
942
+ if (!selectedFile) return;
943
+ const newName = prompt("Enter new filename (without .md extension):");
944
+ if (!newName) return;
945
+ const duplicateName = currentPath ? `${currentPath}/${newName}.md` : `${newName}.md`;
946
+ setLoading(true);
947
+ setMessage("");
948
+ try {
949
+ const frontmatter = {};
950
+ let content = "";
951
+ Object.entries(fields).forEach(([key, field]) => {
952
+ if (key === "content") {
953
+ content = field.value;
954
+ } else {
955
+ frontmatter[key] = field.value;
956
+ }
957
+ });
958
+ const response = await fetch(`/api/cms/content/${duplicateName}`, {
959
+ method: "POST",
960
+ headers: { "Content-Type": "application/json" },
961
+ body: JSON.stringify({ data: frontmatter, content })
962
+ });
963
+ if (response.ok) {
964
+ setMessage("\u2705 File duplicated!");
965
+ loadDirectory(currentPath);
966
+ loadFile(duplicateName);
967
+ } else {
968
+ const errorData = await response.json().catch(() => ({}));
969
+ setMessage(
970
+ `\u274C Failed to duplicate file: ${errorData.error || "Unknown error"}`
971
+ );
972
+ }
973
+ } catch (error) {
974
+ setMessage("\u274C Error duplicating file");
975
+ } finally {
976
+ setLoading(false);
977
+ }
978
+ };
979
+ const deleteFile = async (filePath) => {
980
+ if (!confirm(`Delete ${filePath}?`)) return;
981
+ setLoading(true);
982
+ setMessage("");
983
+ try {
984
+ const response = await fetch(`/api/cms/content/${filePath}`, {
985
+ method: "DELETE"
986
+ });
987
+ if (response.ok) {
988
+ setMessage("\u2705 File deleted!");
989
+ if (selectedFile === filePath) {
990
+ setSelectedFile(null);
991
+ setFields({});
992
+ }
993
+ loadDirectory(currentPath);
994
+ } else {
995
+ setMessage("\u274C Failed to delete file");
996
+ }
997
+ } catch (error) {
998
+ setMessage("\u274C Error deleting file");
999
+ } finally {
1000
+ setLoading(false);
1001
+ }
1002
+ };
1003
+ const navigateToDir = (dirPath) => {
1004
+ setCurrentPath(dirPath);
1005
+ setSelectedFile(null);
1006
+ };
1007
+ const goUp = () => {
1008
+ const parts = currentPath.split("/");
1009
+ parts.pop();
1010
+ setCurrentPath(parts.join("/"));
1011
+ setSelectedFile(null);
1012
+ };
1013
+ const handleLogout2 = async () => {
1014
+ await fetch("/api/cms/logout", { method: "POST" });
1015
+ window.location.href = "/admin/login";
1016
+ };
1017
+ const updateField = (key, value) => {
1018
+ setFields((prev) => ({
1019
+ ...prev,
1020
+ [key]: { ...prev[key], value }
1021
+ }));
1022
+ };
1023
+ const addField = () => {
1024
+ const fieldName = prompt("Enter field name:");
1025
+ if (!fieldName || fields[fieldName]) return;
1026
+ const fieldType = prompt(
1027
+ "Enter field type (text/date/number/markdown/image):",
1028
+ "text"
1029
+ );
1030
+ const validTypes = [
1031
+ "text",
1032
+ "date",
1033
+ "number",
1034
+ "markdown",
1035
+ "image"
1036
+ ];
1037
+ if (!validTypes.includes(fieldType)) {
1038
+ alert("Invalid field type! Use: text, date, number, markdown, or image");
1039
+ return;
1040
+ }
1041
+ const defaultValue = fieldType === "number" ? 0 : fieldType === "date" ? (/* @__PURE__ */ new Date()).toISOString().split("T")[0] : "";
1042
+ setFields((prev) => ({
1043
+ ...prev,
1044
+ [fieldName]: { type: fieldType, value: defaultValue }
1045
+ }));
1046
+ };
1047
+ const removeField = (key) => {
1048
+ const newFields = { ...fields };
1049
+ delete newFields[key];
1050
+ setFields(newFields);
1051
+ };
1052
+ const handleImageUpload = async (key, event) => {
1053
+ const file = event.target.files?.[0];
1054
+ if (!file) return;
1055
+ setUploading(true);
1056
+ try {
1057
+ const formData = new FormData();
1058
+ formData.append("file", file);
1059
+ const response = await fetch("/api/cms/upload", {
1060
+ method: "POST",
1061
+ body: formData
1062
+ });
1063
+ const data = await response.json();
1064
+ if (response.ok) {
1065
+ updateField(key, data.url);
1066
+ setMessage(`\u2705 Image uploaded!`);
1067
+ setTimeout(() => setMessage(""), 3e3);
1068
+ } else {
1069
+ setMessage("\u274C Failed to upload image");
1070
+ }
1071
+ } catch (error) {
1072
+ setMessage("\u274C Error uploading image");
1073
+ } finally {
1074
+ setUploading(false);
1075
+ event.target.value = "";
1076
+ }
1077
+ };
1078
+ const handleFileUpload = async (event) => {
1079
+ const file = event.target.files?.[0];
1080
+ if (!file) return;
1081
+ setUploading(true);
1082
+ setMessage("");
1083
+ try {
1084
+ const formData = new FormData();
1085
+ formData.append("file", file);
1086
+ const response = await fetch("/api/cms/upload", {
1087
+ method: "POST",
1088
+ body: formData
1089
+ });
1090
+ const data = await response.json();
1091
+ if (response.ok) {
1092
+ setMessage(`\u2705 File uploaded! Path: ${data.url}`);
1093
+ setTimeout(() => setMessage(""), 5e3);
1094
+ } else {
1095
+ setMessage("\u274C Failed to upload file");
1096
+ }
1097
+ } catch (error) {
1098
+ setMessage("\u274C Error uploading file");
1099
+ } finally {
1100
+ setUploading(false);
1101
+ event.target.value = "";
1102
+ }
1103
+ };
1104
+ const renderField = (key, field) => {
1105
+ switch (field.type) {
1106
+ case "text":
1107
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1108
+ "input",
1109
+ {
1110
+ id: key,
1111
+ type: "text",
1112
+ value: field.value,
1113
+ onChange: (e) => updateField(key, e.target.value),
1114
+ style: {
1115
+ width: "100%",
1116
+ padding: "0.5rem",
1117
+ border: "1px solid #ddd",
1118
+ borderRadius: "4px"
1119
+ }
1120
+ }
1121
+ );
1122
+ case "number":
1123
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1124
+ "input",
1125
+ {
1126
+ id: key,
1127
+ type: "number",
1128
+ value: field.value,
1129
+ onChange: (e) => updateField(key, parseFloat(e.target.value) || 0),
1130
+ style: {
1131
+ width: "100%",
1132
+ padding: "0.5rem",
1133
+ border: "1px solid #ddd",
1134
+ borderRadius: "4px"
1135
+ }
1136
+ }
1137
+ );
1138
+ case "date":
1139
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1140
+ "input",
1141
+ {
1142
+ id: key,
1143
+ type: "date",
1144
+ value: field.value,
1145
+ onChange: (e) => updateField(key, e.target.value),
1146
+ style: {
1147
+ width: "100%",
1148
+ padding: "0.5rem",
1149
+ border: "1px solid #ddd",
1150
+ borderRadius: "4px"
1151
+ }
1152
+ }
1153
+ );
1154
+ case "markdown":
1155
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1156
+ "textarea",
1157
+ {
1158
+ id: key,
1159
+ value: field.value,
1160
+ onChange: (e) => updateField(key, e.target.value),
1161
+ style: {
1162
+ width: "100%",
1163
+ minHeight: key === "content" ? "400px" : "150px",
1164
+ fontFamily: "monospace",
1165
+ padding: "0.5rem",
1166
+ border: "1px solid #ddd",
1167
+ borderRadius: "4px"
1168
+ },
1169
+ placeholder: key === "content" ? "Write your markdown content here..." : "Markdown text..."
1170
+ }
1171
+ );
1172
+ case "image":
1173
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
1174
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1175
+ "input",
1176
+ {
1177
+ id: key,
1178
+ type: "text",
1179
+ value: field.value,
1180
+ onChange: (e) => updateField(key, e.target.value),
1181
+ placeholder: "/uploads/image.jpg or https://...",
1182
+ style: {
1183
+ width: "100%",
1184
+ padding: "0.5rem",
1185
+ marginBottom: "0.5rem",
1186
+ border: "1px solid #ddd",
1187
+ borderRadius: "4px"
1188
+ }
1189
+ }
1190
+ ),
1191
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1192
+ "input",
1193
+ {
1194
+ type: "file",
1195
+ accept: "image/*",
1196
+ onChange: (e) => handleImageUpload(key, e),
1197
+ disabled: uploading,
1198
+ style: { marginBottom: "0.5rem" }
1199
+ }
1200
+ ),
1201
+ field.value && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1202
+ "img",
1203
+ {
1204
+ src: field.value,
1205
+ alt: key,
1206
+ style: {
1207
+ maxWidth: "200px",
1208
+ display: "block",
1209
+ marginTop: "0.5rem",
1210
+ borderRadius: "4px",
1211
+ border: "1px solid #ddd"
1212
+ }
1213
+ }
1214
+ )
1215
+ ] });
1216
+ default:
1217
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1218
+ "input",
1219
+ {
1220
+ id: key,
1221
+ type: "text",
1222
+ value: field.value,
1223
+ onChange: (e) => updateField(key, e.target.value)
1224
+ }
1225
+ );
1226
+ }
1227
+ };
1228
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1229
+ "div",
1230
+ {
1231
+ className: "container",
1232
+ style: { maxWidth: "1200px", margin: "0 auto", padding: "2rem" },
1233
+ children: [
1234
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1235
+ "div",
1236
+ {
1237
+ style: {
1238
+ display: "flex",
1239
+ justifyContent: "space-between",
1240
+ alignItems: "center",
1241
+ marginBottom: "2rem"
1242
+ },
1243
+ children: [
1244
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h1", { children: "\u{1F4DD} Admin Dashboard" }),
1245
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1246
+ "button",
1247
+ {
1248
+ onClick: handleLogout2,
1249
+ style: {
1250
+ padding: "0.5rem 1rem",
1251
+ background: "#f5f5f5",
1252
+ border: "1px solid #ddd",
1253
+ borderRadius: "4px",
1254
+ cursor: "pointer"
1255
+ },
1256
+ children: "Logout"
1257
+ }
1258
+ )
1259
+ ]
1260
+ }
1261
+ ),
1262
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1263
+ "div",
1264
+ {
1265
+ style: {
1266
+ display: "grid",
1267
+ gridTemplateColumns: "minmax(250px, 300px) 1fr",
1268
+ gap: "2rem"
1269
+ },
1270
+ children: [
1271
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1272
+ "div",
1273
+ {
1274
+ style: {
1275
+ background: "white",
1276
+ padding: "1.5rem",
1277
+ borderRadius: "8px",
1278
+ boxShadow: "0 2px 4px rgba(0,0,0,0.1)"
1279
+ },
1280
+ children: [
1281
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h2", { style: { marginTop: 0, marginBottom: "1rem" }, children: "Files" }),
1282
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1283
+ "div",
1284
+ {
1285
+ style: {
1286
+ marginBottom: "1rem",
1287
+ fontSize: "0.875rem",
1288
+ color: "#666"
1289
+ },
1290
+ children: [
1291
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1292
+ "span",
1293
+ {
1294
+ onClick: () => setCurrentPath(""),
1295
+ style: {
1296
+ cursor: "pointer",
1297
+ color: "#0070f3",
1298
+ textDecoration: "underline"
1299
+ },
1300
+ children: "content"
1301
+ }
1302
+ ),
1303
+ currentPath && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
1304
+ " / ",
1305
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: { fontWeight: "bold" }, children: currentPath })
1306
+ ] })
1307
+ ]
1308
+ }
1309
+ ),
1310
+ currentPath && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1311
+ "button",
1312
+ {
1313
+ onClick: goUp,
1314
+ style: {
1315
+ width: "100%",
1316
+ marginBottom: "1rem",
1317
+ padding: "0.5rem",
1318
+ background: "#f5f5f5",
1319
+ border: "1px solid #ddd",
1320
+ borderRadius: "4px",
1321
+ cursor: "pointer"
1322
+ },
1323
+ children: "\u2B06\uFE0F Go Up"
1324
+ }
1325
+ ),
1326
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1327
+ "div",
1328
+ {
1329
+ style: {
1330
+ marginBottom: "1.5rem",
1331
+ paddingBottom: "1.5rem",
1332
+ borderBottom: "1px solid #eee"
1333
+ },
1334
+ children: [
1335
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1336
+ "input",
1337
+ {
1338
+ type: "text",
1339
+ placeholder: "new-file.md",
1340
+ value: newFileName,
1341
+ onChange: (e) => setNewFileName(e.target.value),
1342
+ style: {
1343
+ width: "100%",
1344
+ padding: "0.5rem",
1345
+ marginBottom: "0.5rem",
1346
+ border: "1px solid #ddd",
1347
+ borderRadius: "4px"
1348
+ }
1349
+ }
1350
+ ),
1351
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1352
+ "button",
1353
+ {
1354
+ onClick: createFile,
1355
+ disabled: loading || !newFileName,
1356
+ style: {
1357
+ width: "100%",
1358
+ padding: "0.5rem",
1359
+ background: "#0070f3",
1360
+ color: "white",
1361
+ border: "none",
1362
+ borderRadius: "4px",
1363
+ cursor: "pointer",
1364
+ opacity: loading ? 0.7 : 1
1365
+ },
1366
+ children: "+ New File"
1367
+ }
1368
+ )
1369
+ ]
1370
+ }
1371
+ ),
1372
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("ul", { style: { listStyle: "none", padding: 0, margin: 0 }, children: [
1373
+ entries.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("li", { style: { color: "#999", fontStyle: "italic" }, children: "Empty directory" }),
1374
+ entries.map((entry) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1375
+ "li",
1376
+ {
1377
+ style: {
1378
+ marginBottom: "0.5rem",
1379
+ display: "flex",
1380
+ flexDirection: "column"
1381
+ },
1382
+ children: entry.type === "directory" ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1383
+ "button",
1384
+ {
1385
+ onClick: () => navigateToDir(entry.path),
1386
+ style: {
1387
+ background: "none",
1388
+ border: "none",
1389
+ color: "#0070f3",
1390
+ cursor: "pointer",
1391
+ textAlign: "left",
1392
+ fontSize: "1rem",
1393
+ padding: "0.25rem 0",
1394
+ fontWeight: "bold"
1395
+ },
1396
+ children: [
1397
+ "\u{1F4C1} ",
1398
+ entry.name
1399
+ ]
1400
+ }
1401
+ ) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1402
+ "div",
1403
+ {
1404
+ style: {
1405
+ display: "flex",
1406
+ justifyContent: "space-between",
1407
+ alignItems: "center"
1408
+ },
1409
+ children: [
1410
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1411
+ "button",
1412
+ {
1413
+ onClick: () => loadFile(entry.path),
1414
+ style: {
1415
+ background: "none",
1416
+ border: "none",
1417
+ color: selectedFile === entry.path ? "#000" : "#444",
1418
+ cursor: "pointer",
1419
+ textAlign: "left",
1420
+ padding: "0.25rem 0",
1421
+ fontWeight: selectedFile === entry.path ? "bold" : "normal",
1422
+ textDecoration: selectedFile === entry.path ? "underline" : "none",
1423
+ flex: 1
1424
+ },
1425
+ children: [
1426
+ "\u{1F4C4} ",
1427
+ entry.name
1428
+ ]
1429
+ }
1430
+ ),
1431
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1432
+ "button",
1433
+ {
1434
+ onClick: () => deleteFile(entry.path),
1435
+ disabled: loading,
1436
+ style: {
1437
+ background: "none",
1438
+ border: "none",
1439
+ color: "#d32f2f",
1440
+ cursor: "pointer",
1441
+ fontSize: "0.8rem",
1442
+ opacity: 0.7
1443
+ },
1444
+ children: "\u2715"
1445
+ }
1446
+ )
1447
+ ]
1448
+ }
1449
+ )
1450
+ },
1451
+ entry.path
1452
+ ))
1453
+ ] })
1454
+ ]
1455
+ }
1456
+ ),
1457
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1458
+ "div",
1459
+ {
1460
+ style: {
1461
+ background: "white",
1462
+ padding: "2rem",
1463
+ borderRadius: "8px",
1464
+ boxShadow: "0 2px 4px rgba(0,0,0,0.1)"
1465
+ },
1466
+ children: selectedFile ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
1467
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1468
+ "div",
1469
+ {
1470
+ style: {
1471
+ display: "flex",
1472
+ justifyContent: "space-between",
1473
+ alignItems: "center",
1474
+ marginBottom: "2rem",
1475
+ paddingBottom: "1rem",
1476
+ borderBottom: "1px solid #eee"
1477
+ },
1478
+ children: [
1479
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("h2", { style: { margin: 0 }, children: [
1480
+ "Edit: ",
1481
+ selectedFile
1482
+ ] }),
1483
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", gap: "0.5rem" }, children: [
1484
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1485
+ "button",
1486
+ {
1487
+ onClick: duplicateFile,
1488
+ disabled: loading,
1489
+ style: {
1490
+ padding: "0.5rem 1rem",
1491
+ background: "#f5f5f5",
1492
+ border: "1px solid #ddd",
1493
+ borderRadius: "4px",
1494
+ cursor: "pointer"
1495
+ },
1496
+ children: "\u{1F4CB} Duplicate"
1497
+ }
1498
+ ),
1499
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1500
+ "button",
1501
+ {
1502
+ onClick: saveFile,
1503
+ disabled: loading,
1504
+ style: {
1505
+ padding: "0.5rem 1rem",
1506
+ background: "#0070f3",
1507
+ color: "white",
1508
+ border: "none",
1509
+ borderRadius: "4px",
1510
+ cursor: "pointer",
1511
+ opacity: loading ? 0.7 : 1
1512
+ },
1513
+ children: loading ? "Saving..." : "\u{1F4BE} Save Changes"
1514
+ }
1515
+ )
1516
+ ] })
1517
+ ]
1518
+ }
1519
+ ),
1520
+ message && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1521
+ "div",
1522
+ {
1523
+ style: {
1524
+ marginBottom: "1.5rem",
1525
+ padding: "0.75rem",
1526
+ background: message.includes("\u2705") ? "#e6ffe6" : "#ffe6e6",
1527
+ borderRadius: "4px",
1528
+ border: message.includes("\u2705") ? "1px solid #a5d6a7" : "1px solid #ef9a9a"
1529
+ },
1530
+ children: message
1531
+ }
1532
+ ),
1533
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { marginBottom: "2rem" }, children: [
1534
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1535
+ "div",
1536
+ {
1537
+ style: {
1538
+ display: "flex",
1539
+ justifyContent: "space-between",
1540
+ alignItems: "center",
1541
+ marginBottom: "1rem"
1542
+ },
1543
+ children: [
1544
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h3", { style: { fontSize: "1.1rem", margin: 0 }, children: "Fields" }),
1545
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1546
+ "button",
1547
+ {
1548
+ onClick: addField,
1549
+ style: {
1550
+ fontSize: "0.875rem",
1551
+ padding: "0.4rem 0.8rem",
1552
+ background: "#e1f5fe",
1553
+ color: "#0288d1",
1554
+ border: "none",
1555
+ borderRadius: "4px",
1556
+ cursor: "pointer"
1557
+ },
1558
+ children: "+ Add Field"
1559
+ }
1560
+ )
1561
+ ]
1562
+ }
1563
+ ),
1564
+ Object.keys(fields).length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1565
+ "div",
1566
+ {
1567
+ style: {
1568
+ padding: "2rem",
1569
+ textAlign: "center",
1570
+ background: "#f9f9f9",
1571
+ borderRadius: "4px",
1572
+ border: "1px dashed #ccc"
1573
+ },
1574
+ children: [
1575
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1576
+ "p",
1577
+ {
1578
+ style: { color: "#666", fontStyle: "italic", margin: 0 },
1579
+ children: 'No fields yet. Click "+ Add Field" to start.'
1580
+ }
1581
+ ),
1582
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1583
+ "small",
1584
+ {
1585
+ style: {
1586
+ color: "#999",
1587
+ marginTop: "0.5rem",
1588
+ display: "block"
1589
+ },
1590
+ children: "Add 'content' field for the main body"
1591
+ }
1592
+ )
1593
+ ]
1594
+ }
1595
+ ) : Object.entries(fields).map(([key, field]) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1596
+ "div",
1597
+ {
1598
+ style: {
1599
+ marginBottom: "1.5rem",
1600
+ background: "#fff",
1601
+ border: "1px solid #eee",
1602
+ padding: "1rem",
1603
+ borderRadius: "6px"
1604
+ },
1605
+ children: [
1606
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1607
+ "div",
1608
+ {
1609
+ style: {
1610
+ display: "flex",
1611
+ justifyContent: "space-between",
1612
+ alignItems: "center",
1613
+ marginBottom: "0.5rem"
1614
+ },
1615
+ children: [
1616
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { htmlFor: key, style: { fontWeight: 500 }, children: [
1617
+ key,
1618
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1619
+ "span",
1620
+ {
1621
+ style: {
1622
+ marginLeft: "0.5rem",
1623
+ fontSize: "0.75rem",
1624
+ color: "#999",
1625
+ background: "#f0f0f0",
1626
+ padding: "0.1rem 0.4rem",
1627
+ borderRadius: "4px"
1628
+ },
1629
+ children: field.type
1630
+ }
1631
+ )
1632
+ ] }),
1633
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1634
+ "button",
1635
+ {
1636
+ onClick: () => removeField(key),
1637
+ style: {
1638
+ background: "none",
1639
+ border: "none",
1640
+ color: "#e57373",
1641
+ cursor: "pointer",
1642
+ fontSize: "0.875rem"
1643
+ },
1644
+ children: "\u2715 Remove"
1645
+ }
1646
+ )
1647
+ ]
1648
+ }
1649
+ ),
1650
+ renderField(key, field)
1651
+ ]
1652
+ },
1653
+ key
1654
+ ))
1655
+ ] }),
1656
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1657
+ "div",
1658
+ {
1659
+ style: {
1660
+ marginTop: "3rem",
1661
+ paddingTop: "1rem",
1662
+ borderTop: "1px solid #eee"
1663
+ },
1664
+ children: [
1665
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1666
+ "h3",
1667
+ {
1668
+ style: {
1669
+ fontSize: "1rem",
1670
+ marginBottom: "0.5rem",
1671
+ color: "#666"
1672
+ },
1673
+ children: "Quick File Upload"
1674
+ }
1675
+ ),
1676
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", gap: "0.5rem" }, children: [
1677
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1678
+ "input",
1679
+ {
1680
+ type: "file",
1681
+ onChange: handleFileUpload,
1682
+ disabled: uploading,
1683
+ style: { flex: 1 }
1684
+ }
1685
+ ),
1686
+ uploading && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: { color: "#666" }, children: "Uploading..." })
1687
+ ] })
1688
+ ]
1689
+ }
1690
+ )
1691
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1692
+ "div",
1693
+ {
1694
+ style: {
1695
+ textAlign: "center",
1696
+ padding: "5rem 2rem",
1697
+ color: "#999"
1698
+ },
1699
+ children: [
1700
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { fontSize: "3rem", marginBottom: "1rem" }, children: "\u{1F448}" }),
1701
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: { fontSize: "1.1rem" }, children: "Select a file from the sidebar to edit" }),
1702
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { children: "or create a new file to get started" })
1703
+ ]
1704
+ }
1705
+ )
1706
+ }
1707
+ )
1708
+ ]
1709
+ }
1710
+ )
1711
+ ]
1712
+ }
1713
+ );
1714
+ }
1715
+
1716
+ // src/ui/Login.tsx
1717
+ var import_react2 = require("react");
1718
+ var import_jsx_runtime2 = require("react/jsx-runtime");
1719
+ function Login() {
1720
+ const [username, setUsername] = (0, import_react2.useState)("");
1721
+ const [password, setPassword] = (0, import_react2.useState)("");
1722
+ const [error, setError] = (0, import_react2.useState)("");
1723
+ const [loading, setLoading] = (0, import_react2.useState)(false);
1724
+ const handleSubmit = async (e) => {
1725
+ e.preventDefault();
1726
+ setError("");
1727
+ setLoading(true);
1728
+ try {
1729
+ const response = await fetch("/api/cms/login", {
1730
+ method: "POST",
1731
+ headers: { "Content-Type": "application/json" },
1732
+ body: JSON.stringify({ username, password })
1733
+ });
1734
+ const data = await response.json();
1735
+ if (response.ok) {
1736
+ window.location.href = "/admin";
1737
+ } else {
1738
+ setError(data.error || "Login failed");
1739
+ }
1740
+ } catch (err) {
1741
+ setError("An error occurred. Please try again.");
1742
+ } finally {
1743
+ setLoading(false);
1744
+ }
1745
+ };
1746
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1747
+ "div",
1748
+ {
1749
+ style: {
1750
+ display: "flex",
1751
+ justifyContent: "center",
1752
+ alignItems: "center",
1753
+ minHeight: "100vh",
1754
+ background: "#f5f5f5",
1755
+ fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif'
1756
+ },
1757
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
1758
+ "div",
1759
+ {
1760
+ style: {
1761
+ width: "100%",
1762
+ maxWidth: "400px",
1763
+ padding: "2rem",
1764
+ background: "white",
1765
+ borderRadius: "8px",
1766
+ boxShadow: "0 4px 6px rgba(0,0,0,0.1)"
1767
+ },
1768
+ children: [
1769
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("h1", { style: { marginBottom: "0.5rem", textAlign: "center" }, children: "Admin Login" }),
1770
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { style: { color: "#666", marginBottom: "2rem", textAlign: "center" }, children: "Enter your credentials to access the CMS" }),
1771
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("form", { onSubmit: handleSubmit, children: [
1772
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: { marginBottom: "1rem" }, children: [
1773
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1774
+ "label",
1775
+ {
1776
+ htmlFor: "username",
1777
+ style: {
1778
+ display: "block",
1779
+ marginBottom: "0.5rem",
1780
+ fontWeight: 500
1781
+ },
1782
+ children: "Username"
1783
+ }
1784
+ ),
1785
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1786
+ "input",
1787
+ {
1788
+ id: "username",
1789
+ type: "text",
1790
+ value: username,
1791
+ onChange: (e) => setUsername(e.target.value),
1792
+ required: true,
1793
+ autoFocus: true,
1794
+ style: {
1795
+ width: "100%",
1796
+ padding: "0.75rem",
1797
+ border: "1px solid #ddd",
1798
+ borderRadius: "4px"
1799
+ }
1800
+ }
1801
+ )
1802
+ ] }),
1803
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: { marginBottom: "1.5rem" }, children: [
1804
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1805
+ "label",
1806
+ {
1807
+ htmlFor: "password",
1808
+ style: {
1809
+ display: "block",
1810
+ marginBottom: "0.5rem",
1811
+ fontWeight: 500
1812
+ },
1813
+ children: "Password"
1814
+ }
1815
+ ),
1816
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1817
+ "input",
1818
+ {
1819
+ id: "password",
1820
+ type: "password",
1821
+ value: password,
1822
+ onChange: (e) => setPassword(e.target.value),
1823
+ required: true,
1824
+ style: {
1825
+ width: "100%",
1826
+ padding: "0.75rem",
1827
+ border: "1px solid #ddd",
1828
+ borderRadius: "4px"
1829
+ }
1830
+ }
1831
+ )
1832
+ ] }),
1833
+ error && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1834
+ "div",
1835
+ {
1836
+ style: {
1837
+ marginBottom: "1rem",
1838
+ padding: "0.75rem",
1839
+ background: "#ffe6e6",
1840
+ color: "#d32f2f",
1841
+ borderRadius: "4px",
1842
+ fontSize: "0.875rem"
1843
+ },
1844
+ children: error
1845
+ }
1846
+ ),
1847
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1848
+ "button",
1849
+ {
1850
+ type: "submit",
1851
+ disabled: loading,
1852
+ style: {
1853
+ width: "100%",
1854
+ padding: "0.75rem",
1855
+ background: "#0070f3",
1856
+ color: "white",
1857
+ border: "none",
1858
+ borderRadius: "4px",
1859
+ fontSize: "1rem",
1860
+ fontWeight: 500,
1861
+ cursor: "pointer",
1862
+ opacity: loading ? 0.7 : 1
1863
+ },
1864
+ children: loading ? "Logging in..." : "Login"
1865
+ }
1866
+ )
1867
+ ] })
1868
+ ]
1869
+ }
1870
+ )
1871
+ }
1872
+ );
1873
+ }
1874
+
819
1875
  // src/middleware/auth.ts
820
1876
  var import_server3 = require("next/server");
821
1877
  function createAuthMiddleware(options = {}) {
@@ -849,6 +1905,16 @@ var import_path3 = __toESM(require("path"));
849
1905
  async function initCMS(config = {}) {
850
1906
  const contentDir = config.contentDir || "public/content";
851
1907
  const fullPath = import_path3.default.join(process.cwd(), contentDir);
1908
+ const appDir = import_path3.default.join(process.cwd(), "app");
1909
+ try {
1910
+ await import_promises2.default.access(appDir);
1911
+ } catch {
1912
+ console.error(
1913
+ '\u274C Error: "app" directory not found. This init script requires Next.js App Router.'
1914
+ );
1915
+ return;
1916
+ }
1917
+ console.log("\u{1F680} Initializing @fydemy/cms...");
852
1918
  await import_promises2.default.mkdir(fullPath, { recursive: true });
853
1919
  const exampleContent = `---
854
1920
  title: Example Post
@@ -870,25 +1936,145 @@ This is an example markdown file. You can edit or delete it from the admin dashb
870
1936
  `;
871
1937
  const examplePath = import_path3.default.join(fullPath, "example.md");
872
1938
  await import_promises2.default.writeFile(examplePath, exampleContent, "utf-8");
873
- console.log("\u2705 CMS initialized successfully!");
874
- console.log(`\u{1F4C1} Content directory: ${contentDir}`);
875
- console.log(`\u{1F4DD} Example file created: ${contentDir}/example.md`);
1939
+ console.log("\u2705 Created content directory and example file");
1940
+ const adminDir = import_path3.default.join(appDir, "admin");
1941
+ const loginDir = import_path3.default.join(adminDir, "login");
1942
+ await import_promises2.default.mkdir(loginDir, { recursive: true });
1943
+ await import_promises2.default.writeFile(
1944
+ import_path3.default.join(adminDir, "page.tsx"),
1945
+ `import { AdminDashboard } from '@fydemy/cms';
1946
+
1947
+ export default AdminDashboard;
1948
+ `,
1949
+ "utf-8"
1950
+ );
1951
+ await import_promises2.default.writeFile(
1952
+ import_path3.default.join(loginDir, "page.tsx"),
1953
+ `import { Login } from '@fydemy/cms';
1954
+
1955
+ export default Login;
1956
+ `,
1957
+ "utf-8"
1958
+ );
1959
+ console.log("\u2705 Created Admin UI pages");
1960
+ const apiCmsDir = import_path3.default.join(appDir, "api", "cms");
1961
+ await import_promises2.default.mkdir(import_path3.default.join(apiCmsDir, "login"), { recursive: true });
1962
+ await import_promises2.default.writeFile(
1963
+ import_path3.default.join(apiCmsDir, "login", "route.ts"),
1964
+ `import { handleLogin } from '@fydemy/cms';
1965
+ export { handleLogin as POST };
1966
+ `,
1967
+ "utf-8"
1968
+ );
1969
+ await import_promises2.default.mkdir(import_path3.default.join(apiCmsDir, "logout"), { recursive: true });
1970
+ await import_promises2.default.writeFile(
1971
+ import_path3.default.join(apiCmsDir, "logout", "route.ts"),
1972
+ `import { handleLogout } from '@fydemy/cms';
1973
+ export { handleLogout as POST };
1974
+ `,
1975
+ "utf-8"
1976
+ );
1977
+ await import_promises2.default.mkdir(import_path3.default.join(apiCmsDir, "upload"), { recursive: true });
1978
+ await import_promises2.default.writeFile(
1979
+ import_path3.default.join(apiCmsDir, "upload", "route.ts"),
1980
+ `import { handleUpload } from '@fydemy/cms';
1981
+ export { handleUpload as POST };
1982
+ `,
1983
+ "utf-8"
1984
+ );
1985
+ await import_promises2.default.mkdir(import_path3.default.join(apiCmsDir, "list", "[[...path]]"), {
1986
+ recursive: true
1987
+ });
1988
+ await import_promises2.default.writeFile(
1989
+ import_path3.default.join(apiCmsDir, "list", "[[...path]]", "route.ts"),
1990
+ `import { createListApiHandlers } from '@fydemy/cms';
1991
+
1992
+ const handlers = createListApiHandlers();
1993
+ export const GET = handlers.GET;
1994
+ `,
1995
+ "utf-8"
1996
+ );
1997
+ await import_promises2.default.mkdir(import_path3.default.join(apiCmsDir, "content", "[...path]"), {
1998
+ recursive: true
1999
+ });
2000
+ await import_promises2.default.writeFile(
2001
+ import_path3.default.join(apiCmsDir, "content", "[...path]", "route.ts"),
2002
+ `import { createContentApiHandlers } from '@fydemy/cms';
2003
+
2004
+ const handlers = createContentApiHandlers();
2005
+ export const GET = handlers.GET;
2006
+ export const POST = handlers.POST;
2007
+ export const DELETE = handlers.DELETE;
2008
+ `,
2009
+ "utf-8"
2010
+ );
2011
+ console.log("\u2705 Created API routes");
2012
+ const middlewarePath = import_path3.default.join(process.cwd(), "middleware.ts");
2013
+ try {
2014
+ await import_promises2.default.access(middlewarePath);
2015
+ console.log(
2016
+ "\u26A0\uFE0F middleware.ts already exists. Please manually add the CMS auth middleware:"
2017
+ );
2018
+ console.log(`
2019
+ import { createAuthMiddleware } from '@fydemy/cms';
2020
+ // ... existing imports
2021
+
2022
+ export function middleware(request: NextRequest) {
2023
+ // Add this:
2024
+ const authResponse = createAuthMiddleware()(request);
2025
+ if (authResponse) return authResponse;
2026
+
2027
+ // ... existing middleware logic
2028
+ }
2029
+ `);
2030
+ } catch {
2031
+ await import_promises2.default.writeFile(
2032
+ middlewarePath,
2033
+ `import { createAuthMiddleware } from '@fydemy/cms';
2034
+ import { NextRequest } from 'next/server';
2035
+
2036
+ export function middleware(request: NextRequest) {
2037
+ return createAuthMiddleware()(request);
2038
+ }
2039
+
2040
+ export const config = {
2041
+ matcher: ['/admin/:path*'],
2042
+ };
2043
+ `,
2044
+ "utf-8"
2045
+ );
2046
+ console.log("\u2705 Created middleware.ts");
2047
+ }
2048
+ const envExamplePath = import_path3.default.join(process.cwd(), ".env.local.example");
2049
+ await import_promises2.default.writeFile(
2050
+ envExamplePath,
2051
+ `CMS_ADMIN_USERNAME=admin
2052
+ CMS_ADMIN_PASSWORD=password
2053
+ CMS_SESSION_SECRET=ensure_this_is_at_least_32_chars_long_random_string
2054
+
2055
+ # GitHub Storage (Production)
2056
+ GITHUB_TOKEN=
2057
+ GITHUB_REPO=owner/repo
2058
+ GITHUB_BRANCH=main
2059
+ `,
2060
+ "utf-8"
2061
+ );
876
2062
  console.log("");
877
- console.log("Next steps:");
878
- console.log("1. Set up environment variables in .env.local:");
879
- console.log(" - CMS_ADMIN_USERNAME");
880
- console.log(" - CMS_ADMIN_PASSWORD");
881
- console.log(" - CMS_SESSION_SECRET (min 32 characters)");
882
- console.log("2. For production: GITHUB_TOKEN, GITHUB_REPO, GITHUB_BRANCH");
883
- console.log("3. Import and use the CMS utilities in your Next.js app");
2063
+ console.log("\u{1F389} CMS initialized successfully!");
2064
+ console.log(
2065
+ "1. Copy .env.local.example to .env.local and set your credentials"
2066
+ );
2067
+ console.log("2. Run your dev server and visit /admin");
884
2068
  }
885
2069
 
886
2070
  // src/index.ts
887
2071
  init_rate_limit();
888
2072
  // Annotate the CommonJS export names for ESM import in node:
889
2073
  0 && (module.exports = {
2074
+ AdminDashboard,
890
2075
  GitHubStorage,
891
2076
  LocalStorage,
2077
+ Login,
892
2078
  MAX_FILE_SIZE,
893
2079
  MAX_PASSWORD_LENGTH,
894
2080
  MAX_USERNAME_LENGTH,