Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 25 additions & 14 deletions src/components/common/input/DateInput.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,25 @@
"use client";

import { forwardRef, useState } from "react";
import { formatDate } from "@/lib/utils/dateUtils";
import { forwardRef } from "react";
import { ko } from "date-fns/locale";
import DatePicker, { registerLocale } from "react-datepicker";
import "react-datepicker/dist/react-datepicker.css";
import Image from "next/image";
import "react-datepicker/dist/react-datepicker.css";
import calendarIcon from "../../../../public/icon/calendar_icon.svg";

registerLocale("ko", ko);

interface DateInputProps {
interface DateInputTriggerProps {
value: string;
onClick?: () => void;
}

const DateInputTrigger = forwardRef<HTMLDivElement, DateInputProps>(
interface DateInputProps {
value: string;
onChange: (date: string) => void;
}

const DateInputTrigger = forwardRef<HTMLDivElement, DateInputTriggerProps>(
({ value, onClick }, ref) => (
<div
ref={ref}
Expand All @@ -33,26 +37,33 @@ const DateInputTrigger = forwardRef<HTMLDivElement, DateInputProps>(
);
DateInputTrigger.displayName = "DateInputTrigger";

const DateInput = () => {
const [selectedDate, setSelectedDate] = useState<Date | null>(null);

const formattedDate = selectedDate
? formatDate(selectedDate.toISOString(), true)
: "";
const DateInput = ({ value, onChange }: DateInputProps) => {
const handleDateChange = (date: Date | null) => {
if (date) {
const localDate = new Date(date.getTime() + 9 * 60 * 60 * 1000);
const formattedDate = localDate
.toISOString()
.slice(0, 16)
.replace("T", " ");
onChange(formattedDate);
} else {
onChange("");
}
};
return (
<div className="flex flex-col">
<label className="font-medium text-gray-800 text-lg mb-[8px] tablet:text-2lg pc:text-2lg">
마감일
</label>
<DatePicker
selected={selectedDate}
onChange={(date) => setSelectedDate(date)}
selected={value ? new Date(value) : null}
onChange={handleDateChange}
dateFormat="Pp"
locale="ko"
showTimeSelect
timeFormat="HH:mm"
timeIntervals={5}
customInput={<DateInputTrigger value={formattedDate} />}
customInput={<DateInputTrigger value={value} />}
/>
</div>
);
Expand Down
5 changes: 5 additions & 0 deletions src/components/common/input/ImageInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ interface BaseImageInputProps
label?: string;
variant: "task" | "profile";
initialImageUrl?: string | null;
onImageUrlChange?: (url: string) => void;
}

interface TaskImageInputProps extends BaseImageInputProps {
Expand Down Expand Up @@ -64,6 +65,10 @@ const ImageInput = ({
: undefined;
const imageUrl = await postImage(variant, columnId, file);
setUploadImgUrl(imageUrl);

if (props.onImageUrlChange) {
props.onImageUrlChange(imageUrl);
}
} catch (error: unknown) {
if (error instanceof Error) {
setError(error.message || "이미지 업로드에 실패했습니다.");
Expand Down
10 changes: 4 additions & 6 deletions src/components/common/input/TagInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,13 @@
import { useState } from "react";
import TagList from "../tag/TagList";

export default function TagInput({
label,
tags,
setTags,
}: {
interface TagInputProps {
label: string;
tags: string[];
setTags: (tags: string[]) => void;
}) {
}

export default function TagInput({ label, tags, setTags }: TagInputProps) {
const [inputValue, setInputValue] = useState("");

const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
Expand Down
2 changes: 1 addition & 1 deletion src/components/common/modal/Modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export default function Modal({
<div className="flex justify-center items-center fixed top-0 left-0 w-full h-full p-6 bg-black/70 z-50">
<div
className={clsx(
"flex flex-col max-h-[92vh] px-4 rounded border-none bg-white",
"flex flex-col max-h-[80vh] px-4 rounded border-none bg-white",
isPage
? "gap-2 max-w-[327px] py-4 tablet:px-8 tablet:gap-6 tablet:max-w-[1200px] tablet:py-6"
: "gap-8 max-w-[327px] py-6 tablet:max-w-[1200px] tablet:p-8"
Expand Down
2 changes: 0 additions & 2 deletions src/components/modal/create-task/CreateTaskModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import Image from "next/image";
import Modal from "@/components/common/modal/Modal";
import TagInput from "@/components/common/input/TagInput";
import Input from "@/components/common/input/Input";
import DateInput from "@/components/common/input/DateInput";
import ImageInput from "@/components/common/input/ImageInput";
import Textarea from "@/components/common/textarea/Textarea";
import dropdownIcon from "../../../../public/icon/dropdown_icon.svg";
Expand Down Expand Up @@ -74,7 +73,6 @@ export default function CreateDashboardModal() {
labelClassName="font-medium text-lg tablet:text-2lg"
textareaClassName="font-normal placeholder:text-gray-500 rounded-md text-md h-[84px] px-4 py-[13px] tablet:rounded-lg tablet:h-[126px] tablet:py-[15px] tablet:text-lg"
/>
<DateInput />
<TagInput label="태그" tags={tags} setTags={setTags} />
<ImageInput label="이미지" variant="task" columnId={46355} />
</div>
Expand Down
105 changes: 105 additions & 0 deletions src/components/modal/edit-task/AssigneeDropdown.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { useState, useEffect } from "react";
import { fetchDashboardMember } from "@/lib/apis/membersApi";
import Image from "next/image";
import dropdownIcon from "../../../../public/icon/dropdown_icon.svg";
import checkItem from "../../../../public/icon/check_icon.svg";
import UserIcon from "@/components/common/user-icon/UserIcon";

interface AssigneeDropdownProps {
token: string;
dashboardId: string;
memberId: number;
onChange: (value: number) => void;
}

export default function AssigneeDropdown({
token,
dashboardId,
memberId,
onChange,
}: AssigneeDropdownProps) {
const [members, setMembers] = useState<
{ userId: number; nickname: string; profileImageUrl: string | null }[]
>([]);
const [isOpen, setIsOpen] = useState(false);

useEffect(() => {
const getData = async () => {
const res = await fetchDashboardMember({
token,
page: 1,
size: 20,
id: dashboardId,
});
setMembers(res.members);
};

getData();
}, []);

const selected = members.find((member) => member.userId === memberId);

return (
<div className="relative flex flex-col w-full tablet:min-w-[217px]">
<label className="block mb-2.5 text-lg font-medium text-gray-800 tablet:mb-2 tablet:text-2lg">
담당자
</label>
<button
type="button"
onClick={() => setIsOpen((prev) => !prev)}
className="h-12 px-4 py-3 font-normal text-md text-gray-500 border border-gray-400 rounded-md tablet:py-[11px] tablet:text-lg"
>
<div className="flex items-center justify-between h-full w-full">
{selected ? (
<div className="flex items-center gap-[6px]">
<UserIcon
name={selected.nickname}
img={selected.profileImageUrl}
size="sm"
/>
<div className="font-normal text-lg text-gray-800">
{selected.nickname}
</div>
</div>
) : (
<span className="text-gray-500">담당자 선택</span>
)}
<Image src={dropdownIcon} width={8} height={8} alt="arrow" />
</div>
</button>

{isOpen && (
<ul className="absolute top-[80px] z-10 w-full mt-2 bg-white border border-gray-300 rounded-md shadow-md max-h-[200px] overflow-y-auto">
{members.map((member) => (
<li
key={member.userId}
className="px-4 py-2 hover:bg-gray-100 cursor-pointer"
onClick={() => {
onChange(member.userId);
setIsOpen(false);
}}
>
<div className="flex gap-3 items-center">
<div className="invert brightness-75 relative w-4 h-3">
{member.userId === memberId && (
<Image src={checkItem} fill alt="" />
)}
</div>
<div className="flex items-center gap-[6px]">
<UserIcon
name={member.nickname}
img={member.profileImageUrl}
size="sm"
/>
<div className="font-normal text-lg text-gray-800">
{member.nickname}
</div>
</div>
</div>
</li>
))}
</ul>
)}
</div>
);
}
90 changes: 90 additions & 0 deletions src/components/modal/edit-task/ColumnDropdown.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { useState, useEffect } from "react";
import { fetchColumnList } from "@/lib/apis/columnsApi";
import Image from "next/image";
import dropdownIcon from "../../../../public/icon/dropdown_icon.svg";
import checkItem from "../../../../public/icon/check_icon.svg";

interface ColumnDropdownProps {
token: string;
dashboardId: string;
columnId: number;
onChange: (value: number) => void;
}

export default function ColumnDropdown({
token,
dashboardId,
columnId,
onChange,
}: ColumnDropdownProps) {
const [columns, setColumns] = useState<{ id: number; title: string }[]>([]);
const [isOpen, setIsOpen] = useState(false);

useEffect(() => {
const getData = async () => {
const res = await fetchColumnList({
token,
id: dashboardId,
});
setColumns(res.data);
};

getData();
}, []);

const selected = columns.find((col) => col.id === columnId);

return (
<div className="relative flex flex-col w-full tablet:min-w-[217px]">
<label className="block mb-2.5 text-lg font-medium text-gray-800 tablet:mb-2 tablet:text-2lg">
상태
</label>
<button
type="button"
onClick={() => setIsOpen((prev) => !prev)}
className="h-12 px-4 py-3 font-normal text-md text-gray-500 border border-gray-400 rounded-md tablet:py-[11px] tablet:text-lg"
>
<div className="flex items-center justify-between h-full w-full">
{selected ? (
<div className="flex shrink-0 items-center gap-[6px] px-2 py-1 rounded-2xl bg-violet-8">
<div className="w-[6px] h-[6px] rounded-full bg-violet"></div>
<div className="font-normal text-xs leading-[18px] text-violet">
{selected.title}
</div>
</div>
) : (
<span className="text-gray-500">상태 선택</span>
)}
<Image src={dropdownIcon} width={8} height={8} alt="arrow" />
</div>
</button>

{isOpen && (
<ul className="absolute top-[80px] z-10 w-full mt-2 bg-white border border-gray-300 rounded-md shadow-md max-h-[200px] overflow-y-auto">
{columns.map((col) => (
<li
key={col.id}
className="px-4 py-2 hover:bg-gray-100 cursor-pointer"
onClick={() => {
onChange(col.id);
setIsOpen(false);
}}
>
<div className="flex gap-3 items-center">
<div className="invert brightness-75 relative w-4 h-3">
{col.id === columnId && <Image src={checkItem} fill alt="" />}
</div>
<div className="flex shrink-0 items-center gap-[6px] px-2 py-1 rounded-2xl bg-violet-8 w-fit">
<div className="w-[6px] h-[6px] rounded-full bg-violet"></div>
<div className="font-normal text-xs leading-[18px] text-violet">
{col.title}
</div>
</div>
</div>
</li>
))}
</ul>
)}
</div>
);
}
Loading