frontend.js 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310
  1. //static/js/frontend.js
  2. function setLoggedIn(loggedInBoolean) {
  3. let body = document.body;
  4. body.dataset.loggedIn = !!loggedInBoolean;
  5. }
  6. function createExistingVote(voter, vote) {
  7. let voteDiv = document.createElement("div");
  8. voteDiv.innerHTML = "";
  9. if (vote === "yes") {
  10. voteDiv.classList.add("vote-yes");
  11. } else if (vote === "no") {
  12. voteDiv.classList.add("vote-no");
  13. }
  14. voteDiv.innerText = voter;
  15. return voteDiv;
  16. }
  17. function createOneDayCard(dayData, currentSession, weather) {
  18. let date = new Date(dayData.date + "T00:00:00");
  19. let card = document.createElement("div");
  20. let temp = "";
  21. let iconUrl = "50d";
  22. let weatherDescription = "no data!";
  23. if (weather && weather.main && weather.weather && weather.weather[0]) {
  24. temp = weather.main.temp + "°C";
  25. iconUrl = `https://openweathermap.org/img/wn/${weather.weather[0].icon}@2x.png`;
  26. weatherDescription = weather.weather[0].description;
  27. }
  28. card.innerHTML = `
  29. <div class="cardtop">
  30. <div class="date">
  31. <div class="dow">${date.toLocaleString("en-CA", {
  32. weekday: "long",
  33. })}</div>
  34. <div class="dom">${date.toLocaleString("en-CA", {
  35. month: "short",
  36. day: "numeric",
  37. })}</div>
  38. </div>
  39. <div class="weather">
  40. <div class="temp">
  41. ${temp}
  42. </div>
  43. <div class="weath">
  44. <img src="${iconUrl}" alt="${weatherDescription}">
  45. </div>
  46. </div>
  47. </div>
  48. <div class="make-vote">
  49. <div class="satis-tier forloggedin">
  50. Can you attend?
  51. <div class="">
  52. <button class="vote yes" data-vote="yes">
  53. Yes ✔️
  54. </button>
  55. <button class="vote maybe" data-vote="">
  56. ??
  57. </button>
  58. <button class="vote no" data-vote="no">
  59. No ❌
  60. </button>
  61. </div>
  62. </div>
  63. </div>
  64. <div class="existing-votes">
  65. </div>
  66. `;
  67. card.classList.add("card");
  68. card.dataset.date = dayData.date;
  69. if (weather) {
  70. // you need to figure this part out yourself
  71. }
  72. let existingVotesDiv = card.querySelector(".existing-votes");
  73. for (let [voter, vote] of Object.entries(dayData.votes)) {
  74. existingVotesDiv.append(createExistingVote(voter, vote));
  75. }
  76. return card;
  77. }
  78. function updateVotableDays(daysWithVotes, currentSession, weatherForecasts) {
  79. let daysView = document.querySelector(".days-view");
  80. if (!daysView) {
  81. console.error("could not find element to put days into");
  82. return;
  83. }
  84. daysView.innerHTML = "";
  85. for (let date in daysWithVotes) {
  86. let votes = daysWithVotes[date];
  87. let weather = weatherForecasts.list.find((w) => w.dt_txt.startsWith(date));
  88. daysView.append(
  89. createOneDayCard({
  90. date,
  91. votes,
  92. },
  93. currentSession,
  94. weather
  95. )
  96. );
  97. }
  98. }
  99. class FrontendState {
  100. constructor() {
  101. this.currentSession = undefined;
  102. this.daysWithVotes = [];
  103. this.weatherForecasts = {};
  104. this.initWebSocket(); //初始化
  105. }
  106. initWebSocket() {
  107. const host = window.location.hostname; //設定WebSocket host ip
  108. const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; //設定protocol 如果是https 就用wss 反之ws
  109. const wsPort = window.location.port ? `:${window.location.port}` : ''; //設定ws用的port
  110. const wsUrl = `${wsProtocol}//${host}${wsPort}`; //拼起來就是完整的WebSocket囉
  111. this.ws = new WebSocket(wsUrl); //設定完畢並開啟服務
  112. this.ws.onopen = () => { //onopen ws服務開啟的時候會先執行的
  113. console.log('WebSocket connected'); //提示ws已經連線成功
  114. };
  115. this.ws.onmessage = (event) => { //這邊負責監聽比如從伺服端傳來的訊息
  116. console.log('Message from server ', event.data); //log 從server端傳來的訊息
  117. const data = JSON.parse(event.data); //json解析 data
  118. if (data.type === 'voteUpdate') { //如果type為voteUpdate 更新vote結果 ps: 這是自己定義的
  119. this.refreshVotesState(true);
  120. }
  121. if (data.type === 'testMessage') { //如果type為voteUpdate 更新vote結果 ps: 這是自己定義的
  122. this.updateWSMessage(data.message);
  123. }
  124. };
  125. this.ws.onerror = (error) => { //這邊負責紀錄傳送異常的訊息
  126. console.error('WebSocket error', error);
  127. };
  128. }
  129. async refreshAllState(updateView = true) {
  130. await Promise.all([
  131. this.refreshVotesState(false),
  132. this.refreshWeatherState(false),
  133. this.refreshSessionState(false),
  134. ]);
  135. if (updateView) {
  136. this.updateView();
  137. }
  138. }
  139. async refreshVotesState(updateView = true) {
  140. let {
  141. success,
  142. data,
  143. error
  144. } = await getVotesFromBackend();
  145. if (success) {
  146. this.daysWithVotes = data;
  147. } else {
  148. // ha ha I'm being lazy. can you do better?
  149. updateErrorMessage(error);
  150. }
  151. if (updateView) {
  152. this.updateView();
  153. }
  154. }
  155. async refreshWeatherState(updateView = true) {
  156. let {
  157. success,
  158. data,
  159. error
  160. } = await fetchWeatherForCurrentLocation();
  161. if (success) {
  162. this.weatherForecasts = data;
  163. } else {
  164. // ha ha I'm being lazy. can you do better?
  165. updateErrorMessage(error);
  166. }
  167. if (updateView) {
  168. this.updateView();
  169. }
  170. }
  171. async refreshSessionState(updateView = true) {
  172. let {
  173. success,
  174. data,
  175. error
  176. } = await getSessionFromBackend();
  177. if (success) {
  178. this.currentSession = data;
  179. } else {
  180. // 處理錯誤情況
  181. updateErrorMessage(
  182. "Unable to refresh session state. Please try again later."
  183. );
  184. }
  185. if (updateView) {
  186. this.updateView();
  187. }
  188. }
  189. async updateView() {
  190. // 1. update the whole frontend so that it shows relevant logged-in vs logged-out features
  191. setLoggedIn(!!this.currentSession);
  192. // 2. optionally update the header so that it shows the username of the person logged in
  193. if (this.currentSession) updateHeader(this.currentSession.username);
  194. else updateHeader("");
  195. // 3. render the days
  196. updateVotableDays(
  197. this.daysWithVotes,
  198. this.currentSession,
  199. this.weatherForecasts
  200. );
  201. }
  202. async updateWSMessage(message) {
  203. const wsMessageDiv = document.querySelector(".ws");
  204. wsMessageDiv.textContent = message;
  205. }
  206. }
  207. const fes = new FrontendState();
  208. fes.refreshAllState();
  209. async function handleAuthEvent(event) {
  210. event.preventDefault();
  211. // console.log(event.currentTarget, event.target)
  212. let usernameInput = event.currentTarget.querySelector("#headerusername");
  213. let usernameValue = usernameInput.value;
  214. let passwordInput = event.currentTarget.querySelector("#headerpassword");
  215. let passwordValue = passwordInput.value;
  216. let button = event.target.closest("button");
  217. if (button) {
  218. let authActionName = button?.dataset?.authAction;
  219. let authActionFunction = {
  220. signup: ajaxSignup,
  221. login: ajaxLogin,
  222. logout: ajaxLogout,
  223. } [authActionName];
  224. if (authActionFunction) {
  225. let authResult = await authActionFunction(usernameValue, passwordValue);
  226. if (authResult && authResult.success) {
  227. await fes.refreshSessionState();
  228. usernameInput.value = passwordInput.value = "";
  229. } else if (authResult) {
  230. // 處理登入、註冊或登出失敗的情況
  231. updateErrorMessage(authResult.error);
  232. } else {
  233. // 處理未知網絡錯誤
  234. updateErrorMessage("unknown network error");
  235. }
  236. }
  237. }
  238. }
  239. function updateHeader(username) {
  240. const usernameSpan = document.querySelector(".username");
  241. usernameSpan.textContent = username ? username : "nobody";
  242. }
  243. function updateErrorMessage(message) {
  244. const errorMessageDiv = document.querySelector(".error-message");
  245. errorMessageDiv.textContent = message;
  246. }
  247. const authform = document.querySelector("form.authform");
  248. authform.addEventListener("click", handleAuthEvent);
  249. async function handleVoteEvent(event) {
  250. event.preventDefault();
  251. let button = event.target.closest("button.vote");
  252. if (button) {
  253. let voteVal = button.dataset.vote; // 從按鈕的 data-vote 屬性中獲取投票選項
  254. let cardDiv = button.closest("div.card");
  255. let date = cardDiv.dataset.date; // 從卡片的 data-date 屬性中獲取日期
  256. if (fes.currentSession && fes.currentSession.username && voteVal && date) {
  257. // 假設用戶已登入,並且 fes.currentSession 中包含 username
  258. let voteActionResult = await setMyVote(
  259. fes.currentSession.username,
  260. date,
  261. voteVal
  262. );
  263. if (voteActionResult.success) {
  264. await fes.refreshVotesState(); // 重新獲取並顯示最新的投票資訊
  265. } else {
  266. // 顯示錯誤訊息
  267. updateErrorMessage(voteActionResult.error);
  268. }
  269. }
  270. }
  271. }
  272. const daysViewDiv = document.querySelector("section.days-view");
  273. daysViewDiv.addEventListener("click", handleVoteEvent);
  274. document.getElementById('refreshVotes').addEventListener('click', function () {
  275. fes.refreshVotesState(true);// 重新獲取並顯示最新的投票資訊 並更新VIEW
  276. });