Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

에디터/게시판 글 디자인을 일관성 있게 수정한다 #97

Merged
merged 8 commits into from
Dec 27, 2024
47 changes: 47 additions & 0 deletions src/components/pages/post/PostAdjacentButtons.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { ChevronLeft, ChevronRight } from 'lucide-react';
import { Link } from 'react-router-dom';
import { useQuery } from '@tanstack/react-query';
import { Button } from '@/components/ui/button';
import { fetchAdjacentPosts } from '@/utils/postUtils';

interface PostAdjacentButtonsProps {
boardId: string;
postId: string;
}

export function PostAdjacentButtons({ boardId, postId }: PostAdjacentButtonsProps) {
const { data: adjacentPosts } = useQuery(
['adjacentPosts', boardId, postId],
() => fetchAdjacentPosts(boardId, postId),
{
enabled: !!boardId && !!postId,
}
);

return (
<div className='mt-6 flex justify-between'>
{adjacentPosts?.prevPost ? (
<Link to={`/board/${boardId}/post/${adjacentPosts.prevPost}`}>
<Button variant='ghost' className='px-0 hover:bg-transparent'>
<ChevronLeft className='mr-2 size-4' /> 이전 글
</Button>
</Link>
) : (
<Button variant='ghost' disabled className='px-0'>
<ChevronLeft className='mr-2 size-4' /> 이전 글
</Button>
)}
{adjacentPosts?.nextPost ? (
<Link to={`/board/${boardId}/post/${adjacentPosts.nextPost}`}>
<Button variant='ghost' className='px-0 hover:bg-transparent'>
다음 글 <ChevronRight className='ml-2 size-4' />
</Button>
</Link>
) : (
<Button variant='ghost' disabled className='px-0'>
다음 글 <ChevronRight className='ml-2 size-4' />
</Button>
)}
</div>
);
}
18 changes: 18 additions & 0 deletions src/components/pages/post/PostBackButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { ChevronLeft } from 'lucide-react';
import { Link } from 'react-router-dom';
import { Button } from '@/components/ui/button';

interface PostBackButtonProps {
boardId: string;
className?: string;
}

