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