Skip to content

Commit 0ba7fee

Browse files
committed
feat: add file handling and tag management in NewPost component
1 parent 981f573 commit 0ba7fee

4 files changed

Lines changed: 230 additions & 54 deletions

File tree

app/new/page.tsx

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,6 @@ import PostEditorDesktop from "@/components/PostEditor/desktop/PostEditor";
1111
// Mobile Components
1212
import PostEditorMobile from "@/components/PostEditor/mobile/PostEditor";
1313

14-
// Styles
15-
import styles from "./page.module.scss";
16-
1714
// Types
1815
import type { TPostAPI } from "../../components/PostEditor/types/postAPI";
1916

@@ -218,7 +215,8 @@ export default function NewPostPage() {
218215
discardFunction={discard}
219216
postFunction={NewPostAPI}
220217
postButtonDisable={postButtonDisable}
221-
></PostEditorMobile>
218+
handleFileChange={handleFileChange}
219+
/>
222220
) : (
223221
<PostEditorDesktop
224222
data={data}
@@ -229,6 +227,6 @@ export default function NewPostPage() {
229227
handleFormEventFunction={handleFormChange}
230228
postButtonDisable={postButtonDisable}
231229
handleFileChange={handleFileChange}
232-
></PostEditorDesktop>
230+
/>
233231
);
234232
}