export function PostBackButton({ boardId, className }: PostBackButtonProps) {
return (
<Link to={`/board/${boardId}`}>
<Button variant='ghost' className={className}>
<ChevronLeft className='mr-2 size-4' /> 피드로 돌아가기
</Button>
</Link>
);
}
76 changes: 14 additions & 62 deletions src/components/pages/post/PostCreationPage.tsx
Original file line number Diff line number Diff line change
@@ -1,55 +1,19 @@
import { ChevronLeft } from 'lucide-react';
import React, { useState, useRef, useEffect } from 'react';
import ReactQuill from 'react-quill-new';
import { useNavigate, Link, useParams } from 'react-router-dom';
import React, { useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { Button } from '@/components/ui/button';
import { cn } from '@/lib/utils';
import { useAuth } from '../../../contexts/AuthContext';
import { createPost } from '@/utils/postUtils';
import 'react-quill-new/dist/quill.snow.css';

const TitleInput = React.forwardRef<
HTMLTextAreaElement,
React.TextareaHTMLAttributes<HTMLTextAreaElement>
>(({ className, ...props }, ref) => {
const innerRef = useRef<HTMLTextAreaElement>(null);

useEffect(() => {
if (innerRef.current) {
innerRef.current.style.height = 'auto';
innerRef.current.style.height = `${innerRef.current.scrollHeight}px`;
}
}, [props.value]);

useEffect(() => {
if (typeof ref === 'function') {
ref(innerRef.current);
} else if (ref) {
ref.current = innerRef.current;
}
}, [ref]);

return (
<textarea
ref={innerRef}
className={cn(
'w-full resize-none overflow-hidden text-3xl font-bold focus:outline-none placeholder:text-muted-foreground',
className,
)}
rows={1}
{...props}
/>
);
});

TitleInput.displayName = 'TitleInput';
import { PostTextEditor } from './PostTextEditor';
import { PostTitleEditor } from './PostTitleEditor';
import { PostBackButton } from './PostBackButton';

export default function PostCreationPage() {
const [title, setTitle] = useState<string>('');
const [content, setContent] = useState<string>('');
const { currentUser } = useAuth();
const navigate = useNavigate();
const { boardId } = useParams<{ boardId: string }>();

const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!title.trim() || !content.trim()) return;
Expand All @@ -64,32 +28,20 @@ export default function PostCreationPage() {
};

return (
<div className='mx-auto max-w-3xl px-4 py-8'>
<Link to={`/board/${boardId}`}>
<Button variant='ghost' className='mb-6'>
<ChevronLeft className='mr-2 size-4' /> 피드로 돌아가기
</Button>
</Link>
<div className='mx-auto max-w-4xl px-6 sm:px-8 lg:px-12 py-8'>
{boardId && <PostBackButton boardId={boardId} className='mb-6' />}
<form onSubmit={handleSubmit} className='space-y-6'>
<TitleInput
<PostTitleEditor
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder='제목을 입력하세요'
className='mb-4'
/>
<div className='min-h-[300px]'>
<ReactQuill
value={content}
onChange={setContent}
placeholder='내용을 입력하세요...'
className='h-full'
modules={{
toolbar: [['bold'], ['link']],
}}
/>
</div>
<PostTextEditor
value={content}
onChange={setContent}
/>
<div className='flex justify-end'>
<Button type='submit' className='px-6 py-2'>
<Button type='submit' className='px-6'>
게시하기
</Button>
</div>
Expand Down
63 changes: 13 additions & 50 deletions src/components/pages/post/PostDetailPage.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import DOMPurify from 'dompurify';
import { deleteDoc, doc } from 'firebase/firestore';
import { ChevronLeft, ChevronRight, Edit, Trash2 } from 'lucide-react';
import { Edit, Trash2 } from 'lucide-react';
import { useNavigate, useParams, Link } from 'react-router-dom';
import { Button } from '@/components/ui/button';
import { Skeleton } from '@/components/ui/skeleton';
import { fetchUserNickname } from '@/utils/userUtils';
import { useAuth } from '../../../contexts/AuthContext';
import { firestore } from '../../../firebase';
import { useQuery } from '@tanstack/react-query';
import { fetchPost, fetchAdjacentPosts } from '../../../utils/postUtils';
import { fetchPost } from '../../../utils/postUtils';
import Comments from '../comment/Comments';
import { PostBackButton } from './PostBackButton';
import { PostAdjacentButtons } from './PostAdjacentButtons';

const deletePost = async (boardId: string, id: string): Promise<void> => {
await deleteDoc(doc(firestore, `boards/${boardId}/posts`, id));
Expand Down Expand Up @@ -52,17 +54,9 @@ export default function PostDetailPage() {
}
);

const { data: adjacentPosts } = useQuery(
['adjacentPosts', boardId, postId],
() => fetchAdjacentPosts(boardId!, postId!),
{
enabled: !!boardId && !!postId,
}
);

if (isLoading) {
return (
<div className='mx-auto max-w-4xl px-4 py-8'>
<div className='mx-auto max-w-4xl px-6 sm:px-8 lg:px-12 py-8'>
<Skeleton className='mb-4 h-12 w-3/4' />
<Skeleton className='mb-2 h-4 w-full' />
<Skeleton className='mb-2 h-4 w-full' />
Expand All @@ -73,13 +67,9 @@ export default function PostDetailPage() {

if (error || !post) {
return (
<div className='mx-auto max-w-4xl px-4 py-8 text-center'>
<div className='mx-auto max-w-4xl px-6 sm:px-8 lg:px-12 py-8 text-center'>
<h1 className='mb-4 text-2xl font-bold'>게시물을 찾을 수 없습니다.</h1>
<Link to={`/board/${boardId}`}>
<Button>
<ChevronLeft className='mr-2 size-4' /> 피드로 돌아가기
</Button>
</Link>
{boardId && <PostBackButton boardId={boardId} />}
</div>
);
}
Expand All @@ -92,16 +82,12 @@ export default function PostDetailPage() {
});

return (
<div className='mx-auto max-w-4xl px-4 py-8'>
<Link to={`/board/${boardId}`}>
<Button variant='ghost' className='mb-6'>
<ChevronLeft className='mr-2 size-4' /> 피드로 돌아가기
</Button>
</Link>
<div className='mx-auto max-w-4xl px-6 sm:px-8 lg:px-12 py-8'>
{boardId && <PostBackButton boardId={boardId} className='mb-6' />}
<article className='space-y-6'>
<header className='space-y-4'>
<h1 className='text-4xl font-bold leading-tight'>{post.title}</h1>
<div className='flex items-center justify-between text-sm text-muted-foreground'>
<h1 className='text-4xl font-bold leading-tight tracking-tight text-gray-900 dark:text-gray-100 sm:text-5xl mb-4'>{post.title}</h1>
<div className='flex items-center justify-between text-sm text-gray-500 dark:text-gray-400'>
<p>
작성자: {authorNickname || '??'} | 작성일: {post.createdAt?.toLocaleString() || '?'}
</p>
Expand All @@ -127,34 +113,11 @@ export default function PostDetailPage() {
</header>
<div
dangerouslySetInnerHTML={{ __html: sanitizedContent }}
className='prose prose-lg max-w-none'
className='prose prose-lg prose-slate dark:prose-invert max-w-none mt-6 prose-h1:text-3xl prose-h1:font-semibold prose-h2:text-2xl prose-h2:font-semibold'
/>
</article>
<div className='mt-12 border-t border-gray-200'></div>
<div className='mt-6 flex justify-between'>
{adjacentPosts?.prevPost ? (
<Link to={`/board/${boardId}/post/${adjacentPosts.prevPost}`}>
<Button variant='outline'>
<ChevronLeft className='mr-2 size-4' /> 이전 글
</Button>
</Link>
) : (
<Button variant='outline' disabled>
<ChevronLeft className='mr-2 size-4' /> 이전 글
</Button>
)}
{adjacentPosts?.nextPost ? (
<Link to={`/board/${boardId}/post/${adjacentPosts.nextPost}`}>
<Button variant='outline'>
다음 글 <ChevronRight className='ml-2 size-4' />
</Button>
</Link>
) : (
<Button variant='outline' disabled>
다음 글 <ChevronRight className='ml-2 size-4' />
</Button>
)}
</div>
{boardId && postId && <PostAdjacentButtons boardId={boardId} postId={postId} />}
<div className='mt-12'>
{boardId && postId && <Comments boardId={boardId} postId={postId} />}
</div>
Expand Down
35 changes: 18 additions & 17 deletions src/components/pages/post/PostEditPage.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
import { ChevronLeft, Save } from 'lucide-react';
import React, { useState } from 'react';
import ReactQuill from 'react-quill';
import { useParams, useNavigate } from 'react-router-dom';
import { useQuery } from '@tanstack/react-query';

import 'react-quill/dist/quill.snow.css';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardFooter } from '@/components/ui/card';
import { Skeleton } from '@/components/ui/skeleton';
import { fetchPost, updatePost } from '../../../utils/postUtils';
import { PostTextEditor } from './PostTextEditor';
import { PostTitleEditor } from './PostTitleEditor';
import { PostBackButton } from './PostBackButton';


export default function PostEditPage() {
const { postId, boardId } = useParams<{ postId: string; boardId: string }>();
const [title, setTitle] = useState<string>('');
const [content, setContent] = useState<string>('');
const navigate = useNavigate();

Expand All @@ -22,6 +25,7 @@ export default function PostEditPage() {
enabled: !!boardId && !!postId,
onSuccess: (fetchedPost) => {
if (fetchedPost) {
setTitle(fetchedPost.title);
setContent(fetchedPost.content);
}
},
Expand All @@ -30,7 +34,7 @@ export default function PostEditPage() {

const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!content.trim() || !postId) return;
if (!title.trim() || !content.trim() || !postId) return;
try {
await updatePost(boardId!, postId!, content);
navigate(`/board/${boardId}/post/${postId}`);
Expand All @@ -39,13 +43,9 @@ export default function PostEditPage() {
}
};

const modules = {
toolbar: [['bold'], ['link']],
};

if (isLoading) {
return (
<div className='mx-auto max-w-2xl px-4 py-8'>
<div className='mx-auto max-w-4xl px-6 sm:px-8 lg:px-12 py-8'>
<Skeleton className='mb-4 h-12 w-3/4' />
<Skeleton className='mb-2 h-4 w-full' />
<Skeleton className='mb-2 h-4 w-full' />
Expand All @@ -56,7 +56,7 @@ export default function PostEditPage() {

if (error || !post) {
return (
<div className='mx-auto max-w-2xl px-4 py-8 text-center'>
<div className='mx-auto max-w-4xl px-6 sm:px-8 lg:px-12 py-8 text-center'>
<h1 className='mb-4 text-2xl font-bold'>게시물을 찾을 수 없습니다.</h1>
<Button onClick={() => navigate(`/board/${boardId}`)}>
<ChevronLeft className='mr-2 size-4' /> 피드로 돌아가기
Expand All @@ -66,26 +66,26 @@ export default function PostEditPage() {
}

return (
<div className='mx-auto max-w-2xl px-4 py-8'>
<Button variant='ghost' onClick={() => navigate(`/board/${boardId}`)} className='mb-6'>
<ChevronLeft className='mr-2 size-4' /> 피드로 돌아가기
</Button>
<div className='mx-auto max-w-4xl px-6 sm:px-8 lg:px-12 py-8'>
{boardId && <PostBackButton boardId={boardId} className='mb-6' />}
<form onSubmit={handleSubmit}>
<Card>
<CardHeader className='flex flex-col space-y-2'>
<h1 className='text-3xl font-bold'>{post.title}</h1>
<PostTitleEditor
value={title}
onChange={(e) => setTitle(e.target.value)}
/>
<div className='flex items-center justify-between text-sm text-muted-foreground'>
<p>
작성자: {post.authorName} | 작성일: {post.createdAt?.toLocaleString() || '?'}
</p>
</div>
</CardHeader>
<CardContent>
<ReactQuill
<PostTextEditor
value={content}
onChange={setContent}
modules={modules}
className='min-h-[200px]'
placeholder='내용을 수정하세요...'
/>
</CardContent>
<CardFooter className='flex justify-end'>
Expand All @@ -98,3 +98,4 @@ export default function PostEditPage() {
</div>
);
}

Loading
Loading