Skip to content

Commit

Permalink
Merge pull request #535 from gloddy-dev/feature/528-create-post
Browse files Browse the repository at this point in the history
Feature: 게시글 생성 페이지 퍼블리싱
  • Loading branch information
dev-dong-su authored Dec 26, 2023
2 parents a3df4bd + 47b93a7 commit bfd6b55
Show file tree
Hide file tree
Showing 12 changed files with 514 additions and 4 deletions.
94 changes: 94 additions & 0 deletions src/app/[lng]/(main)/community/write/components/ImageSection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import Image from 'next/image';
import { memo, useCallback } from 'react';
import { Control, useController } from 'react-hook-form';

import { WriteFormType } from '../type';
import { Icon } from '@/components/Icon';
import { Flex } from '@/components/Layout';
import { Loading } from '@/components/Loading';
import { useFileUpload } from '@/hooks/useFileUpload';

interface ImageSectionProps {
control: Control<WriteFormType>;
}

export default function ImageSection({ control }: ImageSectionProps) {
const {
field: { value, onChange },
} = useController({
name: 'images',
control,
});

const { handleFileUploadClick, isLoading } = useFileUpload((files) => {
onChange([...value, ...files]);
});

const handleDeleteClick = useCallback(
(imageUrl: string) => onChange(value.filter((v) => v !== imageUrl)),
[onChange, value]
);

return (
<section className="px-20 pb-8 pt-16">
<Flex className="gap-8">
{value.map((imageUrl, index) => (
<ImageThumbnail key={imageUrl + index} imageUrl={imageUrl} onClick={handleDeleteClick} />
))}
{isLoading && (
<Flex
direction="column"
justify="center"
align="center"
className="h-96 w-96 rounded-8 bg-card-ui"
>
<Loading />
</Flex>
)}
{value.length < 3 && !(isLoading && value.length === 2) && (
<AddImageButton imageCount={value.length} onClick={handleFileUploadClick} />
)}
</Flex>
</section>
);
}

interface AddImageSectionProps {
imageCount: number;
onClick: () => void;
}

function AddImageButton({ imageCount, onClick }: AddImageSectionProps) {
return (
<Flex
direction="column"
justify="center"
align="center"
className="h-96 w-96 cursor-pointer rounded-8 bg-sub"
onClick={onClick}
>
<Icon id="48-add_photo" width={48} height={48} />
<p className="text-caption text-sign-caption">{imageCount}/3</p>
</Flex>
);
}

interface ImageThumbnailProps {
imageUrl: string;
onClick: (imageUrl: string) => void;
}

const ImageThumbnail = memo(({ imageUrl, onClick }: ImageThumbnailProps) => {
return (
<div className="relative h-96 w-96">
<Image src={imageUrl} alt="select-img" className="rounded-8 object-cover" fill />
<Icon
id="32-close"
width={32}
height={32}
className="absolute right-0 top-0 cursor-pointer"
onClick={() => onClick(imageUrl)}
/>
</div>
);
});
93 changes: 93 additions & 0 deletions src/app/[lng]/(main)/community/write/components/InputSection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
'use client';
import { SubmitHandler, useForm } from 'react-hook-form';

import WriteModal from '../components/WriteModal';
import { WriteFormType } from '../type';
import { useTranslation } from '@/app/i18n/client';
import { Button, ButtonGroup } from '@/components/Button';
import MultiImageUploader from '@/components/Image/MultiImageUploader';
import ListBoxController from '@/components/ListBox/ListBoxController';
import { Spacing } from '@/components/Spacing';
import { TextFieldController } from '@/components/TextField';
import { useModal } from '@/hooks/useModal';

export default function InputSection() {
const { open, exit } = useModal();
const { t } = useTranslation('community');
const hookForm = useForm<WriteFormType>({
mode: 'onChange',
defaultValues: {
category: 'NONE',
title: '',
content: '',
images: [],
},
});

const { register, handleSubmit, formState, control } = hookForm;

const onSubmit: SubmitHandler<WriteFormType> = (formData) => {
console.log(formData);
};

const options = [t('create.category.kpop'), t('create.category.qna'), t('create.category.lang')];

return (
<section>
<div className="px-20 pb-8 pt-20">
<Spacing size={4} />
<ListBoxController
name={t('create.category.name')}
options={options}
register={register('category', {
required: true,
validate: (value) => value !== 'NONE',
})}
/>

<Spacing size={4} />
<TextFieldController
placeholder={t('create.title.placeholder')}
hookForm={hookForm}
register={register('title', {
required: true,
maxLength: 60,
})}
maxCount={60}
/>
</div>

<div className="px-20 py-8">
<Spacing size={4} />
<TextFieldController
placeholder={t('create.content.placeholder')}
register={register('content', {
required: true,
maxLength: 300,
})}
hookForm={hookForm}
as="textarea"
maxCount={300}
className={'h-339'}
/>
</div>

<MultiImageUploader<WriteFormType> control={control} name={'images'} />

<Spacing size={60} />
<ButtonGroup>
<Button
onClick={() => {
handleSubmit(onSubmit);
open(() => (
<WriteModal onOkClick={handleSubmit(onSubmit)} onCancelClick={exit} type={'write'} />
));
}}
disabled={!formState.isValid}
>
{t('create.submit.label')}
</Button>
</ButtonGroup>
</section>
);
}
40 changes: 40 additions & 0 deletions src/app/[lng]/(main)/community/write/components/WriteHeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
'use client';

