index.html 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370
  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <!-- HTML Meta Tags -->
  5. <title>Welcome to My Blog</title>
  6. <meta name="description" content="A personal blog hosted on WebStick!">
  7. <meta name="viewport" content="width=device-width, initial-scale=1" >
  8. <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
  9. <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.4.1/semantic.min.css" />
  10. <script src="https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.4.1/semantic.min.js"></script>
  11. <script src="https://cdnjs.cloudflare.com/ajax/libs/showdown/2.1.0/showdown.min.js"></script>
  12. <style>
  13. .imgwrapper{
  14. max-height: 200px;
  15. overflow: hidden;
  16. }
  17. .homebtn{
  18. cursor: pointer;
  19. color: grey;
  20. }
  21. .homebtn:hover{
  22. opacity: 0.5;
  23. }
  24. </style>
  25. </head>
  26. <body>
  27. <br>
  28. <div class="ui text container">
  29. <h2 class="ui header">
  30. <span class="homebtn" title="Homepage" onclick="window.location.href = '../';"><i class="ui chevron left circle icon"></i></span>
  31. <span id="blogtitle">WebStick Blog</span> <span onclick="editBlogTitle();" class="adminOnly"><i class="ui small edit grey icon"></i></span>
  32. <div class="sub header"><span id="blogsubtitle">Welcome to this new personal blog of mine!</span> <span class="adminOnly" onclick="editBlogSubtitle();"><i class="ui edit icon"></i></span></div>
  33. </h2>
  34. <div class="adminOnly">
  35. <div class="ui divider"></div>
  36. <button id="createNewPostBtn" class="ui basic button"><i class="blue add icon"></i> New Post</button>
  37. <button onclick="forceUpdatePostIndex()" class="ui basic button"><i class="green refresh icon"></i> Force Update Post Index</button>
  38. <br>
  39. </div>
  40. <div id="newPostModal" class="ui basic segment" style="display:none;">
  41. <div class="ui divider"></div>
  42. <h3><i class="ui blue add icon"></i> Create a new Post</h3>
  43. <p>New post in WebStick Blog Engine are markdown files strored in the posts/ folder. Create a new post with filename as title (exclude the .md extension when filling in the filename).<br>
  44. A new window with markdown editor will be opened. After finish writing, save & close the markdown editor and refresh this page to see your post.</p>
  45. <div class="ui action fluid input">
  46. <input id="newPostTitle" type="text" maxlength="16">
  47. <button id="confirmNewPostBtn" onclick="createNewPost();" class="ui green button">Create & Edit</button>
  48. </div>
  49. </div>
  50. <div id="nopost" class="ui message" style="display:none;">
  51. <h4 class="ui header">
  52. <i class="green check icon"></i>
  53. <div class="content">
  54. Seems this blog has nothing posted. Check back later!
  55. <div class="sub header">You own the place? Try create a new post after login.</div>
  56. </div>
  57. </h4>
  58. </div>
  59. <div id="posttable">
  60. <div class="ui basic segment">
  61. <i class="ui loading spinner icon"></i> Loading Posts
  62. </div>
  63. </div>
  64. </div>
  65. <div class="ui text container center aligned">
  66. <div class="ui divider"></div>
  67. <small>Proudly powered by <a href="https://hackaday.io/project/192618-instant-webstick-esp8266-web-server-nas">WebStick</a></small>
  68. </div>
  69. </div>
  70. <script>
  71. let loggedIn = false;
  72. //Check the user has logged in
  73. $.get("/api/auth/chk", function(data){
  74. if (data == false){
  75. //User cannot use admin function. Hide the buttons.
  76. $(".adminOnly").hide();
  77. loggedIn = false;
  78. }else{
  79. loggedIn = true;
  80. }
  81. loadValue("blog-posts", function(){
  82. initPosts();
  83. })
  84. });
  85. //Initialize blog info
  86. function initBlogInfo(){
  87. loadValue("blog-title", function(title){
  88. if (title.error != undefined || title == ""){
  89. title = "WebStick Blog";
  90. }
  91. document.title = decodeURIComponent(title);
  92. $("#blogtitle").text(decodeURIComponent(title));
  93. });
  94. loadValue("blog-subtitle", function(title){
  95. if (title.error != undefined || title == ""){
  96. title = "Welcome to my personal blog!";
  97. }
  98. $("#blogsubtitle").text(decodeURIComponent(title));
  99. });
  100. }
  101. initBlogInfo();
  102. //Edit blog title and subtitles
  103. function editBlogSubtitle(){
  104. let newtitle = prompt("New Blog Subtitle", "");
  105. if (newtitle != null) {
  106. setValue("blog-subtitle", encodeURIComponent(newtitle), function(){
  107. initBlogInfo();
  108. })
  109. }
  110. }
  111. function editBlogTitle(){
  112. let newtitle = prompt("New Blog Title", "");
  113. if (newtitle != null) {
  114. setValue("blog-title", encodeURIComponent(newtitle), function(){
  115. initBlogInfo();
  116. })
  117. }
  118. }
  119. //Storage and loader utils
  120. function setValue(key, value, callback){
  121. $.get("/api/pref/set?key=" + key + "&value=" + value, function(data){
  122. callback(data);
  123. });
  124. }
  125. function loadValue(key, callback){
  126. $.get("/api/pref/get?key=" + key, function(data){
  127. callback(data);
  128. });
  129. }
  130. /*
  131. New Post
  132. New post is created via creating a markdown file in the server
  133. side and open it with the markdown editor
  134. */
  135. $("#createNewPostBtn").on("click", function(){
  136. $("#newPostModal").toggle("fast");
  137. });
  138. function createNewPost(){
  139. let filename = $("#newPostTitle").val().trim();
  140. if (filename == ""){
  141. alert("Post title cannot be empty.");
  142. return;
  143. }
  144. if (filename.indexOf("/") >= 0){
  145. //Contains /. Reject
  146. alert("File name cannot contain path seperator");
  147. return;
  148. }
  149. $("#confirmNewPostBtn").addClass("loading").addClass("disabled");
  150. //Create the markdown file at the /blog/posts folder
  151. const blob = new Blob(["# Hello World\n"], { type: 'text/plain' });
  152. let storeFilename = parseInt(Date.now()/1000) + "_" + filename+'.md';
  153. const file = new File([blob], storeFilename);
  154. handleFile(file, "/blog/posts", function(){
  155. //Update the post index
  156. updatePostIndex();
  157. $("#confirmNewPostBtn").removeClass("loading").removeClass("disabled");
  158. //Open the markdown file in new tab
  159. let hash = encodeURIComponent(JSON.stringify({
  160. "filename": storeFilename,
  161. "filepath": "/blog/posts/" + storeFilename
  162. }))
  163. window.open("/admin/mde/index.html#" + hash);
  164. $("#newPostModal").hide();
  165. });
  166. }
  167. function handleFile(file, dir=currentPath, callback=undefined) {
  168. // Perform actions with the selected file
  169. var formdata = new FormData();
  170. formdata.append("file1", file);
  171. var ajax = new XMLHttpRequest();
  172. ajax.addEventListener("load", function(event){
  173. let responseText = event.target.responseText;
  174. try{
  175. responseText = JSON.parse(responseText);
  176. if (responseText.error != undefined){
  177. alert(responseText.error);
  178. }
  179. }catch(ex){
  180. }
  181. if (callback != undefined){
  182. callback();
  183. }
  184. }, false); // doesnt appear to ever get called even upon success
  185. ajax.addEventListener("error", errorHandler, false);
  186. //ajax.addEventListener("abort", abortHandler, false);
  187. ajax.open("POST", "/upload?dir=" + dir);
  188. ajax.send(formdata);
  189. }
  190. function errorHandler(event) {
  191. aelrt("New Post creation failed");
  192. $("#pasteButton").removeClass("disabled");
  193. }
  194. /*
  195. Post Edit functions
  196. */
  197. function editPost(btn){
  198. let postFilename = $(btn).attr("filename");
  199. let hash = encodeURIComponent(JSON.stringify({
  200. "filename": postFilename,
  201. "filepath": "/blog/posts/" + postFilename
  202. }))
  203. window.open("/admin/mde/index.html#" + hash);
  204. }
  205. function deletePost(btn){
  206. let postFilename = $(btn).attr("filename");
  207. let postTitle = $(btn).attr("ptitle");
  208. if (confirm("Confirm remove post titled: " + postTitle + "?")){
  209. $.ajax({
  210. url: "/api/fs/del?target=/blog/posts/" + postFilename,
  211. method: "POST",
  212. success: function(data){
  213. if (data.error != undefined){
  214. alert("Post delete failed. See console for more info.");
  215. console.log(data.error);
  216. }else{
  217. //Deleted
  218. initPosts();
  219. }
  220. }
  221. });
  222. }
  223. }
  224. /*
  225. Rendering for Posts
  226. */
  227. //Load a markdown file from URL and render it to target element
  228. function loadMarkdownToHTML(markdownURL, targetElement){
  229. fetch(markdownURL).then( r => r.text() ).then( text =>{
  230. var converter = new showdown.Converter();
  231. let targetHTML = converter.makeHtml(text);
  232. console.log(targetHTML);
  233. $(targetElement).html(targetHTML);
  234. });
  235. }
  236. function initPosts(){
  237. $("#posttable").html("<div class='ui basic segment'><p><i class='ui loading spinner icon'></i> Loading Blog Posts</p></div>");
  238. loadValue("blog-posts", function(data){
  239. $("#posttable").html("");
  240. try{
  241. let postList = JSON.parse(decodeURIComponent(atob(data)));
  242. //From latest to oldest
  243. postList.reverse();
  244. console.log("Post listed loaded: ", postList);
  245. if (postList.length == 0){
  246. $("#nopost").show();
  247. }else{
  248. $("#nopost").hide();
  249. postList.forEach(postFilename => {
  250. renderPost(postFilename);
  251. })
  252. }
  253. }catch(ex){
  254. $("#nopost").show();
  255. }
  256. })
  257. }
  258. function forceUpdatePostIndex(){
  259. updatePostIndex(function(){
  260. window.location.reload();
  261. });
  262. }
  263. function updatePostIndex(callback=undefined){
  264. let postList = [];
  265. $.ajax({
  266. url: "/api/fs/list?dir=/blog/posts",
  267. success: function(data){
  268. data.forEach(file => {
  269. let filename = file.Filename;
  270. let ext = filename.split(".").pop();
  271. if (ext == "md" && file.IsDir == false){
  272. //Markdown file. Render it
  273. postList.push(filename);
  274. }
  275. });
  276. setValue("blog-posts", btoa(encodeURIComponent(JSON.stringify(postList))), function(data){
  277. console.log(data);
  278. if (callback != undefined){
  279. callback();
  280. }
  281. });
  282. }
  283. });
  284. }
  285. //Render post
  286. function renderPost(filename){
  287. //Remove the timestamp
  288. let postTitle = filename.split("_");
  289. let timeStamp = postTitle.shift();
  290. postTitle = postTitle.join("_");
  291. //Pop the file extension
  292. postTitle = postTitle.split(".");
  293. postTitle.pop();
  294. postTitle = postTitle.join(".");
  295. var postTime = new Date(parseInt(timeStamp) * 1000).toLocaleDateString("en-US")
  296. let postEditFeature = `<div class="adminOnly" style="position: absolute; top: 3em; right: 0.4em;">
  297. <a class="ui basic mini icon button" onclick="editPost(this);" filename="${filename}" title="Edit Post"><i class="edit icon"></i></a>
  298. <button class="ui basic mini icon button" onclick="deletePost(this);" ptitle="${postTitle}" filename="${filename}" title="Remove Post"><i class="red trash icon"></i></button>
  299. </div>`;
  300. if (!loggedIn){
  301. postEditFeature = "";
  302. }
  303. //Create a wrapper element
  304. $("#posttable").append(`
  305. <div class="ui basic segment postObject" id="${timeStamp}">
  306. <div class="ui divider"></div>
  307. <h4 class="ui header">
  308. <i class="blue paperclip icon"></i>
  309. <div class="content">
  310. ${postTitle}
  311. </div>
  312. </h4>
  313. ${postEditFeature}
  314. <div class="postContent">
  315. </div>
  316. <small><i class="calendar alternate outline icon"></i> ${postTime}</small>
  317. </div>
  318. `);
  319. let targetElement = $("#" + timeStamp).find(".postContent");
  320. loadMarkdownToHTML("/blog/posts/" + filename,targetElement);
  321. }
  322. </script>
  323. </body>
  324. </html>