steve07s 1 rok temu
rodzic
commit
7ef622b072
5 zmienionych plików z 579 dodań i 65 usunięć
  1. 177 0
      README-tw.md
  2. 43 0
      static/imageSeeds.js
  3. 9 5
      static/index.html
  4. 174 38
      static/spa.js
  5. 176 22
      static/style.css

+ 177 - 0
README-tw.md

@@ -0,0 +1,177 @@
+在這項作業中,你將製作一個僅限前端的單頁應用程式,使用單元 2 中學到的技能。特別地,你將創建一個用於製作「心情板」的應用程式。據說 Pinterest 有提供這類功能。
+
+你將使用前端技術撰寫解決方案:HTML、CSS 和前端 JS。我提供了一些前端的啟動檔案。你可以隨意使用這些檔案,包括修改它們或重新開始。所有的代碼應該放在 `static` 目錄中。
+
+我還提供了一個骨架伺服器,因為某些功能除非通過 `file:` 傳遞前端,否則無法工作。沒有理由更改這個設定,請在未咨詢我之前不要更改,那會很奇怪。
+
+這項作業佔你最終成績的 25%。
+
+## 截止日期
+
+這項作業的截止日期為 3 月 24 日,星期日,晚上 11:59。
+
+## 提交方式
+
+和以前一樣,使用 GitHub Classroom 提交。如果你更改了 GitHub 用戶名,請像之前一樣告訴我。
+
+## 關於 CSS 的評論
+
+我幾乎花了和作業其餘部分一樣多的時間在 CSS 上。不要像我一樣傻(不要浪費太多時間),但要警惕你可能需要花一些時間來處理 CSS。
+
+## 學術誠信
+
+所有之前作業的注意事項同樣適用於此次作業。這項作業是**個人工作**。你可以使用非人類的工具,如教科書、Stack Overflow、AI,除非它們是秘密偽裝的 AI,實際上是由人回答的問題,等等。你不可以請求他人幫助(除了我)、查看其他學生的代碼、幫助其他學生、向其他學生展示你的代碼。
+
+建立一個名為 `SOURCES.md` 的檔案仍然是一個好主意,儘管不是嚴格要求的。
+
+## 概念
+
+這是一款用於製作「心情板」的軟體。心情板是圖像(和通常其他元素,但我們將只涉及圖像)的排列,就像拼貼一樣。所以我們需要讓用戶添加圖像,並排列它們,調整它們的大小。
+
+為了給這個計劃帶來一些秩序,我們實際上將我們的 UI 組織成***卡片***的形式。我們的卡片將有兩種模式:*展示模式*和*編輯模式*。在展示模式下,卡片只會顯示圖像,沒有更多,也沒有更少。在編輯模式下,卡片應該仍然佔用相同的空間,但圖像應該縮小,以便有空間放置按鈕和其他 UI 元素來修改卡片(從而修改圖像的顯示方式)。
+
+另一點注意:在這個應用程式的更完整版本中,你需要能夠處理不同寬高比的圖片(包括裁剪和縮放輸入圖片)。在這個應用程式中,我們將假設所有圖片都有 16:9 的寬高比。你可以選擇忽略這個假設,但那樣的話,責任就落在你身上,需要做額外的工作來讓它看起來不錯。
+
+### 滿足基本要求
+
+在這個級別,網頁應用必須處於可以展示的狀態,足以給客戶留下深刻印象。
+
+- **給評分者的聲明**
+    - 如同之前的作業,請包含一個名為 `STATEMENT_TO_GRADER.md` 的檔案。
+    - 如同之前的作業,這個檔案應該以「我相信我已經完成了____級別的 100% 要求」開頭。
+    - 如同之前的作業,你可以隨意添加你想讓我在評分時知道的任何其他事項。
+- **主視圖**
+    - 頁面的主要部分必須能夠顯示零個或多個卡片。
+        - 「零個或多個」意味著如果我添加了 200 張卡片,它應該仍然可以正常工作!
+    - 每張卡片是一張圖片的容器,在正常視圖中卡片僅顯示圖片,沒有其他 UI 元素。
+    - 卡片按照特定順序顯示,最初它們按照添加的順序顯示。
+    - 你的佈局不需要像我的一樣精緻,但請至少有某種程度上合理的佈局。
+- **卡片**
+    - 卡片總是以展示模式開始,在此模式中它們僅展示圖片。
+    - 點擊一張圖片會切換其卡片之間的展示模式和編輯模式。
+        - 切換一張卡片到編輯模式會使所有其他卡片退出編輯模式。
+    - 在編輯模式下,卡片以較小的尺寸顯示圖片,加上一些編輯按鈕。
+        - 有一個刪除按鈕。
+            - 如果你比我更好的開發者,你可以添加某種確認對話框。
+        - 有兩個按鈕用於將卡片向前或向後移動順序。
+        - 有兩個按鈕用於增加或減少卡片的尺寸。
+    - 卡片可以在列表中向前或向後移動,並且通過過度移動它們不會出現錯亂(太早或太晚)。
+    - 卡片必須具有某種「尺寸」屬性,該屬性可以變化(如上所述)。
+        - 在我的實現中,這個尺寸屬性以 50px 的倍數變化。
+            - 如果稍微調整這個尺寸讓你能夠製作出看起來不錯的應用,那就可以,但不要偏離太遠。
+        - 應該有關於卡片可以獲得的最小和最大尺寸的上下限制(在我的案例中,是 100px 到 450px)。
+    - 所有按鈕功能 **必須** 使用代理來實現,以避免激活數百個事件監聽器。
+        - 在我的解決方案中,PASS 級別有 7 個事件監聽器,但確切的數字並不重要。
+        - 但如果我在心情板上再添加十張圖片,而這增加了 10(或 50!)個監聽器,那就不行了。
+- **頭部**
+    - 必須有一個頭部。頭部必須包含一個名稱,和以下事物的按鈕:添加圖片、種子填充、清除。
+    - 添加圖片按鈕必須打開一個合適的模態對話框,允許添加圖片。
+        - 當這個對話框啟用時,它不應被其他 UI 元素遮擋。
+        - 對話框應該有一個文本輸入框,允許用戶輸入一個 URL。
+        - 對話框應該有兩個按鈕,或者可選三個或四個:
+            - 取消按鈕應該關閉對話框(即使對話框消失)。
+            - 添加按鈕應該:
+                - 添加一個新卡片,其 src 設置為用戶輸入的 URL。
+                    - 新卡片添加在所有現有卡片之後。
+                - 關閉對話框。
+                - 錯誤檢查:
+                    - 如果 URL 是空的,確認按鈕應該什麼都不做。
+                    - 如果 URL 不是以 `http://` 或 `https://` 開頭,你應該默默地幫助用戶,猜測他們想要添加 `http://`。
+                    - 否則,不用擔心錯誤檢查。
+            - 可選:添加更多按鈕應該:
+                - 表現得就像確認按鈕一樣,但它不會關閉對話框。
+                    - (這對於測試很有用,呵呵)
+            - 可選:隨機按鈕應該添加一個隨機圖片。
+                - 是否關閉對話框,我不知道,你決定。
+        - 當一張圖片被添加時,創建一個新卡片並將其添加到卡片列表的末尾。
+    - 清除按鈕應該從主視圖中移除所有卡片(因此移除所有圖片)。
+    - 種子按鈕應該插入一些卡片,隨機地。它們應該有不同的圖片和不同的大小。第二次按下種子按鈕應該帶來更多的圖片。應該有足夠的多樣性,以便容易測試各種功能是否正常工作。
+- **CSS**
+    - 你的 CSS 不應該極度難看。這不是一個專門講授 CSS 的課程,但我知道你們都通過了關於 CSS 的課程,我也知道你們將來需要在工作中使用 CSS,所以我想你們還是需要做一些 CSS 的。
+    - 特別是,在從編輯模式切換回展示模式時,卡片不應該像嗑藥一樣到處跳。
+
+- **代碼質量**
+    - 和往常一樣,我期望你的代碼接近專業水準。如果你忘記了我的想法,可以回去重新閱讀之前作業的 README 文件。
+- **防止跨站腳本攻擊(XSS)**
+    - 我相信在這個應用中,唯一一個壞人可以插入攻擊的機會是通過「添加圖片」表單。請確保這個表單提供的 URL 不會不安全地插入到你的 HTML 中。
+
+總之,你必須能夠:
+
+- 添加圖片
+- 調整圖片大小
+- 移動圖片
+- 刪除圖片
+
+這需要你:
+
+- 選擇 DOM 節點
+- 創建 DOM 節點
+- 修改 DOM 節點
+- 刪除 DOM 節點
+
+### 滿意級別
+
+- **彈出卡片**
+    - 卡片獲得一個新的編輯按鈕(僅在編輯模式下可見),用於「彈出」。
+        - 按鈕在「彈出」狀態和正常狀態之間切換。
+    - 彈出的卡片從其他卡片的正常流程中移出,好像它們是絕對定位的一樣。
+        - 彈出卡片的 z-index 應該位於正常流卡片的前面,但位於添加圖片對話框的後面。
+        - 你可以將卡片彈出到螢幕中央,或許是到它們被彈出之前的位置,或者是任何其他合理且可預測的地方。
+    - 彈出的卡片仍然使用與正常卡片相同的尺寸規則,並且某種方式仍然可以被調整大小。
+    - 彈出的卡片不再有在順序中的位置,並且應該沒有與此相關的奇怪錯誤。
+    - 彈出的卡片應該有一個按鈕(在編輯模式下)返回它們到正常流程。
+        - 出於多種原因,將它們放在直觀正確的位置可能非常困難......
+        - 所以你應該只是將它們放在卡片列表的末尾,好像它們剛剛被添加一樣。
+    - 彈出的卡片也可以被移動。
+        - 我認為它們應該大概有向左、向右、向上和向下的按鈕。
+        - 但如果你能做些其他事情,只要它至少和 4 個按鈕一樣困難就行。
+- **localStorage**
+    - localStorage 是瀏覽器提供給前端 JavaScript 的 API。
+        - 在 MDN 上閱讀關於它的內容。
+            - [https://developer.mozilla.org/zh-TW/docs/Web/API/Window/localStorage](https://developer.mozilla.org/zh-TW/docs/Web/API/Window/localStorage)
+        - 你也可以為了完整性閱讀以下內容,但那裡有很多你可能不需要的額外內容。
+            - [https://developer.mozilla.org/zh-TW/docs/Web/API/Web_Storage_API/Using_the_Web_Storage_API](https://developer.mozilla.org/zh-TW/docs/Web/API/Web_Storage_API/Using_the_Web_Storage_API)
+    - 將你的狀態存儲策略與 localStorage 整合。
+    - 頁面上每一次的改動都應該記錄在 localStorage 中,這樣如果我刷新頁面,一切都會回到之前的狀態。
+        - 這包括圖片在哪裡,它們的順序,大小,是否被「彈出」...
+            - 對於被「彈出」的卡片,它們在頁面上的位置也應該被包括。
+
+- **代碼質量**
+    - 你懂的;寫出你會樂意在代碼審查中讓你的老闆看到的代碼。
+    - 老實說,我不知道應該如何組織這樣一個項目的代碼。
+        - 也許你能想出一些方法。請嘗試吧。
+
+### 卓越級別
+
+- **鍵盤快捷鍵**
+    - 應該可以使用鍵盤完成所有編輯任務。
+        - 如果你願意,可以省略添加圖片的功能,以及來自頭部按鈕的其他功能。
+    - 這意味著必須有一些“當前選中”的卡片概念,儘管它可以是 `null`/`undefined`(如果沒有卡片的話)。
+    - 你必須能夠按下 Tab 使“當前選中”的卡片移動到下一張卡片,理想情況下 Shift-Tab 應該反向移動。
+        - 如果沒有選中任何東西且用戶按下 Tab,那意味著移動到第一張卡片。
+        - 應該可以從末尾繞回到開頭,反之亦然。
+    - 你必須為每一個編輯按鈕都有一個鍵盤快捷鍵,這樣我就可以使用鍵盤來調整大小、移動和彈出/收回。
+        - 也許對於刪除沒有快捷鍵,這似乎太魯莽了,但由你決定。
+    - 當應用程序首次加載時,它應該在控制台輸出一個幫助消息,解釋所有的鍵盤快捷鍵。
+    - 我使用 WASD 進行移動,+ 和 - 進行大小調整,P 用於彈出,但你可以選擇其他鍵。
+
+- **可選**:在頭部添加一個“幫助”按鈕,解釋所有的鍵盤快捷鍵。
+
+- **拖放**
+    - 被「彈出」的卡片應該能夠使用滑鼠移動。
+        - 找出點擊拖動應該在哪裡工作。
+    - 這應該與現有移動事物的方式完全兼容。
+
+- **撤銷/重做**
+    - 通過在頭部添加按鈕,加入撤銷/重做功能。
+    - 你不需要將撤銷/重做堆棧保存在 localStorage 中。
+    - 同樣,你需要為撤銷/重做提供鍵盤快捷方式。
+        - 我使用了 Z 和 X,但你可以選擇其他按鍵。
+
+這項作業要求你使用你在單元 2 中學到的技能來創建一個只有前端的單頁應用程序。這個應用程序允許用戶創建和編輯心情板,這是一種類似於拼貼的圖像排列。你將需要實現添加圖像、排列圖像、調整圖像大小以及刪除圖像的功能。
+
+整個過程中,請注意代碼質量,並確保你的解決方案不僅功能完整,而且代碼組織有序、易於理解。另外,考慮到網絡安全,請確保你的應用程序能夠抵禦常見的安全威脅,如跨站腳本攻擊(XSS)。
+
+隨著你開發這個應用,你可能會遇到許多挑戰,包括但不限於如何有效地管理 DOM 元素、如何存儲和恢復應用狀態以及如何實現用戶界面的互動性。這些挑戰都是珍貴的學習機會,讓你能夠深化對前端開發的理解。
+
+最後,請記住,這是一項個人作業,意味著你應該獨立完成。然而,你可以使用非人類的資源,如教科書、Stack Overflow 和 AI 工具,來幫助你解決問題。如果你有任何疑問或需要進一步的指導,請隨時尋求老師的幫助。祝你在這次作業中取得成功!

+ 43 - 0
static/imageSeeds.js

@@ -0,0 +1,43 @@
+const imageSeeds = [{
+        // buddy 睡覺
+        url: "https://scontent.fcxh3-1.fna.fbcdn.net/v/t39.30808-6/369322165_18202936651248510_5733562580316737973_n.jpg?_nc_cat=109&ccb=1-7&_nc_sid=5f2048&_nc_ohc=LqNOOuOv8psAX9TMyt4&_nc_ht=scontent.fcxh3-1.fna&oh=00_AfAThUaLDrZl7OEkH7mtAXTL2uuTB4KgI1uTuZhuUv6tdw&oe=6604DCC7",
+    },
+    {
+        // 過來睡覺
+        url: "https://scontent.fcxh3-1.fna.fbcdn.net/v/t39.30808-6/308070575_10221607443363504_6922165815242997869_n.jpg?_nc_cat=106&ccb=1-7&_nc_sid=5f2048&_nc_ohc=U7qgvybQSOAAX-fNqeF&_nc_ht=scontent.fcxh3-1.fna&oh=00_AfAhQx2vC3rXiMSHV7wyQCZnBIvdH7_5ExPMe7NhNJXWJA&oe=6604A1AE",
+    },
+    {
+        // buddy 跟牛牛
+        url: "https://scontent.fcxh3-1.fna.fbcdn.net/v/t39.30808-6/290700001_10221273826303286_5494102163229627495_n.jpg?_nc_cat=103&ccb=1-7&_nc_sid=5f2048&_nc_ohc=YRiL226WVSMAX86pQLt&_nc_ht=scontent.fcxh3-1.fna&oh=00_AfADDJ9_YZprcm69Jci1wmdWem7UNDdkIKBcMeeZEQHqUw&oe=6604A3E4",
+    },
+    {
+        // buddy
+        url: "https://scontent.fcxh3-1.fna.fbcdn.net/v/t39.30808-6/414136866_18220352803248510_2174990983309232692_n.jpg?_nc_cat=111&ccb=1-7&_nc_sid=5f2048&_nc_ohc=V93IOm_aXrMAX9PKUUV&_nc_oc=AdiLvwTQ3n21cDGzVs9fQDSUzvQjGnr5UYuM6NBCyzm7pDnrwgdTsqegteXxZf9dPMeRD2a4TCWxthCoB-ffe11v&_nc_ht=scontent.fcxh3-1.fna&oh=00_AfDvMVwwO1Rio4h-lnbSYn-7IaXQry5YxK0IHKkk8hzifw&oe=66036312",
+    },
+    {
+        // 水彩盒
+        url: "https://scontent.fcxh3-1.fna.fbcdn.net/v/t39.30808-6/307438397_10221596344606042_711337386677613977_n.jpg?_nc_cat=103&ccb=1-7&_nc_sid=5f2048&_nc_ohc=a7PUYQaNxlIAX_pwILC&_nc_ht=scontent.fcxh3-1.fna&oh=00_AfCvGM2ArxgoaHob4-h_tG2UW_S-2R7BCbm6w2UcOJlYkg&oe=6603C0B0",
+    },
+    {
+        // 鄉村房子
+        url: "https://scontent.fcxh3-1.fna.fbcdn.net/v/t39.30808-6/338971513_1369564727223213_8976937699924543464_n.jpg?_nc_cat=101&ccb=1-7&_nc_sid=5f2048&_nc_ohc=ItHkwtoEZb0AX98-3Ud&_nc_ht=scontent.fcxh3-1.fna&oh=00_AfDOMOESdiToTp4VsvJf0fK519qu8Frw6HAPNqxW4s2DOA&oe=6603D543",
+    },
+    {
+        // 貓
+        url: "https://scontent.fcxh3-1.fna.fbcdn.net/v/t39.30808-6/341894751_964020538374088_6122458135331442650_n.jpg?_nc_cat=109&ccb=1-7&_nc_sid=5f2048&_nc_ohc=X3rNb4xggaMAX97YF3q&_nc_ht=scontent.fcxh3-1.fna&oh=00_AfAT6XbepvudEFdyOEYWwUW1GQd4nNEiBwROT9lr-Vp7ew&oe=6604768C",
+    },
+    {
+        // 花車
+        url: "https://scontent.fcxh3-1.fna.fbcdn.net/v/t39.30808-6/368054260_10223656037497077_4780098404352868837_n.jpg?_nc_cat=105&ccb=1-7&_nc_sid=5f2048&_nc_ohc=jxVGDg36AHkAX9ATwFc&_nc_ht=scontent.fcxh3-1.fna&oh=00_AfCVSoeOdYpm5BnYlrGJ5TZ3SSiFHpdhTPdxLwh2r-eKiw&oe=6603D081",
+    },
+    {
+        // 建築
+        url: "https://scontent.fcxh3-1.fna.fbcdn.net/v/t39.30808-6/376823199_10223780172280369_2797377430130194385_n.jpg?_nc_cat=109&ccb=1-7&_nc_sid=5f2048&_nc_ohc=29REX443NQ4AX9kkqhp&_nc_ht=scontent.fcxh3-1.fna&oh=00_AfAnfgu6y06mlZWoFh4aLeiP_3WvB2owlsHl90pXhob-jw&oe=6603AB18",
+    },
+    {
+        // house
+        url: "https://scontent.fcxh3-1.fna.fbcdn.net/v/t39.30808-6/268607011_10220413435714059_7158418870755097476_n.jpg?_nc_cat=101&ccb=1-7&_nc_sid=5f2048&_nc_ohc=vIDqdRbZtccAX_k37C-&_nc_ht=scontent.fcxh3-1.fna&oh=00_AfBGRLDO-SDLh1oymH3_naE6rY1vRLdc9cyO6wL2k-7-9w&oe=66050F46",
+    },
+];
+
+window.imageSeeds = imageSeeds;

+ 9 - 5
static/index.html

@@ -7,11 +7,15 @@
   <meta name="viewport" content="width=device-width, initial-scale=1.0">
   <title>Mood Jorts</title>
   <link rel="stylesheet" href="/style.css">
-  <!-- 添加 Packery CSS (如果有的話) -->
+  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tabler/icons-webfont@latest/tabler-icons.min.css">
+  <script src="https://cdn.jsdelivr.net/npm/@tabler/icons@1.74.0/icons-react/dist/index.umd.min.js"></script>
+
   <script src="https://unpkg.com/packery@2/dist/packery.pkgd.js"></script>
   <script src="https://cdnjs.cloudflare.com/ajax/libs/draggabilly/2.3.0/draggabilly.pkgd.min.js"></script>
 
+  <script src="imageSeeds.js"></script>
   <script defer src="spa.js"></script>
+  
 </head>
 
 <body>
@@ -45,17 +49,17 @@
     <div class="modal-content">
       <span class="close">&times;</span>
       <!-- 模態視窗的內容 -->
-      <form class="newPictureForm formNoDefault">
+      <div class="newPictureForm formNoDefault">
         <div>
           <input type="url" id="newPicUrl" name="newPicUrl">
         </div>
         <div>
-          <button class="addPicture buttonAddRandom displayNone">🎲</button>
-          <button class="addPicture buttonApproveAddMore displayNone">Add More</button>
+          <button class="addPicture buttonAddMeme">🎲</button>
+          <button class="addPicture buttonApproveAddMore">Add More</button>
           <button class="addPicture buttonApproveAdd">Add</button>
           <button class="closeForm buttonCancelAdd">Cancel</button>
         </div>
-      </form>
+      </div>
     </div>
   </div>
 

+ 174 - 38
static/spa.js

@@ -10,9 +10,12 @@ function init() {
     document.querySelector('.buttonAddImage').addEventListener('click', showAddImageForm);
     document.querySelector('.buttonCancelAdd').addEventListener('click', hideAddImageForm);
     document.querySelector('.buttonApproveAdd').addEventListener('click', addImage);
+    document.querySelector('.buttonApproveAddMore').addEventListener('click', addMoreImage);
     document.querySelector('.buttonClearCards').addEventListener('click', clearAllCards);
+    document.querySelector('.buttonAddMeme').addEventListener('click', addMeme);
     document.querySelector('.buttonSeedCards').addEventListener('click', seedCards);
 
+
     var cardholder = document.querySelector('.cardholder');
     pckry = new Packery(cardholder, {
         // Packery 的選項
@@ -20,9 +23,72 @@ function init() {
         //   percentPosition: true, // 如果你想要使用百分比定位
         gutter: 1
     });
-    makeAllCardsDraggable();
+    // makeAllCardsDraggable();
+
+    // 綁定卡片點擊事件
+    cardholder.addEventListener('click', function (event) {
+        const card = event.target.closest('.card');
+        if (!card) return; // 如果點擊的不是卡片或卡片內的元素,則不進行任何操作
+
+        // 如果點擊的是圖片或圖片的父元素(可能包括一些包裝圖片的容器)
+        if (event.target === card.querySelector('.mood-image') || event.target.parentNode === card.querySelector('.mood-image')) {
+            // 如果已經有選中的卡片,則取消選中
+            const selectedCard = document.querySelector('.card-selected');
+            if (selectedCard && selectedCard !== card) {
+                selectedCard.classList.remove('card-selected');
+                const buttons = selectedCard.querySelector('.card-buttons');
+                if (buttons) {
+                    buttons.style.display = 'none'; // 隱藏按鈕
+                }
+            }
+            // 切換當前卡片的選中狀態
+            card.classList.toggle('card-selected');
+            const currentButtons = card.querySelector('.card-buttons');
+            if (currentButtons) {
+                currentButtons.style.display = card.classList.contains('card-selected') ? 'flex' : 'none'; // 顯示或隱藏按鈕
+            }
+        }
+
+        // 根據點擊的是哪個按鈕,執行相應的操作
+        if (event.target.matches('.delete, .delete *')) { // 匹配刪除按鈕或其內部的元素
+            pckry.remove(card);
+            card.remove();
+            pckry.layout();
+        } else if (event.target.matches('.zoom-out, .zoom-out *')) { // 匹配縮小按鈕或其內部的元素
+            // ...縮小操作...
+            const currentSize = parseInt(card.classList[1].split('-')[2]);
+            const newSize = Math.max(currentSize - 1, 1); // 確保尺寸不會小於 1
+            // 更新卡片的尺寸類別
+            card.className = card.className.replace(`card-size-${currentSize}`, `card-size-${newSize}`);
+            pckry.layout(); // 通知 Packery 重新佈局
+        } else if (event.target.matches('.zoom-in, .zoom-in *')) { // 匹配放大按鈕或其內部的元素
+            // ...放大操作...
+            const currentSize = parseInt(card.classList[1].split('-')[2]);
+            const newSize = Math.min(currentSize + 1, 4); // 確保尺寸不會大於 4
+            // 更新卡片的尺寸類別
+            card.className = card.className.replace(`card-size-${currentSize}`, `card-size-${newSize}`);
+            pckry.layout(); // 通知 Packery 重新佈局
+        } else if (event.target.matches('.move-left, .move-left *')) { // 匹配向左移動按鈕或其內部的元素
+            // ...向左移動操作...
+            const previousCard = card.previousElementSibling;
+            if (previousCard) {
+                // 將卡片移動到前一個卡片之前
+                cardholder.insertBefore(card, previousCard);
+                pckry.reloadItems(); // 重新加載 Packery 的項目
+                pckry.layout(); // 重新佈局
+            }
+        } else if (event.target.matches('.move-right, .move-right *')) { // 匹配向右移動按鈕或其內部的元素
+            // ...向右移動操作...
+            const nextCard = card.nextElementSibling;
+            if (nextCard) {
+                // 將卡片移動到下一個卡片之後
+                cardholder.insertBefore(nextCard, card);
+                pckry.reloadItems(); // 重新加載 Packery 的項目
+                pckry.layout(); // 重新佈局
+            }
+        }
 
-    // 如果有撤銷/重做按鈕的功能,這裡綁定相應的事件
+    });
 }
 
 function showAddImageForm() {
@@ -53,41 +119,44 @@ function makeAllCardsDraggable() {
 }
 
 function hideAddImageForm() {
-    document.querySelector('.newPictureForm').classList.add('displayNone');
+    var modal = document.getElementById('addImageModal');
+    modal.style.display = "none";
 }
 
 function addImage(event) {
     event.preventDefault();
-    const imageUrl = document.getElementById('newPicUrl').value.trim();
-    if (imageUrl) {
-        // 創建卡片容器
-        const card = document.createElement('div');
-        card.className = 'card';
-
-        // 創建圖片元素
-        const img = document.createElement('img');
-        img.src = imageUrl;
-        img.alt = 'Mood Image';
-        img.className = 'mood-image';
-
-        // 將圖片添加到卡片容器
-        card.appendChild(img);
-
-        // 將卡片容器添加到主體(cardholder)中
-        const cardholder = document.querySelector('.cardholder');
-        cardholder.appendChild(card);
-
-        // 通知 Packery 新增加了一個卡片元素
-        pckry.appended(card);
+    let  imageUrl = document.getElementById('newPicUrl').value.trim();
+    if (!imageUrl) {
+        // 如果 URL 是空的,直接返回並且不進行任何操作
+        console.log('URL is empty. No action taken.');
+        return;
+    }
+    if (!imageUrl.startsWith('http://') && !imageUrl.startsWith('https://')) {
+        // 如果不是,則默默地為用戶添加 http://
+        imageUrl = 'https://' + imageUrl;
+        console.log('URL corrected to: ', imageUrl);
+    }
+    // 添加圖片
+    addImageWithUrl(imageUrl);
 
-        // 使新加入的卡片可拖動
-        var draggie = new Draggabilly(card);
-        // 將 Draggabilly 事件綁定到 Packery
-        pckry.bindDraggabillyEvents(draggie);
+    // 清空輸入欄位並隱藏添加圖片表單
+    document.getElementById('newPicUrl').value = '';
+    hideAddImageForm();
+}
 
-        // 清空輸入欄位
-        document.getElementById('newPicUrl').value = '';
+function addMoreImage(event) {
+    event.preventDefault();
+    let  imageUrl = document.getElementById('newPicUrl').value.trim();
+    if (!imageUrl) {
+        console.log('URL is empty. No action taken.');
+        return;
+    }
+    if (!imageUrl.startsWith('http://') && !imageUrl.startsWith('https://')) {
+        imageUrl = 'http://' + imageUrl;
+        console.log('URL corrected to: ', imageUrl);
     }
+    addImageWithUrl(imageUrl);
+    document.getElementById('newPicUrl').value = '';
 }
 
 
@@ -104,13 +173,19 @@ function clearAllCards() {
     pckry.layout();
 }
 
-function seedCards() {
-    getRandomMemeGif(function(gifUrl) {
+function addMeme() {
+    getRandomMemeGif(function (gifUrl) {
         // 這裡我們直接調用 addImage 函數,將 Giphy 圖片 URL 作為參數
         addImageWithUrl(gifUrl);
     });
 }
 
+function seedCards() {
+    imageSeeds.forEach(seed => {
+        addImageWithUrl(seed.url);
+    });
+}
+
 // 撤銷/重做功能的實現可以根據你的需求設計
 
 // 將 YOUR_API_KEY 替換成你的 Giphy API 密鑰
@@ -139,20 +214,81 @@ function addImageWithUrl(imageUrl) {
     const sizeClass = 'card-size-' + (Math.floor(Math.random() * 4) + 1);
     card.classList.add(sizeClass);
 
+    // 創建按鈕容器
+    const buttonsContainer = document.createElement('div');
+    buttonsContainer.className = 'card-buttons';
+
+    // 創建按鈕組
+    const buttonGroupVertical = document.createElement('div');
+    buttonGroupVertical.className = 'button-group vertical';
+
+    const buttonGroupHorizontal = document.createElement('div');
+    buttonGroupHorizontal.className = 'button-group horizontal';
+
+    // 創建刪除按鈕
+    const deleteBtn = document.createElement('button');
+    deleteBtn.className = 'button delete';
+    deleteBtn.innerHTML = '<i class="ti ti-trash"></i>';
+
+    // 創建放大按鈕
+    const zoomInBtn = document.createElement('button');
+    zoomInBtn.className = 'button zoom-in';
+    zoomInBtn.innerHTML = '<i class="ti ti-arrows-maximize"></i>';
+
+    // 創建縮小按鈕
+    const zoomOutBtn = document.createElement('button');
+    zoomOutBtn.className = 'button zoom-out';
+    zoomOutBtn.innerHTML = '<i class="ti ti-arrows-minimize"></i>';
+
+    // 向按鈕組中添加按鈕
+    buttonGroupVertical.appendChild(deleteBtn);
+    buttonGroupVertical.appendChild(zoomInBtn);
+    buttonGroupVertical.appendChild(zoomOutBtn);
+
+    // 創建向左移動按鈕
+    const moveLeftBtn = document.createElement('button');
+    moveLeftBtn.className = 'button move-left';
+    moveLeftBtn.innerHTML = '<i class="ti ti-arrow-left"></i>';
+
+    // 創建向右移動按鈕
+    const moveRightBtn = document.createElement('button');
+    moveRightBtn.className = 'button move-right';
+    moveRightBtn.innerHTML = '<i class="ti ti-arrow-right"></i>';
+
+    // 向按鈕組中添加按鈕
+    buttonGroupHorizontal.appendChild(moveLeftBtn);
+    buttonGroupHorizontal.appendChild(moveRightBtn);
+
+    // 向總按鈕容器添加按鈕組
+    buttonsContainer.appendChild(buttonGroupVertical);
+    buttonsContainer.appendChild(buttonGroupHorizontal);
+
+    // 設置按鈕組的位置
+    buttonsContainer.style.justifyContent = 'space-between';
+    buttonsContainer.style.position = 'absolute';
+    buttonsContainer.style.top = '0';
+    buttonsContainer.style.left = '0';
+    buttonsContainer.style.zIndex = '1';
+
+
+    // 創建圖片元素
     const img = document.createElement('img');
     img.src = imageUrl;
     img.alt = 'Mood Image';
     img.className = 'mood-image';
-    img.onload = function() {
+    img.style.position = 'relative';
+    img.style.zIndex = '2';
+
+    // 當圖片加載完成後,將圖片和按鈕容器添加到卡片
+    img.onload = function () {
         card.appendChild(img);
+        card.appendChild(buttonsContainer);
+        // 將卡片添加到卡片容器中
         const cardholder = document.querySelector('.cardholder');
         cardholder.appendChild(card);
 
+        // 通知 Packery 新增了卡片並重新布局
         pckry.appended(card);
         pckry.layout();
-
-        var draggie = new Draggabilly(card);
-        pckry.bindDraggabillyEvents(draggie);
     };
-}
-
+}

+ 176 - 22
static/style.css

@@ -16,8 +16,10 @@ body {
   margin: 0;
   min-height: 100vh;
   background-color: var(--light);
-  color: var(--darkest); /* 為文字顏色添加一個變量 */
-  font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; /* 選擇一個基本字體 */
+  color: var(--darkest);
+  /* 為文字顏色添加一個變量 */
+  font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
+  /* 選擇一個基本字體 */
 }
 
 header {
@@ -26,7 +28,8 @@ header {
   padding: 0.25em;
   display: flex;
   justify-content: space-between;
-  align-items: center; /* 垂直居中對齊 */
+  align-items: center;
+  /* 垂直居中對齊 */
 }
 
 header h1 {
@@ -44,42 +47,73 @@ header h1 {
   background: var(--lightest);
   box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
   border-radius: 5px;
+  /* 卡片圓角 */
   overflow: hidden;
-  max-width: 300px; /* 最大寬度設定 */
-  /* 你可以根據實際的網站佈局需要調整這個最大寬度 */
+  max-width: 350px;
+  /* 最大寬度設定 */
+  margin: 8px;
+  /* 卡片之間的間隔 */
+}
+
+.card-size-1 {
+  width: calc(150px - 20px);
+}
+
+/* 調整寬度以補償邊距 */
+.card-size-2 {
+  width: calc(200px - 20px);
+}
+
+.card-size-3 {
+  width: calc(250px - 20px);
+}
+
+.card-size-4 {
+  width: calc(300px - 20px);
 }
-.card-size-1 { width: 25%; }
-.card-size-2 { width: 50%; }
-.card-size-3 { width: 75%; }
-.card-size-4 { width: 100%; }
 
 .mood-image {
-  width: 100%; /* 使圖片寬度適應容器 */
-  height: auto; /* 高度自動 */
-  object-fit: cover; /* 保持圖片比例,並填滿整個格子 */
+  width: 100%;
+  /* 使圖片寬度適應容器 */
+  height: auto;
+  /* 高度自動 */
+  object-fit: cover;
+  /* 保持圖片比例,並填滿整個格子 */
+  border-radius: 5px;
+  /* 圖片圓角 */
 }
 
 /* 模態視窗背景樣式 */
 .modal {
-  display: none; /* Hidden by default */
-  position: fixed; /* Stay in place */
-  z-index: 1; /* Sit on top */
+  display: none;
+  /* Hidden by default */
+  position: fixed;
+  /* Stay in place */
+  z-index: 3;
+  /* Sit on top */
   left: 0;
   top: 0;
-  width: 100%; /* Full width */
-  height: 100%; /* Full height */
-  overflow: auto; /* Enable scroll if needed */
-  background-color: rgb(0,0,0); /* Fallback color */
-  background-color: rgba(0,0,0,0.4); /* Black w/ opacity */
+  width: 100%;
+  /* Full width */
+  height: 100%;
+  /* Full height */
+  overflow: auto;
+  /* Enable scroll if needed */
+  background-color: rgb(0, 0, 0);
+  /* Fallback color */
+  background-color: rgba(0, 0, 0, 0.4);
+  /* Black w/ opacity */
 }
 
 /* 模態視窗內容樣式 */
 .modal-content {
   background-color: #fefefe;
-  margin: 15% auto; /* 15% from the top and centered */
+  margin: 15% auto;
+  /* 15% from the top and centered */
   padding: 20px;
   border: 1px solid #888;
-  width: 80%; /* Could be more or less, depending on screen size */
+  width: 80%;
+  /* Could be more or less, depending on screen size */
 }
 
 /* The Close Button */
@@ -97,3 +131,123 @@ header h1 {
   cursor: pointer;
 }
 
+.mood-image-small {
+  transform: scale(0.9);
+  /* 縮小圖片 */
+  transition: transform 0.3s ease;
+  /* 平滑過渡效果 */
+}
+
+.card-buttons {
+  position: absolute;
+  top: 0;
+  right: 0;
+  display: none;
+  flex-direction: column;
+  align-items: flex-end;
+  height: 100%;
+  padding: 10px;
+}
+
+.button-group {
+  display: flex;
+  margin-bottom: -10px;
+}
+
+.button-group.vertical {
+  flex-direction: column;
+  margin-right: -27px;
+  margin-top: -8px;
+}
+
+.button-group.horizontal {
+  flex-direction: row;
+  position: absolute;
+  bottom: 10px;
+  margin-left: -45px;
+  transform: translateX(-50%);
+}
+
+.button {
+  margin: 4px;
+  /* 為按鈕添加一些空間 */
+}
+
+.card .button {
+  padding: 0.3em 0.6em;
+  /* 按鈕的內邊距,使用 em 單位 */
+  margin: 0.1em;
+  /* 按鈕之間的外邊距,使用 em 單位 */
+  border: none;
+  border-radius: 5px;
+  /* 圓角 */
+  background-color: var(--med);
+  /* 使用 CSS 變量設定背景色 */
+  color: white;
+  cursor: pointer;
+  line-height: 1;
+  /* 行高與按鈕大小相匹配 */
+  white-space: nowrap;
+  /* 防止圖示換行 */
+}
+
+.button:hover {
+  background-color: var(--dark);
+  /* 滑鼠懸停時的背景色 */
+}
+
+/* Tabler 圖示的基本樣式 */
+.tabler-icon {
+  display: inline-block;
+  vertical-align: middle;
+}
+
+.card-selected .card-buttons {
+  display: flex;
+  /* 顯示按鈕 */
+}
+
+/* 根據卡片大小設定按鈕尺寸的樣式 */
+.card-size-1 .card-buttons {
+  font-size: 4vw;
+}
+
+.card-size-2 .card-buttons {
+  font-size: 3.5vw;
+}
+
+.card-size-3 .card-buttons {
+  font-size: 3vw;
+}
+
+.card-size-4 .card-buttons {
+  font-size: 2.5vw;
+}
+
+/* 卡片縮小時的轉換效果 */
+.card-selected .mood-image {
+  transform: scale(0.7);
+  /* 縮小圖片 */
+  transform-origin: top left;
+  /* 從左上角開始縮放 */
+  transition: transform 0.3s ease;
+  /* 平滑過渡效果 */
+}
+
+/* 卡片被選中時的樣式 */
+.card-selected .mood-image {
+  transform-origin: top left;
+  transform: scale(0.7);
+  transition: transform 0.3s ease;
+}
+
+/* 卡片被選中時顯示按鈕 */
+.card-selected .card-buttons {
+  display: flex;
+  justify-content: space-around;
+  position: absolute;
+  width: calc(100% - 20px);
+  /* 考慮到 padding */
+  bottom: 5px;
+  left: 10px;
+}