Skip to content

Commit

Permalink
✨ feat: TagInput 기능 구현 (#70)
Browse files Browse the repository at this point in the history
* ✨ feat : TagsInput 컴포넌트 작성
- zag 패키지 사용
- 인풋 디자인

* ✨ feat: 제목 입력 인풋 활성,에러 처리
- TitleInput 컴포넌트로 분리
- borderStyle 상수로 분리

* feat: Chip color 에 primary 추가
- size 옵션 medium 을 디폴트로

* 💄 style : border 스타일에 none 추가

* ✨ feat : 밈 입력 폼 상태 생성

* 📦 fix: 콘솔로스 삭제
- borderstyle 수정
  • Loading branch information
SeieunYoo authored and ojj1123 committed Jan 19, 2024
1 parent ab19504 commit 6711142
Show file tree
Hide file tree
Showing 36 changed files with 1,189 additions and 237 deletions.
882 changes: 713 additions & 169 deletions .pnp.cjs

Large diffs are not rendered by default.

101 changes: 54 additions & 47 deletions .pnp.loader.mjs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@
"@tanstack/react-query": "^4.19.1",
"@use-gesture/react": "^10.2.24",
"@vercel/og": "^0.5.3",
"@zag-js/react": "^0.22.0",
"@zag-js/tags-input": "^0.22.0",
"axios": "^1.2.0",
"lottie-web": "^5.10.2",
"next": "13.0.5",
Expand Down
3 changes: 3 additions & 0 deletions public/icon/delete3.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 4 additions & 2 deletions src/common/components/Chip/Chip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ type Props<T extends ElementType> = {
as?: T;
className?: string;
label: string;
size: keyof typeof sizes;
size?: keyof typeof sizes;
color: keyof typeof colors;
} & ComponentPropsWithoutRef<T>;

Expand All @@ -15,11 +15,13 @@ const sizes = {
const colors = {
white: "border-[1px] border-gray-200 bg-white text-gray-700",
black: "bg-black text-white",
primary:
"bg-gray-100 text-primary-700 focus:bg-white focus:text-primary-500 hover:bg-white hover:text-primary-500 active:bg-gray-300 active:text-primary-800",
};

export const Chip = <T extends ElementType>({
as,
size,
size = "medium",
color,
className = "",
label,
Expand Down
1 change: 1 addition & 0 deletions src/common/components/Icon/assets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export { default as chevronDown2 } from "/public/icon/chevronDown2.svg";
export { default as collection } from "/public/icon/collection.svg";
export { default as delete } from "/public/icon/delete.svg";
export { default as delete2 } from "/public/icon/delete2.svg";
export { default as delete3 } from "/public/icon/delete3.svg";
export { default as download } from "/public/icon/download.svg";
export { default as google } from "/public/icon/google.svg";
export { default as instagram } from "/public/icon/instagram-logo.svg";
Expand Down
89 changes: 89 additions & 0 deletions src/features/upload/components/TagsInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { normalizeProps, useMachine } from "@zag-js/react";
import * as tagsInput from "@zag-js/tags-input";
import type { InputHTMLAttributes } from "react";
import { useState } from "react";

import { Chip } from "@/common/components/Chip";
import { Icon } from "@/common/components/Icon";

import { borderStyle } from "../styles";

interface Props extends InputHTMLAttributes<HTMLInputElement> {
word: string;
description: string;
}

export const TagsInput = (props: Props) => {
const [focus, setFocus] = useState(false);
const [state, send] = useMachine(
tagsInput.machine({
id: props.placeholder as string,
max: 3,
allowEditTag: false,
validate(details) {
return details.inputValue.length < 11;
},
}),
);

const api = tagsInput.connect(state, send, normalizeProps);
const isValidInput = api.inputValue.length < 11;
//TODO: 연관 검색 태그 api 연결

return (
<div className="px-16 text-16-semibold-140 leading-[160%]">
<div className="flex">
<div className="w-200" {...api.rootProps}>
<div
className={`flex overflow-x-auto border-b px-4 pb-4 ${
!isValidInput ? borderStyle.error : focus ? borderStyle.active : borderStyle.normal
}`}
>
<div className="flex gap-4">
{api.value.map((value, index) => (
<div
className="cursor-pointer rounded-8 bg-gray-100 p-8 text-14-semibold-140 text-gray-700"
key={index}
>
<div
{...api.getItemProps({ index, value })}
className="flex gap-4 whitespace-nowrap"
>
<span>{value.trim()}</span>
<button {...api.getItemDeleteTriggerProps({ index, value })}>
<Icon id="delete3" name="delete3" />
</button>
</div>
<input {...api.getItemInputProps({ index, value })} />
</div>
))}
</div>
<input
{...api.inputProps}
className="ml-4 placeholder:text-gray-500 focus:outline-none"
maxLength={11}
placeholder={api.count ? "" : props.placeholder}
onBlur={() => setFocus(false)}
onFocus={() => setFocus(true)}
/**
* TODO: 자동으로 포커스가 넘어가게 하는 거 구현
* api.focus 함수 이용
*/
/>
</div>
</div>
<span className="text-gray-500 ">{props.word}</span>
</div>
<span className="text-12-regular-160 text-gray-500">
{isValidInput ? props.description : "태그는 11자로 이내로 작성해주세요"}
</span>
<div css={{ height: "1.6rem" }} />
{
/**
* 입력한 태그의 연관 검색 태그 리스트
*/
<Chip color="primary" label="박명수" />
}
</div>
);
};
45 changes: 45 additions & 0 deletions src/features/upload/components/TitleInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { useState } from "react";

import { borderStyle } from "../styles";

export const TitleInput = () => {
const [text, setText] = useState<string>("");
const [focus, setFocus] = useState(false);

const isValidInput = text.length < 24;
const isFilled = !focus && text.length;

return (
<>
<input
maxLength={24}
placeholder=" "
type="text"
value={text}
className={`peer w-full border-b px-4 pb-4 placeholder:text-gray-500 focus:outline-none ${
!isValidInput
? borderStyle.error
: focus
? borderStyle.active
: isFilled
? borderStyle.none
: borderStyle.normal
}`}
onBlur={() => setFocus(false)}
onChange={(e) => setText(e.target.value)}
onFocus={() => setFocus(true)}
/>
<div className="flex justify-between px-4">
<span className="text-12-regular-160 text-gray-500">
{isValidInput
? "밈을 잘 설명할 수 있는 제목을 작성해주세요."
: "24자 미만으로 작성해주세요."}
</span>
<span className="text-12-regular-160 text-gray-500">{text.length}/24</span>
</div>
<span className="pointer-events-none absolute inset-y-0 left-20 text-gray-500 peer-[:not(:placeholder-shown)]:opacity-0">
제목 작성 <span className="text-secondary-700">*</span>
</span>
</>
);
};
23 changes: 21 additions & 2 deletions src/features/upload/components/UploadInitialMeme.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,38 @@
import type { ChangeEventHandler } from "react";
import type { ChangeEventHandler, Dispatch, SetStateAction } from "react";

import { Icon } from "@/common/components/Icon";
import { useToast } from "@/common/hooks";
import { UploadImage } from "@/features/upload/components/UploadImage";
import { UploadMemeData } from "@/features/upload/components/UploadMemeData";
import type { MemeUploadFormData } from "@/types";

interface Props {
onChange?: () => void;
setMemeData: Dispatch<SetStateAction<MemeUploadFormData[]>>;
}

export const UploadInitialMeme = ({ onChange }: Props) => {
export const UploadInitialMeme = ({ onChange, setMemeData }: Props) => {
const { show } = useToast();
const handleChange: ChangeEventHandler<HTMLInputElement> = (e) => {
if (!e.target.files) {
return;
}
onChange?.();
const length = e.target?.files?.length || 0;
const imageArray = Array.from(e.target.files);

imageArray.forEach((image, index) => {
const reader = new FileReader();

reader.onload = () => {
setMemeData((prevMemeData) => [
...prevMemeData,
{ index: index, image: reader.result, title: "", tags: [] },
]);
};

reader.readAsDataURL(image);
});
show(`${length > 1 ? "여러" : length}개의 밈을 선택했어요!`, {
className: "mb-16 drop-shadow-[0_0_20px_rgba(255,255,255,0.5)]",
});
Expand Down
Loading

0 comments on commit 6711142

Please sign in to comment.