Skip to content

Commit

Permalink
Merge branch 'develop' of https://github.com/GU-99/grow-up-fe into fe…
Browse files Browse the repository at this point in the history
…ature/#69-user-setting-ui
  • Loading branch information
Yoonyesol committed Aug 23, 2024
2 parents 22e0104 + 855f6ab commit d5cb38f
Show file tree
Hide file tree
Showing 16 changed files with 1,517 additions and 75 deletions.
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,12 @@
"react-dom": "^18.2.0",
"react-hook-form": "^7.51.4",
"react-icons": "^5.2.1",
"react-markdown": "^9.0.1",
"react-router-dom": "^6.23.1",
"react-syntax-highlighter": "^15.5.0",
"react-toastify": "^10.0.5",
"rehype-raw": "^7.0.0",
"remark-gfm": "^4.0.0",
"tailwind-scrollbar-hide": "^1.1.7",
"zustand": "^4.5.2"
},
Expand Down
134 changes: 134 additions & 0 deletions src/components/common/CustomMarkdown.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import React from 'react';
import Markdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import rehypeRaw from 'rehype-raw';
import type { Components } from 'react-markdown';

type CustomMarkdownProps = {
markdown: string;
};

const component: Partial<Components> = {
h1(props) {
const { children } = props;
return (
<>
<h1 className="text-3xl">{children}</h1>
<hr className="my-3" />
</>
);
},
h2(props) {
const { children } = props;
return <h2 className="mb-3 text-2xl">{children}</h2>;
},
h3(props) {
const { children } = props;
return <h3 className="mb-3 text-xl">{children}</h3>;
},
h4(props) {
const { children } = props;
return <h3 className="mb-3 text-lg">{children}</h3>;
},
h5(props) {
const { children } = props;
return <h3 className="mb-3 text-base">{children}</h3>;
},
h6(props) {
const { children } = props;
return <h3 className="mb-3 text-sm">{children}</h3>;
},
hr() {
return <hr className="my-5" />;
},
a(props) {
const { href, children } = props;
return (
<a href={href} className="text-cyan-700">
{children}
</a>
);
},
img(props) {
const { src, alt } = props;
return <img src={src} alt={alt} className="m-auto" />;
},
blockquote(props) {
const { children } = props;
return <blockquote className="border-l-[3px] border-[#20C997] bg-[#F8F9FA] p-4">{children}</blockquote>;
},
table(props) {
const { children } = props;
return <table className="border-collapse overflow-hidden rounded-md shadow-md">{children}</table>;
},
th(props) {
const { children, style } = props;
return (
<th style={style} className="bg-[#4CAF50] p-5 font-bold uppercase text-white">
{children}
</th>
);
},
td(props) {
const { children, style } = props;
return (
<td style={style} className="last:border-b-none border-b border-[#dddddd] p-5">
{children}
</td>
);
},
tr(props) {
const { children, style } = props;
return (
<tr style={style} className="border-b border-[#dddddd] transition duration-300 even:bg-[#f4f4f4]">
{children}
</tr>
);
},
ol(props) {
const { children } = props;
const timeKey = Date.now();
return (
<ol key={timeKey} className="ml-10 list-decimal">
{children}
</ol>
);
},
ul(props) {
const { children } = props;
const timeKey = Date.now();
return (
<ul key={timeKey} className="ml-10 list-disc">
{children}
</ul>
);
},
section(props) {
const { children, className } = props;

if (className === 'footnotes') {
const newChildren = React.Children.toArray(children).slice(2) as React.ReactElement[];
return (
<section className={`${className} mt-10 border-t border-[#ddd] bg-[#f9f9f9] p-5`}>
<h3 className="mb-2 font-bold">각주 모음</h3>
{newChildren}
</section>
);
}
return <section className={className}>{children}</section>;
},
code(props) {
const { children, className } = props;
return <code className={`${className} rounded-sm border-none bg-[#E9ECEF] px-2`}>{children}</code>;
},
};

export default function CustomMarkdown({ markdown }: CustomMarkdownProps) {
return (
<section className="rounded-md border border-input p-10 text-sm">
<Markdown components={component} remarkPlugins={[remarkGfm]} rehypePlugins={[rehypeRaw]}>
{markdown}
</Markdown>
</section>
);
}
38 changes: 38 additions & 0 deletions src/components/common/RoleIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { TbChess, TbChessKnight, TbChessQueen } from 'react-icons/tb';
import { Role } from '@/types/RoleType';

type RoleIconProps = {
roleName: Role['roleName'];
};

