diff --git a/src/App.tsx b/src/App.tsx index 8bc3121..2155651 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -7,7 +7,7 @@ import Profile from './routes/profile.tsx'; import SearchResult from './routes/search-result.tsx'; import Auth from './routes/auth.tsx'; import Layout from './components/layout.tsx'; -import LoadingScreen from './components/loading-screen.tsx'; +import LoadingSpinner from './components/loading-spinner.tsx'; import * as S from './styles/global.ts'; const router = createBrowserRouter([ @@ -51,7 +51,7 @@ function App() { return ( <> - {isLoading ? : } + {isLoading ? : } ); } diff --git a/src/assets/images/i-edit.svg b/src/assets/images/i-edit.svg new file mode 100644 index 0000000..dc4d356 --- /dev/null +++ b/src/assets/images/i-edit.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/images/loading-spinner-mini.svg b/src/assets/images/loading-spinner-mini.svg new file mode 100644 index 0000000..976b5bd --- /dev/null +++ b/src/assets/images/loading-spinner-mini.svg @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/images/loading-spinner.svg b/src/assets/images/loading-spinner.svg new file mode 100644 index 0000000..d55d18e --- /dev/null +++ b/src/assets/images/loading-spinner.svg @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/components/edit-tweet-form.tsx b/src/components/edit-tweet-form.tsx new file mode 100644 index 0000000..8f1103c --- /dev/null +++ b/src/components/edit-tweet-form.tsx @@ -0,0 +1,110 @@ +import React, { useState } from 'react'; +import { deleteField, doc, updateDoc } from 'firebase/firestore'; +import { + deleteObject, + getDownloadURL, + ref, + uploadBytes, +} from 'firebase/storage'; +import { auth, db, storage } from '../firebase.ts'; +import ITweet from '../interfaces/ITweet.ts'; +import * as S from '../styles/tweet-form.ts'; +import { ReactComponent as IconPhoto } from '../assets/images/i-photo.svg'; +import { ReactComponent as LoadingSpinner } from '../assets/images/loading-spinner-mini.svg'; + +interface IEditTweetForm extends Pick { + onClose: () => void; +} + +export default function EditTweetForm({ + id, + tweet: initialTweet, + photo: initialPhoto, + onClose, +}: IEditTweetForm) { + const [isLoading, setLoading] = useState(false); + const [tweet, setTweet] = useState(initialTweet); + const [image, setImage] = useState(null); + const [imagePreview, setImagePreview] = useState(initialPhoto); + + const onChange = (e: React.ChangeEvent) => { + setTweet(e.target.value); + }; + const onImageChange = (e: React.ChangeEvent) => { + const images = e.target.files; + if (images && images.length === 1) { + setImage(images[0]); + const previewUrl = URL.createObjectURL(images[0]); + setImagePreview(previewUrl); + } + }; + const onImageDelete = () => { + setImage(null); + setImagePreview(''); + }; + const onSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + const user = auth.currentUser; + if (!user || isLoading || tweet === '' || tweet.length > 180) return; + try { + setLoading(true); + const tweetDocRef = doc(db, 'tweets', id); + await updateDoc(tweetDocRef, { + tweet, + }); + if (image) { + const locationRef = ref( + storage, + initialPhoto || `tweets/${user.uid}/${id}`, + ); + const result = await uploadBytes(locationRef, image); + const url = await getDownloadURL(result.ref); + await updateDoc(tweetDocRef, { + photo: url, + }); + } else if (!image && initialPhoto && initialPhoto !== imagePreview) { + const locationRef = ref(storage, initialPhoto); + await deleteObject(locationRef); + await updateDoc(tweetDocRef, { + photo: deleteField(), + }); + } + setImage(null); + } catch (error) { + console.log(error); + } finally { + setLoading(false); + onClose(); + } + }; + return ( + + + {imagePreview ? ( + <> + + + + ) : ( + + + + )} + + + {isLoading ? : '수정'} + + + ); +} diff --git a/src/components/loading-screen.tsx b/src/components/loading-screen.tsx deleted file mode 100644 index be4175e..0000000 --- a/src/components/loading-screen.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import * as S from '../styles/components/loading-screen.ts'; - -export default function LoadingScreen() { - return ( - - Loading... - - ); -} diff --git a/src/components/loading-spinner.tsx b/src/components/loading-spinner.tsx new file mode 100644 index 0000000..1ed6dd3 --- /dev/null +++ b/src/components/loading-spinner.tsx @@ -0,0 +1,10 @@ +import Wrapper from '../styles/loading-spinner.ts'; +import { ReactComponent as Spinner } from '../assets/images/loading-spinner.svg'; + +export default function LoadingSpinner() { + return ( + + + + ); +} diff --git a/src/components/post-tweet-form.tsx b/src/components/post-tweet-form.tsx index f8ce76e..dd905b9 100644 --- a/src/components/post-tweet-form.tsx +++ b/src/components/post-tweet-form.tsx @@ -2,8 +2,9 @@ import React, { useState } from 'react'; import { addDoc, collection, updateDoc } from 'firebase/firestore'; import { getDownloadURL, ref, uploadBytes } from 'firebase/storage'; import { auth, db, storage } from '../firebase.ts'; -import * as S from '../styles/post-tweet-form.ts'; +import * as S from '../styles/tweet-form.ts'; import { ReactComponent as IconPhoto } from '../assets/images/i-photo.svg'; +import { ReactComponent as LoadingSpinner } from '../assets/images/loading-spinner-mini.svg'; export default function PostTweetForm() { const [isLoading, setLoading] = useState(false); @@ -56,7 +57,7 @@ export default function PostTweetForm() { } }; return ( - + - {isLoading ? '포스팅 중...' : '포스팅'} + {isLoading ? : '포스팅'} - + ); } diff --git a/src/components/signIn.tsx b/src/components/sign-in.tsx similarity index 95% rename from src/components/signIn.tsx rename to src/components/sign-in.tsx index 3856e39..39c4242 100644 --- a/src/components/signIn.tsx +++ b/src/components/sign-in.tsx @@ -6,6 +6,7 @@ import { auth } from '../firebase.ts'; import * as S from '../styles/auth.ts'; import * as P from '../styles/popup.ts'; import ImageComputer from '../assets/images/logo-small.png'; +import { ReactComponent as LoadingSpinner } from '../assets/images/loading-spinner-mini.svg'; interface ISignInProps { onClose: () => void; @@ -90,7 +91,7 @@ export default function SignIn({ onClose }: ISignInProps) { required /> - {isLoading ? '로딩...' : '로그인하기'} + {isLoading ? : '로그인하기'} {firebaseError !== '' ? {firebaseError} : null} diff --git a/src/components/signUp.tsx b/src/components/sign-up.tsx similarity index 96% rename from src/components/signUp.tsx rename to src/components/sign-up.tsx index 4007a09..0cdd9b1 100644 --- a/src/components/signUp.tsx +++ b/src/components/sign-up.tsx @@ -7,6 +7,7 @@ import { auth, db } from '../firebase.ts'; import * as S from '../styles/auth.ts'; import * as P from '../styles/popup.ts'; import ImageComputer from '../assets/images/logo-small.png'; +import { ReactComponent as LoadingSpinner } from '../assets/images/loading-spinner-mini.svg'; interface ISignUpProps { onClose: () => void; @@ -116,7 +117,7 @@ export default function SignUp({ onClose }: ISignUpProps) { required /> - {isLoading ? '로딩...' : '가입하기'} + {isLoading ? : '가입하기'} {firebaseError !== '' ? {firebaseError} : null} diff --git a/src/components/socialSignIn.tsx b/src/components/social-sign-in.tsx similarity index 100% rename from src/components/socialSignIn.tsx rename to src/components/social-sign-in.tsx diff --git a/src/components/tweet-form.tsx b/src/components/tweet-form.tsx new file mode 100644 index 0000000..e69de29 diff --git a/src/components/tweet.tsx b/src/components/tweet.tsx index d6117d3..9da3043 100644 --- a/src/components/tweet.tsx +++ b/src/components/tweet.tsx @@ -4,9 +4,11 @@ import { deleteObject, ref } from 'firebase/storage'; import { auth, db, storage } from '../firebase.ts'; import ITweet from '../interfaces/ITweet.ts'; import FormattedDate from '../utils/formattedDate.tsx'; +import EditTweetForm from './edit-tweet-form.tsx'; import * as S from '../styles/tweet.ts'; import * as P from '../styles/popup.ts'; import { ReactComponent as IconUser } from '../assets/images/i-user.svg'; +import { ReactComponent as IconEdit } from '../assets/images/i-edit.svg'; export default function Tweet({ id, @@ -23,6 +25,10 @@ export default function Tweet({ const userData = userDoc.data(); setUserAvatar(userData?.userAvatar || null); }; + const [editPopup, setEditPopup] = useState(false); + const toggleEditPopup = () => { + setEditPopup(!editPopup); + }; const [deletePopup, setDeletePopup] = useState(false); const toggleDeletePopup = () => { setDeletePopup(!deletePopup); @@ -43,7 +49,7 @@ export default function Tweet({ }; useEffect(() => { fetchUserAvatar(); - }, [userId, userAvatar]); + }, [userName, userAvatar]); return ( @@ -57,11 +63,28 @@ export default function Tweet({ {photo ? : null} {user?.uid === userId ? ( <> + + 포스팅 수정하기 + + 포스팅 삭제하기 ) : null} + {editPopup ? ( + + + + setEditPopup(false)} + /> + + + ) : null} {deletePopup ? ( diff --git a/src/styles/button.ts b/src/styles/button.ts index d52540c..5aee82a 100644 --- a/src/styles/button.ts +++ b/src/styles/button.ts @@ -4,9 +4,14 @@ import { blackColor, grayColor, primaryColor, whiteColor } from './global.ts'; const Button = styled.button` width: 100%; margin: 8px 0; - padding: 10px 20px; + padding: 0 20px; border-radius: 50px; font-size: 16px; + line-height: 36px; + svg { + width: 36px; + height: 36px; + } `; export const LineButton = styled(Button)` diff --git a/src/styles/components/loading-screen.ts b/src/styles/components/loading-screen.ts deleted file mode 100644 index bd6848b..0000000 --- a/src/styles/components/loading-screen.ts +++ /dev/null @@ -1,12 +0,0 @@ -import styled from 'styled-components'; - -export const Wrapper = styled.div` - display: flex; - justify-content: center; - align-items: center; - height: 100vh; -`; - -export const Text = styled.span` - font-size: 24px; -`; diff --git a/src/styles/loading-spinner.ts b/src/styles/loading-spinner.ts new file mode 100644 index 0000000..c246a75 --- /dev/null +++ b/src/styles/loading-spinner.ts @@ -0,0 +1,22 @@ +import styled from 'styled-components'; + +const Wrapper = styled.div` + z-index: 100; + position: fixed; + top: 0; + bottom: 0; + left: 0; + right: 0; + display: flex; + justify-content: center; + align-items: center; + svg { + position: relative; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + } +`; + +export default Wrapper; diff --git a/src/styles/popup.ts b/src/styles/popup.ts index d663660..6402733 100644 --- a/src/styles/popup.ts +++ b/src/styles/popup.ts @@ -27,15 +27,14 @@ export const PopupBox = styled.div` align-items: center; justify-content: center; width: calc(100vw - 100px); - padding: 30px; + padding: 50px 30px 30px 30px; background-color: ${whiteColor}; border: 3px solid ${blackColor}; `; export const Popup = styled(PopupBox)` - max-width: 757px; - height: calc(100vh - 100px); - max-height: 728px; + max-width: 600px; + max-height: calc(100vh - 100px); border-radius: 10px; `; diff --git a/src/styles/post-tweet-form.ts b/src/styles/tweet-form.ts similarity index 93% rename from src/styles/post-tweet-form.ts rename to src/styles/tweet-form.ts index 2fa8165..a82b072 100644 --- a/src/styles/post-tweet-form.ts +++ b/src/styles/tweet-form.ts @@ -9,6 +9,9 @@ export const Form = styled.form` gap: 10px; width: 100%; padding-bottom: 20px; +`; + +export const PostForm = styled(Form)` &::after { content: ''; position: absolute; @@ -21,6 +24,8 @@ export const Form = styled.form` } `; +export const EditForm = styled(Form)``; + export const TextArea = styled.textarea` width: calc(100% - 120px); padding: 20px; @@ -108,10 +113,14 @@ export const AttachImageInput = styled.input` `; export const SubmitButton = styled.button` - padding: 10px 0px; background-color: ${primaryColor}; border: none; border-radius: 20px; color: white; font-size: 16px; + line-height: 36px; + svg { + width: 36px; + height: 36px; + } `; diff --git a/src/styles/tweet.ts b/src/styles/tweet.ts index 98f4a64..11175aa 100644 --- a/src/styles/tweet.ts +++ b/src/styles/tweet.ts @@ -110,5 +110,6 @@ export const EditButton = styled.button` width: 16px; height: 16px; stroke: ${primaryColor}; + stroke-width: 2; } `;