@fydemy/cms 1.0.1 → 1.0.3
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/README.md +23 -56
- package/dist/admin.template.tsx +661 -0
- package/dist/bin.d.mts +1 -0
- package/dist/bin.d.ts +1 -0
- package/dist/bin.js +276 -0
- package/dist/bin.js.map +1 -0
- package/dist/bin.mjs +253 -0
- package/dist/bin.mjs.map +1 -0
- package/dist/components/ui/badge.tsx +36 -0
- package/dist/components/ui/button.tsx +56 -0
- package/dist/components/ui/card.tsx +86 -0
- package/dist/components/ui/input.tsx +25 -0
- package/dist/components/ui/label.tsx +24 -0
- package/dist/components/ui/textarea.tsx +24 -0
- package/dist/components.json +16 -0
- package/dist/index.d.mts +6 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.js +1273 -10
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1271 -10
- package/dist/index.mjs.map +1 -1
- package/dist/lib/utils.ts +6 -0
- package/dist/login.template.tsx +126 -0
- package/package.json +9 -1
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 = {}) {
|
|
@@ -775,9 +1829,64 @@ function createAuthMiddleware(options = {}) {
|
|
|
775
1829
|
// src/init/setup.ts
|
|
776
1830
|
import fs2 from "fs/promises";
|
|
777
1831
|
import path3 from "path";
|
|
1832
|
+
import { exec } from "child_process";
|
|
1833
|
+
import { promisify } from "util";
|
|
1834
|
+
var execAsync = promisify(exec);
|
|
778
1835
|
async function initCMS(config = {}) {
|
|
779
1836
|
const contentDir = config.contentDir || "public/content";
|
|
780
1837
|
const fullPath = path3.join(process.cwd(), contentDir);
|
|
1838
|
+
const appDir = path3.join(process.cwd(), "app");
|
|
1839
|
+
try {
|
|
1840
|
+
await fs2.access(appDir);
|
|
1841
|
+
} catch {
|
|
1842
|
+
console.error(
|
|
1843
|
+
'\u274C Error: "app" directory not found. This init script requires Next.js App Router.'
|
|
1844
|
+
);
|
|
1845
|
+
return;
|
|
1846
|
+
}
|
|
1847
|
+
console.log("\u{1F680} Initializing @fydemy/cms...");
|
|
1848
|
+
console.log("\u{1F4E6} Checking dependencies...");
|
|
1849
|
+
try {
|
|
1850
|
+
const packageJsonPath = path3.join(process.cwd(), "package.json");
|
|
1851
|
+
const packageJson = JSON.parse(await fs2.readFile(packageJsonPath, "utf-8"));
|
|
1852
|
+
const dependencies = {
|
|
1853
|
+
...packageJson.dependencies,
|
|
1854
|
+
...packageJson.devDependencies
|
|
1855
|
+
};
|
|
1856
|
+
const missingDeps = [];
|
|
1857
|
+
if (!dependencies["@fydemy/cms"]) missingDeps.push("@fydemy/cms");
|
|
1858
|
+
if (!dependencies["tailwindcss"]) missingDeps.push("tailwindcss");
|
|
1859
|
+
if (!dependencies["class-variance-authority"])
|
|
1860
|
+
missingDeps.push("class-variance-authority");
|
|
1861
|
+
if (!dependencies["clsx"]) missingDeps.push("clsx");
|
|
1862
|
+
if (!dependencies["tailwind-merge"]) missingDeps.push("tailwind-merge");
|
|
1863
|
+
if (!dependencies["@radix-ui/react-slot"])
|
|
1864
|
+
missingDeps.push("@radix-ui/react-slot");
|
|
1865
|
+
if (!dependencies["@radix-ui/react-label"])
|
|
1866
|
+
missingDeps.push("@radix-ui/react-label");
|
|
1867
|
+
if (missingDeps.length > 0) {
|
|
1868
|
+
console.log(
|
|
1869
|
+
`\u{1F527} Installing missing dependencies: ${missingDeps.join(", ")}...`
|
|
1870
|
+
);
|
|
1871
|
+
let installCmd = "npm install";
|
|
1872
|
+
try {
|
|
1873
|
+
await fs2.access("pnpm-lock.yaml");
|
|
1874
|
+
installCmd = "pnpm add";
|
|
1875
|
+
} catch {
|
|
1876
|
+
try {
|
|
1877
|
+
await fs2.access("yarn.lock");
|
|
1878
|
+
installCmd = "yarn add";
|
|
1879
|
+
} catch {
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1882
|
+
await execAsync(`${installCmd} ${missingDeps.join(" ")}`);
|
|
1883
|
+
console.log("\u2705 Dependencies installed");
|
|
1884
|
+
}
|
|
1885
|
+
} catch (error) {
|
|
1886
|
+
console.warn(
|
|
1887
|
+
"\u26A0\uFE0F Could not check/install dependencies automatically. Please ensure all dependencies are installed."
|
|
1888
|
+
);
|
|
1889
|
+
}
|
|
781
1890
|
await fs2.mkdir(fullPath, { recursive: true });
|
|
782
1891
|
const exampleContent = `---
|
|
783
1892
|
title: Example Post
|
|
@@ -799,24 +1908,176 @@ This is an example markdown file. You can edit or delete it from the admin dashb
|
|
|
799
1908
|
`;
|
|
800
1909
|
const examplePath = path3.join(fullPath, "example.md");
|
|
801
1910
|
await fs2.writeFile(examplePath, exampleContent, "utf-8");
|
|
802
|
-
console.log("\u2705
|
|
803
|
-
|
|
804
|
-
|
|
1911
|
+
console.log("\u2705 Created content directory and example file");
|
|
1912
|
+
const adminDir = path3.join(appDir, "admin");
|
|
1913
|
+
const loginDir = path3.join(adminDir, "login");
|
|
1914
|
+
await fs2.mkdir(loginDir, { recursive: true });
|
|
1915
|
+
const loginTemplate = await fs2.readFile(
|
|
1916
|
+
path3.join(__dirname, "login.template.tsx"),
|
|
1917
|
+
"utf-8"
|
|
1918
|
+
);
|
|
1919
|
+
const adminTemplate = await fs2.readFile(
|
|
1920
|
+
path3.join(__dirname, "admin.template.tsx"),
|
|
1921
|
+
"utf-8"
|
|
1922
|
+
);
|
|
1923
|
+
await fs2.writeFile(path3.join(adminDir, "page.tsx"), adminTemplate, "utf-8");
|
|
1924
|
+
await fs2.writeFile(path3.join(loginDir, "page.tsx"), loginTemplate, "utf-8");
|
|
1925
|
+
console.log("\u2705 Scaffolded Admin UI (shadcn/ui components)");
|
|
1926
|
+
const componentsDir = path3.join(process.cwd(), "components", "ui");
|
|
1927
|
+
const libDir = path3.join(process.cwd(), "lib");
|
|
1928
|
+
await fs2.mkdir(componentsDir, { recursive: true });
|
|
1929
|
+
await fs2.mkdir(libDir, { recursive: true });
|
|
1930
|
+
const utilsTemplate = await fs2.readFile(
|
|
1931
|
+
path3.join(__dirname, "lib", "utils.ts"),
|
|
1932
|
+
"utf-8"
|
|
1933
|
+
);
|
|
1934
|
+
await fs2.writeFile(path3.join(libDir, "utils.ts"), utilsTemplate, "utf-8");
|
|
1935
|
+
const componentFiles = [
|
|
1936
|
+
"button.tsx",
|
|
1937
|
+
"input.tsx",
|
|
1938
|
+
"card.tsx",
|
|
1939
|
+
"label.tsx",
|
|
1940
|
+
"textarea.tsx",
|
|
1941
|
+
"badge.tsx"
|
|
1942
|
+
];
|
|
1943
|
+
for (const componentFile of componentFiles) {
|
|
1944
|
+
const componentTemplate = await fs2.readFile(
|
|
1945
|
+
path3.join(__dirname, "components", "ui", componentFile),
|
|
1946
|
+
"utf-8"
|
|
1947
|
+
);
|
|
1948
|
+
await fs2.writeFile(
|
|
1949
|
+
path3.join(componentsDir, componentFile),
|
|
1950
|
+
componentTemplate,
|
|
1951
|
+
"utf-8"
|
|
1952
|
+
);
|
|
1953
|
+
}
|
|
1954
|
+
const componentsJsonTemplate = await fs2.readFile(
|
|
1955
|
+
path3.join(__dirname, "components.json"),
|
|
1956
|
+
"utf-8"
|
|
1957
|
+
);
|
|
1958
|
+
await fs2.writeFile(
|
|
1959
|
+
path3.join(process.cwd(), "components.json"),
|
|
1960
|
+
componentsJsonTemplate,
|
|
1961
|
+
"utf-8"
|
|
1962
|
+
);
|
|
1963
|
+
console.log("\u2705 Scaffolded shadcn/ui components");
|
|
1964
|
+
const apiCmsDir = path3.join(appDir, "api", "cms");
|
|
1965
|
+
await fs2.mkdir(path3.join(apiCmsDir, "login"), { recursive: true });
|
|
1966
|
+
await fs2.writeFile(
|
|
1967
|
+
path3.join(apiCmsDir, "login", "route.ts"),
|
|
1968
|
+
`import { handleLogin } from '@fydemy/cms';
|
|
1969
|
+
export { handleLogin as POST };
|
|
1970
|
+
`,
|
|
1971
|
+
"utf-8"
|
|
1972
|
+
);
|
|
1973
|
+
await fs2.mkdir(path3.join(apiCmsDir, "logout"), { recursive: true });
|
|
1974
|
+
await fs2.writeFile(
|
|
1975
|
+
path3.join(apiCmsDir, "logout", "route.ts"),
|
|
1976
|
+
`import { handleLogout } from '@fydemy/cms';
|
|
1977
|
+
export { handleLogout as POST };
|
|
1978
|
+
`,
|
|
1979
|
+
"utf-8"
|
|
1980
|
+
);
|
|
1981
|
+
await fs2.mkdir(path3.join(apiCmsDir, "upload"), { recursive: true });
|
|
1982
|
+
await fs2.writeFile(
|
|
1983
|
+
path3.join(apiCmsDir, "upload", "route.ts"),
|
|
1984
|
+
`import { handleUpload } from '@fydemy/cms';
|
|
1985
|
+
export { handleUpload as POST };
|
|
1986
|
+
`,
|
|
1987
|
+
"utf-8"
|
|
1988
|
+
);
|
|
1989
|
+
await fs2.mkdir(path3.join(apiCmsDir, "list", "[[...path]]"), {
|
|
1990
|
+
recursive: true
|
|
1991
|
+
});
|
|
1992
|
+
await fs2.writeFile(
|
|
1993
|
+
path3.join(apiCmsDir, "list", "[[...path]]", "route.ts"),
|
|
1994
|
+
`import { createListApiHandlers } from '@fydemy/cms';
|
|
1995
|
+
|
|
1996
|
+
const handlers = createListApiHandlers();
|
|
1997
|
+
export const GET = handlers.GET;
|
|
1998
|
+
`,
|
|
1999
|
+
"utf-8"
|
|
2000
|
+
);
|
|
2001
|
+
await fs2.mkdir(path3.join(apiCmsDir, "content", "[...path]"), {
|
|
2002
|
+
recursive: true
|
|
2003
|
+
});
|
|
2004
|
+
await fs2.writeFile(
|
|
2005
|
+
path3.join(apiCmsDir, "content", "[...path]", "route.ts"),
|
|
2006
|
+
`import { createContentApiHandlers } from '@fydemy/cms';
|
|
2007
|
+
|
|
2008
|
+
const handlers = createContentApiHandlers();
|
|
2009
|
+
export const GET = handlers.GET;
|
|
2010
|
+
export const POST = handlers.POST;
|
|
2011
|
+
export const DELETE = handlers.DELETE;
|
|
2012
|
+
`,
|
|
2013
|
+
"utf-8"
|
|
2014
|
+
);
|
|
2015
|
+
console.log("\u2705 Created API routes");
|
|
2016
|
+
const middlewarePath = path3.join(process.cwd(), "middleware.ts");
|
|
2017
|
+
try {
|
|
2018
|
+
await fs2.access(middlewarePath);
|
|
2019
|
+
console.log(
|
|
2020
|
+
"\u26A0\uFE0F middleware.ts already exists. Please manually add the CMS auth middleware:"
|
|
2021
|
+
);
|
|
2022
|
+
console.log(`
|
|
2023
|
+
import { createAuthMiddleware } from '@fydemy/cms';
|
|
2024
|
+
// ... existing imports
|
|
2025
|
+
|
|
2026
|
+
export function middleware(request: NextRequest) {
|
|
2027
|
+
// Add this:
|
|
2028
|
+
const authResponse = createAuthMiddleware()(request);
|
|
2029
|
+
if (authResponse) return authResponse;
|
|
2030
|
+
|
|
2031
|
+
// ... existing middleware logic
|
|
2032
|
+
}
|
|
2033
|
+
`);
|
|
2034
|
+
} catch {
|
|
2035
|
+
await fs2.writeFile(
|
|
2036
|
+
middlewarePath,
|
|
2037
|
+
`import { createAuthMiddleware } from '@fydemy/cms';
|
|
2038
|
+
import { NextRequest } from 'next/server';
|
|
2039
|
+
|
|
2040
|
+
export function middleware(request: NextRequest) {
|
|
2041
|
+
return createAuthMiddleware()(request);
|
|
2042
|
+
}
|
|
2043
|
+
|
|
2044
|
+
export const config = {
|
|
2045
|
+
matcher: ['/admin/:path*'],
|
|
2046
|
+
};
|
|
2047
|
+
`,
|
|
2048
|
+
"utf-8"
|
|
2049
|
+
);
|
|
2050
|
+
console.log("\u2705 Created middleware.ts");
|
|
2051
|
+
}
|
|
2052
|
+
const envExamplePath = path3.join(process.cwd(), ".env.local.example");
|
|
2053
|
+
await fs2.writeFile(
|
|
2054
|
+
envExamplePath,
|
|
2055
|
+
`CMS_ADMIN_USERNAME=admin
|
|
2056
|
+
CMS_ADMIN_PASSWORD=password
|
|
2057
|
+
CMS_SESSION_SECRET=ensure_this_is_at_least_32_chars_long_random_string
|
|
2058
|
+
|
|
2059
|
+
# GitHub Storage (Production)
|
|
2060
|
+
GITHUB_TOKEN=
|
|
2061
|
+
GITHUB_REPO=owner/repo
|
|
2062
|
+
GITHUB_BRANCH=main
|
|
2063
|
+
`,
|
|
2064
|
+
"utf-8"
|
|
2065
|
+
);
|
|
805
2066
|
console.log("");
|
|
806
|
-
console.log("
|
|
807
|
-
console.log(
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
console.log("
|
|
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");
|
|
2067
|
+
console.log("\u{1F389} CMS initialized successfully!");
|
|
2068
|
+
console.log(
|
|
2069
|
+
"1. Copy .env.local.example to .env.local and set your credentials"
|
|
2070
|
+
);
|
|
2071
|
+
console.log("2. Run your dev server and visit /admin");
|
|
813
2072
|
}
|
|
814
2073
|
|
|
815
2074
|
// src/index.ts
|
|
816
2075
|
init_rate_limit();
|
|
817
2076
|
export {
|
|
2077
|
+
AdminDashboard,
|
|
818
2078
|
GitHubStorage,
|
|
819
2079
|
LocalStorage,
|
|
2080
|
+
Login,
|
|
820
2081
|
MAX_FILE_SIZE,
|
|
821
2082
|
MAX_PASSWORD_LENGTH,
|
|
822
2083
|
MAX_USERNAME_LENGTH,
|