import WriteModal from '../components/WriteModal';
import { useTranslation } from '@/app/i18n/client';
import { IconButton } from '@/components/Button';
import { Header } from '@/components/Header';
import { Icon } from '@/components/Icon';
import useAppRouter from '@/hooks/useAppRouter';
import { useModal } from '@/hooks/useModal';

export default function WriteHeader() {
const { t } = useTranslation('community');
const { back } = useAppRouter();
const { open, exit } = useModal();

return (
<Header>
<Header.Left>
<IconButton
size="large"
onClick={() =>
open(() => (
<WriteModal
type="cancel"
onCancelClick={exit}
onOkClick={() => {
exit();
back();
}}
/>
))
}
>
<Icon id="24-arrow_back" />
</IconButton>
<p className="w-full truncate">{t('create.headerTitle')}</p>
</Header.Left>
</Header>
);
}
47 changes: 47 additions & 0 deletions src/app/[lng]/(main)/community/write/components/WriteModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { useTranslation } from '@/app/i18n/client';
import { Icon } from '@/components/Icon';
import { Modal } from '@/components/Modal';
import { Spacing } from '@/components/Spacing';

import type { ComponentProps } from 'react';

type ModalStyleType = {
[key in WriteModalProps['type']]: {
variant: NonNullable<ComponentProps<typeof Modal>['variant']>;
iconId: string;
content: string;
};
};

interface WriteModalProps {
type: 'write' | 'cancel';
onOkClick: () => void;
onCancelClick: () => void;
}

export default function WriteModal({ type, onOkClick, onCancelClick }: WriteModalProps) {
const { t } = useTranslation('community');
const modalStyle: ModalStyleType = {
write: {
variant: 'success',
iconId: '48-check',
content: t('create.submit.content'),
},
cancel: {
variant: 'warning',
iconId: '48-warning',
content: t('create.cancel.content'),
},
};
const { variant, iconId, content } = modalStyle[type];

return (
<Modal variant={variant} onCancelClick={onCancelClick} onOkClick={onOkClick}>
<Spacing size={32} />
<Icon id={iconId} width={48} height={48} />
<Spacing size={12} />
<p className="text-paragraph-1">{content}</p>
<Spacing size={16} />
</Modal>
);
}
10 changes: 7 additions & 3 deletions src/app/[lng]/(main)/community/write/page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import InputSection from '@/app/[lng]/(main)/community/write/components/InputSection';
import WriteHeader from '@/app/[lng]/(main)/community/write/components/WriteHeader';

export default function CommunityWritePage() {
return (
<div>
<h1>커뮤니티 글 작성 페이지</h1>
</div>
<>
<WriteHeader />
<InputSection />
</>
);
}
6 changes: 6 additions & 0 deletions src/app/[lng]/(main)/community/write/type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export interface WriteFormType {
title: string;
content: string;
category: string;
images: string[];
}
26 changes: 25 additions & 1 deletion src/app/i18n/locales/ko/community.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,29 @@
"all": "전체",
"daily": "일상톡톡",
"question": "궁금해요",
"language": "언어교환"
"language": "언어교환",

"create": {
"headerTitle": "게시글 작성",
"category": {
"name": "카테고리",

"kpop": "K-POP",
"qna": "궁금해요",
"lang": "언어교환"
},
"title": {
"placeholder": "게시글 제목"
},
"content": {
"placeholder": "최소 20글자 이상의 게시글을 작성해보세요."
},
"submit": {
"label": "글쓰기",
"content": "게시글을 등록하시겠습니까?"
},
"cancel": {
"content": "게시글 작성을 취소하시겠습니"
}
}
}
Loading

0 comments on commit bfd6b55

Please sign in to comment.