app/post/[postID]/edit/page.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,7 @@ export default function EditPostPage({
237237
postFunction={sendEdit}
238238
postButtonDisable={editButtonDisable}
239239
handleFormEventFunction={handleFormChange}
240+
handleFileChange={handleFileChange}
240241
isEdit
241242
></PostEditorMobile>
242243
) : (

components/PostEditor/desktop/MetaDataForm.tsx

Lines changed: 0 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,6 @@ import Buttons from "./Buttons";
1010
// Types
1111
import type { TPostAPI } from "@/components/PostEditor/types/postAPI";
1212

13-
// Modules
14-
import sigAPI from "@/modules/sigAPI";
15-
import Image from "next/image";
16-
17-
// Utils
18-
import { useUserAccount } from "@/utils/useUserAccount";
19-
20-
// Types
21-
import type { Sig } from "@/interfaces/Sig";
22-
23-
// Config
24-
import { announcementSigId } from "../config/announcement";
2513

2614
interface Props {
2715
discardFunction: Function;
@@ -43,36 +31,10 @@ export default function MetaDataForm({
4331
isEdit,
4432
}: Props) {
4533
const { status } = useSession();
46-
const { userData } = useUserAccount();
47-
48-
const [sigs, setSigs] = useState<any[]>([]);
49-
const [announcementSigData, setAnnouncementSigData] = useState<Sig>();
5034
const [isDragging, setIsDragging] = useState(false);
5135
const [tagInput, setTagInput] = useState("");
5236
const [tags, setTags] = useState<string[]>([]);
5337

54-
useEffect(() => {
55-
(async () => {
56-
try {
57-
const response = await sigAPI.getSigList();
58-
setSigs(response);
59-
} catch (error: any) {
60-
console.error(error.message);
61-
}
62-
})();
63-
}, []);
64-
65-
useEffect(() => {
66-
(async () => {
67-
try {
68-
const response = await sigAPI.getSigData(announcementSigId);
69-
setAnnouncementSigData(response);
70-
} catch (error: any) {
71-
console.error(error.message);
72-
}
73-
})();
74-
}, []);
75-
7638
// 從 data 初始化 tags
7739
useEffect(() => {
7840
if (data?.hashtag) {

components/PostEditor/mobile/PostEditor.tsx

Lines changed: 226 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import type { Dispatch, SetStateAction } from "react";
22
import dynamic from "next/dynamic";
3+
import { useEffect, useState } from "react";
4+
import { useSession } from "next-auth/react";
5+
import Swal from "sweetalert2";
36

47
// Components
58
import MetaDataForm from "./MetaDataForm";
@@ -17,11 +20,13 @@ interface Props {
1720
postFunction: Function;
1821
postButtonDisable: boolean;
1922
isEdit?: boolean;
23+
handleFileChange: Function;
2024
}
2125

2226
const Editor = dynamic(() => import("./Editor"), {
2327
ssr: false,
2428
});
29+
2530
export default function NewPostMobile({
2631
setPostData,
2732
data,
@@ -31,20 +36,230 @@ export default function NewPostMobile({
3136
postFunction,
3237
postButtonDisable,
3338
isEdit,
39+
handleFileChange,
3440
}: Props) {
41+
const { status } = useSession();
42+
const [tagInput, setTagInput] = useState("");
43+
const [tags, setTags] = useState<string[]>([]);
44+
45+
// 從 data 初始化 tags
46+
useEffect(() => {
47+
if (data?.hashtag) {
48+
setTags(Array.isArray(data.hashtag) ? data.hashtag : []);
49+
}
50+
}, [data?.hashtag]);
51+
52+
const showLoginAlert = () => {
53+
Swal.fire({
54+
icon: "warning",
55+
title: "需要登入",
56+
text: "請先登入才能上傳封面圖片",
57+
confirmButtonText: "確定",
58+
confirmButtonColor: "#5FCDF5",
59+
});
60+
};
61+
62+
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
63+
// 檢查登入狀態
64+
if (status !== "authenticated") {
65+
showLoginAlert();
66+
e.target.value = ""; // 清空 input
67+
return;
68+
}
69+
70+
console.log("File input changed:", e.target.files);
71+
if (handleFileChange) {
72+
handleFileChange(e);
73+
}
74+
};
75+
76+
const handleFileInputClick = (e: React.MouseEvent<HTMLLabelElement>) => {
77+
// 檢查登入狀態
78+
if (status !== "authenticated") {
79+
e.preventDefault();
80+
showLoginAlert();
81+
return;
82+
}
83+
};
84+
85+
const handleRemoveCover = (e: React.MouseEvent) => {
86+
e.preventDefault();
87+
e.stopPropagation();
88+
89+
if (handleFormEventFunction) {
90+
handleFormEventFunction({ target: { name: "cover", value: "" } });
91+
}
92+
93+
const fileInput = document.getElementById("file") as HTMLInputElement;
94+
if (fileInput) {
95+
fileInput.value = "";
96+
}
97+
};
98+
99+
const handleAddTag = () => {
100+
const trimmedTag = tagInput.trim();
101+
if (trimmedTag && !tags.includes(trimmedTag)) {
102+
const newTags = [...tags, trimmedTag];
103+
setTags(newTags);
104+
setTagInput("");
105+
106+
// 更新父組件的 data
107+
if (handleFormEventFunction) {
108+
handleFormEventFunction({
109+
target: { name: "hashtag", value: newTags },
110+
});
111+
}
112+
}
113+
};
114+
115+
const handleRemoveTag = (tagToRemove: string) => {
116+
const newTags = tags.filter((tag) => tag !== tagToRemove);
117+
setTags(newTags);
118+
119+
// 更新父組件的 data
120+
if (handleFormEventFunction) {
121+
handleFormEventFunction({ target: { name: "hashtag", value: newTags } });
122+
}
123+
};
124+
125+
const handleTagInputKeyPress = (e: React.KeyboardEvent<HTMLInputElement>) => {
126+
if (e.key === "Enter") {
127+
e.preventDefault();
128+
handleAddTag();
129+
}
130+
};
131+
35132
return (
36-
<div className="pt-[4.5rem] px-2 pb-[4.5rem] h-[100dvh]">
37-
<div className="bg-white h-full rounded-lg p-4 pr-2 overflow-hidden">
38-
<div className="flex flex-col items-center overflow-y-auto small-scrollbar h-full pr-2">
39-
<MetaDataForm
40-
data={data}
41-
handleFormEventFunction={handleFormEventFunction}
42-
/>
43-
<h2 className="text-left w-full h-auto mt-4 mb-1 text-lg">Content</h2>
44-
<hr className="border-gray-300 h-1 w-full" />
45-
<div className="h-auto w-full mb-2 mt-2">
133+
<div className="pt-[4.5rem] px-2 pb-[4.5rem] h-[100dvh] overflow-x-hidden">
134+
<div className="bg-white h-full rounded-lg p-4 overflow-hidden">
135+
<div className="flex flex-col items-center overflow-y-auto small-scrollbar h-full pr-2 overflow-x-hidden">
136+
<div className="w-full max-w-full">
137+
<MetaDataForm
138+
data={data}
139+
handleFormEventFunction={handleFormEventFunction}
140+
/>
141+
</div>
142+
143+
{/* Content Section */}
144+
<h2 className="text-left w-full h-auto mt-6 mb-2 text-lg font-semibold text-gray-700">
145+
Content
146+
</h2>
147+
<hr className="border-gray-200 h-px w-full mb-3" />
148+
<div className="h-auto w-full max-w-full mb-4">
46149
<Editor setPostData={setPostData} data={data} token={token} />
47150
</div>
151+
152+
{/* Hashtag Section */}
153+
<h2 className="text-left w-full h-auto mt-4 mb-2 text-lg font-semibold text-gray-700">
154+
Hashtag
155+
</h2>
156+
<hr className="border-gray-200 h-px w-full mb-3" />
157+
<div className="w-full max-w-full">
158+
{/* Tag Input */}
159+
<div className="flex gap-2 mb-3 h-auto w-full">
160+
<input
161+
type="text"
162+
value={tagInput}
163+
onChange={(e) => setTagInput(e.target.value)}
164+
onKeyDown={handleTagInputKeyPress}
165+
placeholder="新增標籤..."
166+
className="flex-1 px-4 py-2.5 border border-gray-300 rounded-lg text-sm w-full"
167+
/>
168+
<button
169+
onClick={handleAddTag}
170+
className="px-5 py-2.5 bg-[#5FCDF5] text-white rounded-lg transition-colors font-medium text-sm whitespace-nowrap flex-none"
171+
>
172+
ADD
173+
</button>
174+
</div>
175+
176+
{/* Tags Display */}
177+
{tags.length > 0 && (
178+
<div className="flex flex-wrap gap-2 p-3 bg-gray-50 rounded-lg max-w-full">
179+
{tags.map((tag) => (
180+
<span
181+
key={tag}
182+
className="inline-flex items-center gap-1.5 bg-blue-100 text-blue-600 px-3 py-1.5 rounded-full text-sm font-medium break-all"
183+
>
184+
#{tag}
185+
<button
186+
onClick={() => handleRemoveTag(tag)}
187+
className="active:bg-blue-200 rounded-full w-5 h-5 flex items-center justify-center transition-colors active:scale-90 flex-shrink-0"
188+
>
189+
×
190+
</button>
191+
</span>
192+
))}
193+
</div>
194+
)}
195+
</div>
196+
197+
{/* Cover Section */}
198+
<h2 className="text-left w-full h-auto mt-4 mb-2 text-lg font-semibold text-gray-700">
199+
Cover
200+
</h2>
201+
<hr className="border-gray-200 h-px w-full mb-3" />
202+
<div className="w-full max-w-full">
203+
{data?.cover ? (
204+
<div className="relative max-w-full">
205+
<label
206+
htmlFor="file"
207+
onClick={handleFileInputClick}
208+
className="block cursor-pointer active:opacity-90 transition-opacity"
209+
>
210+
<img
211+
src={data.cover}
212+
alt="Cover"
213+
className="w-full max-w-full max-h-64 object-cover rounded-lg"
214+
/>
215+
</label>
216+
217+
<button
218+
onClick={handleRemoveCover}
219+
className="absolute top-2 right-2 w-9 h-9 bg-red-500 active:bg-red-600 text-white rounded-full flex items-center justify-center shadow-lg transition-all duration-200 active:scale-90"
220+
title="移除封面"
221+
>
222+
<svg
223+
xmlns="http://www.w3.org/2000/svg"
224+
className="h-5 w-5"
225+
viewBox="0 0 20 20"
226+
fill="currentColor"
227+
>
228+
<path
229+
fillRule="evenodd"
230+
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
231+
clipRule="evenodd"
232+
/>
233+
</svg>
234+
</button>
235+
</div>
236+
) : (
237+
<label
238+
htmlFor="file"
239+
onClick={handleFileInputClick}
240+
className="min-h-[12rem] border-2 border-dashed border-[#5FCDF5] bg-blue-50 rounded-lg cursor-pointer transition-all active:bg-blue-100 active:scale-[0.98] flex flex-col justify-center items-center gap-3 p-6"
241+
>
242+
<div className="text-6xl opacity-60">📷</div>
243+
<div className="text-[#5FCDF5] font-medium text-base text-center">
244+
點擊上傳圖片
245+
</div>
246+
<div className="text-gray-500 text-sm text-center">
247+
支援 JPG、PNG、GIF 格式
248+
</div>
249+
</label>
250+
)}
251+
252+
<input
253+
id="file"
254+
type="file"
255+
accept="image/*"
256+
onChange={handleInputChange}
257+
className="hidden"
258+
/>
259+
</div>
260+
261+
{/* Bottom Spacing and Buttons */}
262+
<div className="w-full border-t border-gray-200 mt-6 mb-4"></div>
48263
<Button
49264
discardFunction={discardFunction}
50265
postFunction={postFunction}
@@ -55,4 +270,4 @@ export default function NewPostMobile({
55270
</div>
56271
</div>
57272
);
58-
}
273+
}

0 commit comments

Comments
 (0)