Skip to content

Commit

Permalink
feat: Activity Editor 추가 (#46)
Browse files Browse the repository at this point in the history
* chore(package): JoditEditor 추가

* feat: Project Editor 추가

* feat: Social Editor 추가
  • Loading branch information
son-daehyeon authored Sep 17, 2024
1 parent 1f561ea commit 76dd515
Show file tree
Hide file tree
Showing 9 changed files with 430 additions and 11 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
},
"dependencies": {
"framer-motion": "^11.5.4",
"jodit-react": "^4.1.2",
"js-cookie": "^3.0.5",
"next": "^14.2.6",
"react": "^18.3.1",
Expand Down
171 changes: 169 additions & 2 deletions src/app/admin/activity/project/editor/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,172 @@
const AdminActivityProjectEditorPage = () => {
return <></>;
'use client';

import React, { useEffect } from 'react';
import { FaSave, FaUpload } from 'react-icons/fa';
import { toast } from 'react-toastify';

import Image from 'next/image';
import { useRouter } from 'next/navigation';

import { Editor, FormContainer, IconButton, TextField, Title } from '@/component';

import { useForm } from '@/hook';

import { WinkApi } from '@/api';

import * as yup from 'yup';

type Inputs = 'title' | 'tags' | 'content' | 'image';

interface AdminActivityProjectEditorPageProps {
params: { id: string };
}

const AdminActivityProjectEditorPage = ({ params }: AdminActivityProjectEditorPageProps) => {
const router = useRouter();

const { values, setValues, errors, onChange, validate } = useForm<Inputs, string>(
yup.object({
title: yup
.string()
.required('제목을 입력해주세요.')
.min(4, '제목은 4글자 이상이어야 합니다.'),
tags: yup.string().required('태그를 입력해주세요.'),
content: yup.string().required('내용을 입력해주세요.'),
image: yup.string().required('이미지를 업로드해주세요.'),
}),
);

useEffect(() => {
if (params.id !== 'new') {
(async () => {
const { project } = await WinkApi.Activity.Project.getProject({ projectId: params.id });

setValues({
title: project.title,
tags: project.tags.join(', '),
content: project.content,
image: project.image,
});
})();
}
}, []);

const handleImageUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];

if (file) {
const { link } = await WinkApi.Activity.CommonAdmin.upload(file);
setValues((prev) => ({ ...prev, image: link }));
}
};

const handleSave = async () => {
if (!(await validate())) {
return;
}

if (params.id === 'new') {
const { project } = await WinkApi.Activity.ProjectAdmin.createProject({
title: values.title,
tags: values.tags.split(',').map((tag) => tag.trim()),
content: values.content,
image: values.image,
});

toast.success('프로젝트를 생성했습니다.');

router.push(`/activity/project/${project._id}`);
} else {
await WinkApi.Activity.ProjectAdmin.updateProject({
projectId: params.id,
title: values.title,
tags: values.tags.split(',').map((tag) => tag.trim()),
content: values.content,
image: values.image,
});

toast.success('프로젝트를 수정했습니다.');

router.push(`/activity/project/${params.id}`);
}
};

return (
<div className="container mx-auto mt-4">
<Title title="Activity" subtitle="프로젝트" />

<div className="mt-1">
<div className="flex justify-end mb-4">
<IconButton
icon={<FaSave />}
text={params.id === 'new' ? '생성' : '수정'}
className="bg-wink-500 hover:bg-wink-600 border-0 text-white"
onClick={handleSave}
/>
</div>

<input
id="fileInput"
type="file"
accept="image/*"
onChange={handleImageUpload}
className="hidden"
/>

{values.image ? (
<div
className="relative h-44 cursor-pointer"
onClick={() => {
document.getElementById('fileInput')?.click();
}}
>
<Image
src={values.image}
className="rounded-xl"
layout="fill"
objectFit="cover"
alt="image"
/>
</div>
) : (
<div
className="flex justify-center items-center h-44 border border-dashed border-gray-300 rounded-xl cursor-pointer hover:bg-gray-50 transition"
onClick={() => {
document.getElementById('fileInput')?.click();
}}
>
<label htmlFor="fileInput" className="flex flex-col items-center justify-center">
<FaUpload className="text-4xl text-gray-400 mb-1" size={24} />
<span className="text-gray-600">Upload Image</span>
</label>
</div>
)}

<FormContainer values={values} errors={errors} onChange={onChange}>
<div className="flex flex-col items-center gap-2.5 my-2.5">
<TextField
type="text"
id="title"
placeholder="프로젝트 이름을 입력해주세요."
className="p-2 text-sm border"
/>

<TextField
type="text"
id="tags"
placeholder="태그를 입력해주세요. (,로 구분)"
className="p-2 text-sm border"
/>
</div>
</FormContainer>

<Editor
content={values.content}
setContent={(value) => setValues((prev) => ({ ...prev, content: value }))}
/>
</div>
</div>
);
};

export default AdminActivityProjectEditorPage;
11 changes: 9 additions & 2 deletions src/app/admin/activity/project/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { FaTrashCan } from 'react-icons/fa6';
import { toast } from 'react-toastify';

import Link from 'next/link';
import { useRouter } from 'next/navigation';

import { IconButton, Modal, SearchBar, TablePaging, Title } from '@/component';

Expand All @@ -14,6 +15,8 @@ import { ProjectType, WinkApi } from '@/api';
import { formatDate } from '@/util';