function getRoleIcon(roleName: Role['roleName']) {
switch (roleName) {
case 'HEAD':
case 'ADMIN':
return <TbChessQueen />;
case 'LEADER':
return <TbChessKnight />;
case 'MATE':
case 'ASSIGNEE':
return <TbChess />;
default:
return null;
}
}

export default function RoleIcon({ roleName }: RoleIconProps) {
return (
<div className="group relative cursor-help">
{getRoleIcon(roleName)}
{/* prettier-ignore */}
<h4 className="
invisible absolute bottom-full left-1/2 -translate-x-1/2 select-none rounded-md bg-black/50 px-5
my-2 text-center text-xs text-white transition-opacity duration-700 group-hover:visible
after:content[''] after:absolute after:top-full after:left-1/2 after:border-4 after:-ml-2
after:border-t-black/50 after:border-b-transparent after:border-x-transparent
">
{roleName}
</h4>
</div>
);
}
41 changes: 30 additions & 11 deletions src/components/modal/task/ModalTaskForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import { IoSearch } from 'react-icons/io5';
import { IoMdCloseCircle } from 'react-icons/io';

import { TASK_VALIDATION_RULES } from '@constants/formValidationRules';
import RoleIcon from '@components/common/RoleIcon';
import ToggleButton from '@components/common/ToggleButton';
import CustomMarkdown from '@components/common/CustomMarkdown';
import DuplicationCheckInput from '@components/common/DuplicationCheckInput';
import useToast from '@hooks/useToast';
import useAxios from '@hooks/useAxios';
Expand All @@ -29,9 +31,11 @@ export default function ModalTaskForm({ formId, project, taskId, onSubmit }: Mod
const { projectId, startDate, endDate } = project;
const debounceRef = useRef<NodeJS.Timeout | null>(null);
const abortControllerRef = useRef<AbortController | null>(null);

const [hasDeadline, setHasDeadline] = useState(false);
const [keyword, setKeyword] = useState('');
const [workers, setWorkers] = useState<UserWithRole[]>([]);
const [preview, setPreview] = useState(false);

const { statusList } = useStatusQuery(projectId, taskId);
const { taskNameList } = useTaskQuery(projectId);
Expand Down Expand Up @@ -70,7 +74,7 @@ export default function ModalTaskForm({ formId, project, taskId, onSubmit }: Mod
}, [fetchData, projectId, keyword]);

