diff --git a/components/Btn.tsx b/components/Btn.tsx index 524bc2f9..d0303f48 100644 --- a/components/Btn.tsx +++ b/components/Btn.tsx @@ -19,6 +19,7 @@ const CONTENT: Record = { export default function Btn({ className = "", + type = "button", size = "large", mode, ...props @@ -35,7 +36,11 @@ export default function Btn({ }, [mode]); return ( - {children} - + ); } diff --git a/components/CheckListDetail.module.css b/components/CheckListDetail.module.css new file mode 100644 index 00000000..c45bf328 --- /dev/null +++ b/components/CheckListDetail.module.css @@ -0,0 +1,43 @@ +.check-list { + display: flex; + justify-content: center; + align-items: center; + gap: 16px; + height: 64px; + border: 2px solid var(--slate900); + border-radius: 24px; +} + +.check-list.true { + background-color: var(--violet200); +} + +.check-list.false { + background-color: white; +} + +.check-list * { + color: var(--slate900); + line-height: 23px; + font-size: 20px; + font-weight: 700; +} + +.check-list button { + line-height: 0; +} + +.check-list span { + z-index: -100; + position: fixed; + visibility: hidden; + white-space: pre; +} + +.check-list input { + padding: 0; + border: 0; + outline: none; + background-color: transparent; + text-decoration: underline; +} diff --git a/components/CheckListDetail.tsx b/components/CheckListDetail.tsx new file mode 100644 index 00000000..0e902466 --- /dev/null +++ b/components/CheckListDetail.tsx @@ -0,0 +1,48 @@ +import { HTMLAttributes, useEffect, useRef, useState } from "react"; +import Image from "next/image"; +import ic_checked from "@/assets/icons/checkbox_checked.svg"; +import ic_empty from "@/assets/icons//checkbox_empty.svg"; +import styles from "./CheckListDetail.module.css"; + +interface Props extends HTMLAttributes { + defaultIsChecked: boolean; + defaultValue: string; +} + +export default function CheckListDetail({ + className = "", + defaultIsChecked = false, + defaultValue = "", + ...props +}: Props) { + const [value, setValue] = useState(defaultValue ?? ""); + const [isChecked, setIsChecked] = useState(defaultIsChecked); + const spanRef = useRef(null); + const inputRef = useRef(null); + + useEffect(() => { + const width = (spanRef.current?.offsetWidth ?? 0) + 1; + if (inputRef.current) inputRef.current.style.width = width + "px"; + }, [value]); + + return ( +
+ + {value} + setValue(e.currentTarget.value)} + /> +
+ ); +} diff --git a/global.d.ts b/global.d.ts new file mode 100644 index 00000000..f327b15a --- /dev/null +++ b/global.d.ts @@ -0,0 +1,6 @@ +declare module "*.jpg"; +declare module "*.png"; +declare module "*.jpeg"; +declare module "*.gif"; +declare module "*.svg"; +declare module "*.css"; \ No newline at end of file diff --git a/lib/api.ts b/lib/api.ts index ea58d807..98dd1c85 100644 --- a/lib/api.ts +++ b/lib/api.ts @@ -4,14 +4,6 @@ const api = axios.create({ baseURL: process.env.NEXT_PUBLIC_BASE_URL, }); -export type SimpleItem = { - isCompleted: boolean; - name: string; - id: number; -}; - -export type ResponseSimpleItems = SimpleItem[]; - export type ResponseItem = { isCompleted: boolean; imageUrl: string | null; @@ -21,6 +13,10 @@ export type ResponseItem = { id: number; }; +export type SimpleItem = Pick; + +export type ResponseSimpleItems = SimpleItem[]; + export type ResponseDelete = { message: string; }; @@ -60,3 +56,10 @@ export async function deleteItem(itemId: number) { const response = await api.delete("/items/" + itemId); return response.data; } + +export async function postImage(file: File) { + const data = new FormData(); + data.append("image", file); + const response = await api.post<{ url: string }>("/images/upload", data); + return response.data; +} diff --git a/pages/index.tsx b/pages/index.tsx index 9d26b557..ed5e96c8 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -15,7 +15,7 @@ import todo from "@/assets/images/todo.png"; import done from "@/assets/images/done.png"; import empty_todo from "@/assets/images/empty_todo.png"; import empty_done from "@/assets/images/empty_done.png"; -import styles from "@/styles/index.module.css"; +import styles from "@/styles/home.module.css"; export async function getServerSideProps() { const items = await getItems(); @@ -30,16 +30,18 @@ export default function Home({ }) { const [items, setItems] = useState(initialItems); const [disabled, setDisabled] = useState(false); - const searchRef = useRef(null); const handleSubmit = async (e: FormEvent) => { e.preventDefault(); - if (searchRef.current?.value) { + const form = e.currentTarget; + const name = new FormData(form).get("name")?.toString().trim(); + + if (name) { try { setDisabled(true); - const item = await postItem({ name: searchRef.current.value }); + const item = await postItem({ name }); setItems((prev) => [...prev, item]); - searchRef.current.value = ""; + form.reset(); } finally { setDisabled(false); } @@ -61,12 +63,8 @@ export default function Home({
- - + +
@@ -77,6 +75,7 @@ export default function Home({ .map((item) => ( { e.currentTarget.disabled = true; @@ -107,6 +106,7 @@ export default function Home({ .map((item) => ( { e.currentTarget.disabled = true; diff --git a/pages/items/[itemId].tsx b/pages/items/[itemId].tsx new file mode 100644 index 00000000..7f291349 --- /dev/null +++ b/pages/items/[itemId].tsx @@ -0,0 +1,132 @@ +import { ChangeEvent, useEffect, useRef, useState } from "react"; +import { GetServerSidePropsContext } from "next"; +import Image from "next/image"; +import { useRouter } from "next/router"; +import Btn from "@/components/Btn"; +import BtnImage from "@/components/BtnImage"; +import CheckListDetail from "@/components/CheckListDetail"; +import Gnb from "@/components/Gnb"; +import { + deleteItem, + getItem, + patchItem, + postImage, + ResponseItem, +} from "@/lib/api"; +import ic_img from "@/assets/images/img.png"; +import ic_memo from "@/assets/images/memo.png"; +import styles from "@/styles/item.module.css"; + +export async function getServerSideProps(context: GetServerSidePropsContext) { + const itemId = Number(context.params?.itemId); + const item = await getItem(itemId); + + return { props: { item, itemId } }; +} + +export default function Item({ + item, + itemId, +}: { + item: ResponseItem; + itemId: number; +}) { + const [imageUrl, setImageUrl] = useState(item.imageUrl); + const [disabled, setDisabled] = useState(false); + const formRef = useRef(null); + const router = useRouter(); + + const handleInputChange = async (e: ChangeEvent) => { + if (e.target.files) { + try { + const { url } = await postImage(e.target.files[0]); + setImageUrl(url); + } catch (error) { + alert((error as Error).message); + } + } + }; + + const handleTextareaChange = (e: ChangeEvent) => { + e.target.style.height = "auto"; + e.target.style.height = Math.min(e.target.scrollHeight, 229) + "px"; + }; + + const handleEditClick = async () => { + try { + setDisabled(true); + const data = Object.fromEntries( + new FormData(formRef.current ?? undefined).entries() + ); + await patchItem(itemId, { ...data, imageUrl: imageUrl ?? "" }); + router.push("/"); + } catch (error) { + alert((error as Error).message); + setDisabled(false); + } + }; + + const handleDeleteClick = async () => { + try { + setDisabled(true); + await deleteItem(itemId); + router.push("/"); + } catch (error) { + alert((error as Error).message); + setDisabled(false); + } + }; + + useEffect(() => { + const area = document.querySelector( + "form label textarea" + ); + if (area) area.style.height = Math.min(area.scrollHeight, 229) + "px"; + }, []); + + return ( +
+ +
+ +
+
+ {imageUrl ? ( + image + ) : ( + image + )} + + +
+
+ memo +
Memo
+