const AdminActivityProjectPage = () => {
const router = useRouter();

const [projects, setProjects] = useState<ProjectType[]>([]);

const [page, setPage] = useState<number>(1);
Expand Down Expand Up @@ -72,7 +75,7 @@ const AdminActivityProjectPage = () => {
icon={<FaEdit />}
text="프로젝트 추가"
className="bg-wink-500 hover:bg-wink-600 border-0 text-white"
onClick={() => {}}
onClick={() => router.push('/admin/activity/project/editor/new')}
/>
</div>

Expand Down Expand Up @@ -101,7 +104,11 @@ const AdminActivityProjectPage = () => {
</Link>
<div className="py-4 px-4 col-span-3 text-sm">{formatDate(project.createdAt)}</div>
<div className="col-span-1 flex items-center justify-center space-x-8">
<FaEdit size={18} className="cursor-pointer" onClick={() => {}} />
<FaEdit
size={18}
className="cursor-pointer"
onClick={() => router.push(`/admin/activity/project/editor/${project._id}`)}
/>
</div>
<div className="col-span-1 flex items-center justify-center space-x-8">
<FaTrashCan
Expand Down
174 changes: 171 additions & 3 deletions src/app/admin/activity/social/editor/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,173 @@
const AdminActivityProjectEditorPage = () => {
return <></>;
'use client';

import React, { useEffect } from 'react';
import { FaMinus, FaPlus, FaSave, FaUpload } from 'react-icons/fa';
import { toast } from 'react-toastify';

import Image from 'next/image';
import { useRouter } from 'next/navigation';

import { Editor, IconButton, TextField, Title } from '@/component';

import { Content, WinkApi } from '@/api';

interface AdminActivitySocialEditorPageProps {
params: { id: string };
}

const AdminActivitySocialEditorPage = ({ params }: AdminActivitySocialEditorPageProps) => {
const router = useRouter();
const [title, setTitle] = React.useState<string>('');
const [contents, setContents] = React.useState<Content[]>([
{
content: '',
image: '',
},
]);

useEffect(() => {
if (params.id !== 'new') {
(async () => {
const { social } = await WinkApi.Activity.Social.getSocial({ socialId: params.id });

setTitle(social.title);
setContents(social.contents);
})();
}
}, []);

const handleImageUpload = async (e: React.ChangeEvent<HTMLInputElement>, index: number) => {
const file = e.target.files?.[0];

if (file) {
const { link } = await WinkApi.Activity.CommonAdmin.upload(file);
setContents((prev) => {
prev[index].image = link;
return [...prev];
});
}
};

const handleSave = async () => {
if (params.id === 'new') {
await WinkApi.Activity.SocialAdmin.createSocial({
title,
contents,
});

toast.success('친목 활동 생성했습니다.');

router.push('/activity/social');
} else {
await WinkApi.Activity.SocialAdmin.updateSocial({
socialId: params.id,
title,
contents,
});

toast.success('친목 활동 수정했습니다.');

router.push(`/activity/social`);
}
};

return (
<div className="container mx-auto mt-4">
<Title title="Activity" subtitle="친목 활동" />

<div className="mt-1">
<div className="flex justify-end mb-4 gap-2">
<IconButton
icon={<FaPlus />}
text="페이지 추가"
className="bg-green-500 hover:bg-green-600 border-0 text-white"
onClick={() => setContents((prev) => [...prev, { content: '', image: '' }])}
/>

<IconButton
icon={<FaMinus />}
text="페이지 삭제"
disabled={contents.length === 1}
className="bg-red-500 hover:bg-red-600 border-0 text-white disabled:bg-gray-300"
onClick={() => setContents((prev) => prev.slice(0, -1))}
/>

<IconButton
icon={<FaSave />}
text={params.id === 'new' ? '생성' : '수정'}
className="bg-wink-500 hover:bg-wink-600 border-0 text-white"
onClick={handleSave}
/>
</div>

<div className="flex flex-col items-center gap-2.5 my-2.5">
<TextField
type="text"
id="title"
value={title}
placeholder="친목 활동을 입력해주세요."
className="px-3 py-4 border-border rounded focus:outline-none focus:ring-1 focus:ring-wink-300 placeholder-gray-600 p-2 text-sm border"
onChange={(e) => setTitle(e.target.value)}
/>
</div>

{contents.map((content, i) => (
<div key={i}>
<input
id={`fileInput-${i}`}
type="file"
accept="image/*"
onChange={(e) => handleImageUpload(e, i)}
className="hidden"
/>

{content.image ? (
<div
className="relative h-80 cursor-pointer"
onClick={() => {
document.getElementById(`fileInput-${i}`)?.click();
}}
>
<Image
src={content.image}
className="rounded-xl"
layout="fill"
objectFit="cover"
alt="image"
/>
</div>
) : (
<div
className="flex justify-center items-center h-80 border border-dashed border-gray-300 rounded-xl cursor-pointer hover:bg-gray-50 transition"
onClick={() => {
document.getElementById(`fileInput-${i}`)?.click();
}}
>
<label htmlFor="fileInput" className="flex flex-col items-center justify-center">
<FaUpload className="text-4xl text-gray-400 mb-1" size={24} />
<span className="text-gray-600">Upload Image</span>
</label>
</div>
)}

<div className="mt-2">
<Editor
content={content.content}
setContent={(value) =>
setContents((prev) => {
prev[i].content = value;
return [...prev];
})
}
/>
</div>

{i !== contents.length - 1 && <div className="border-b border-gray-200 my-4" />}
</div>
))}
</div>
</div>
);
};

export default AdminActivityProjectEditorPage;
export default AdminActivitySocialEditorPage;
Loading

0 comments on commit 76dd515

Please sign in to comment.