useEffect(() => {
if (keyword.trim()) {
if (keyword) {
debounceRef.current = setTimeout(() => searchUsers(), 500);
}
return () => {
Expand All @@ -85,7 +89,9 @@ export default function ModalTaskForm({ formId, project, taskId, onSubmit }: Mod
setHasDeadline((prev) => !prev);
};

const handleKeywordChange = (e: React.ChangeEvent<HTMLInputElement>) => setKeyword(e.target.value);
const handlePreviewToggle = () => setPreview((prev) => !prev);

const handleKeywordChange = (e: React.ChangeEvent<HTMLInputElement>) => setKeyword(e.target.value.trim());

const handleSearchClick = () => searchUsers();

Expand All @@ -100,9 +106,9 @@ export default function ModalTaskForm({ formId, project, taskId, onSubmit }: Mod
const isIncludedUser = workers.find((worker) => worker.userId === user.userId);
if (isIncludedUser) return toastInfo('이미 포함된 수행자입니다');

const newWorkers = [...workers, user];
const workersIdList = newWorkers.map((worker) => worker.userId);
setWorkers(newWorkers);
const updatedWorkers = [...workers, user];
const workersIdList = updatedWorkers.map((worker) => worker.userId);
setWorkers(updatedWorkers);
setValue('userId', workersIdList);
setKeyword('');
clearData();
Expand Down Expand Up @@ -168,8 +174,8 @@ export default function ModalTaskForm({ formId, project, taskId, onSubmit }: Mod
</div>
</label>
<label htmlFor="endDate" className="w-1/2">
<h3 className="flex items-center text-large">
<span className="mr-2">종료일</span>
<h3 className="flex items-center space-x-2 text-large">
<span>종료일</span>
<ToggleButton id="deadline" checked={hasDeadline} onChange={handleDeadlineToggle} />
</h3>
<input
Expand Down Expand Up @@ -211,7 +217,7 @@ export default function ModalTaskForm({ formId, project, taskId, onSubmit }: Mod
<IoSearch className="size-15 text-emphasis hover:text-black" />
</button>
{keyword && !loading && (
<ul className="invisible absolute left-0 right-0 max-h-110 overflow-auto rounded-md border-2 bg-white group-focus-within:visible">
<ul className="invisible absolute left-0 right-0 z-10 max-h-110 overflow-auto rounded-md border-2 bg-white group-focus-within:visible">
{data && data.length === 0 ? (
<div className="h-20 border px-10 leading-8">&apos;{keyword}&apos; 의 검색 결과가 없습니다.</div>
) : (
Expand All @@ -237,7 +243,7 @@ export default function ModalTaskForm({ formId, project, taskId, onSubmit }: Mod
<section className="flex w-full flex-wrap items-center gap-4">
{workers.map((user) => (
<div key={user.userId} className="flex items-center space-x-4 rounded-md bg-button px-5">
<div>{user.roleName}</div>
<RoleIcon roleName={user.roleName} />
<div>{user.nickname}</div>
<button type="button" aria-label="delete-worker" onClick={() => handleDeleteClick(user)}>
<IoMdCloseCircle className="text-error" />
Expand All @@ -248,8 +254,21 @@ export default function ModalTaskForm({ formId, project, taskId, onSubmit }: Mod
</div>

<label htmlFor="content" className="mb-20">
<h3 className="text-large">내용</h3>
<textarea name="content" id="content" className="w-full border" rows={5} />
<h3 className="flex items-center space-x-2">
<span className="text-large">내용</span>
<ToggleButton id="preview" checked={preview} onChange={handlePreviewToggle} />
</h3>
{preview ? (
<CustomMarkdown markdown={watch('content')} />
) : (
<textarea
id="content"
rows={10}
className="w-full border border-input p-10 placeholder:text-xs"
placeholder="마크다운 형식으로 입력해주세요."
{...register('content')}
/>
)}
</label>

<label htmlFor="files">
Expand Down
3 changes: 2 additions & 1 deletion src/components/modal/team/ModalTeamForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@ import type { Team } from '@/types/TeamType';

type ModalTeamFormProps = {
formId: string;
teamId: Team['teamId'];
onSubmit: SubmitHandler<Team>;
};

export default function ModalTeamForm({ formId, onSubmit }: ModalTeamFormProps) {
export default function ModalTeamForm({ formId, teamId, onSubmit }: ModalTeamFormProps) {
const { handleSubmit } = useForm<Team>();

return (
Expand Down
2 changes: 1 addition & 1 deletion src/components/sidebar/ListSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export default function ListSidebar({ label, title, children, showButton, text,
};

return (
<aside className="mr-10 flex w-1/3 flex-col border border-list bg-contents-box">
<aside className="mr-10 flex w-1/3 min-w-125 max-w-220 shrink-0 flex-col border border-list bg-contents-box">
<div className="flex min-h-30 items-center justify-between bg-sub px-10">
<div>
{label && <small className="mr-5 font-bold text-main">{label}</small>}
Expand Down
2 changes: 1 addition & 1 deletion src/layouts/page/ProjectLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export default function ProjectLayout() {
<ListSidebar label="team" title="팀 이름...">
<ListProject data={PROJECT_DUMMY} targetId={projectId} />
</ListSidebar>
<section className="flex w-2/3 flex-col border border-list bg-contents-box">
<section className="flex grow flex-col border border-list bg-contents-box">
<header className="flex h-30 items-center justify-between border-b p-10">
{/* ToDo: LabelTitle 공통 컴포넌트로 추출할 것 */}
<div>
Expand Down
6 changes: 3 additions & 3 deletions src/layouts/page/SettingLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const navList = [
route: 'password',
},
{
label: 'My Teams',
label: '팀 관리',
route: 'teams',
},
];
Expand All @@ -33,8 +33,8 @@ export default function SettingLayout() {
<ListSidebar title={`${USER_INFO_DUMMY.nickname} 님의 정보`}>
<ListSetting navList={navList} />
</ListSidebar>
<section className="flex w-2/3 flex-col border border-list bg-contents-box">
<header className="flex h-25 items-center justify-between border-b p-10">
<section className="flex grow flex-col border border-list bg-contents-box">
<header className="flex h-30 items-center justify-between border-b p-10">
<div>
<small className="font-bold text-category">{getTitle()}</small>
</div>
Expand Down
2 changes: 1 addition & 1 deletion src/layouts/page/TeamLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export default function TeamLayout() {
<ListSidebar title="팀 목록" showButton text="팀 생성" onClick={openTeamModal}>
<ListTeam data={TEAM_DUMMY} targetId={teamId} />
</ListSidebar>
<section className="flex h-full w-2/3 flex-col border border-list bg-contents-box">
<section className="flex grow flex-col border border-list bg-contents-box">
{teamData.length === 0 ? (
<div className="flex h-full items-center justify-center text-center">
소속된 팀이 없습니다! <br />
Expand Down
Loading

0 comments on commit d5cb38f

Please sign in to comment.