Skip to content

Commit

Permalink
✨ Feat: 모달 API 기능 구현 (#67)
Browse files Browse the repository at this point in the history
* ✨ Feat: 후원하기 api 기능 구현

* 🔨 Refactor: 모달 디렉토리 배치

* ✨ Feat: 투표하기 api 기능 구현

* 🔨 Refactor: 후원 및 투표 여부 boolean 값으로 변경

* ✨ Feat: 차트모달 로딩, 로딩에러 구현

* 🔨 Refacto: 버튼 안에 로딩스피너 스타일 수정
  • Loading branch information
easyhyun00 authored May 14, 2024
1 parent b345991 commit 2825653
Show file tree
Hide file tree
Showing 10 changed files with 184 additions and 55 deletions.
13 changes: 13 additions & 0 deletions src/apis/postVotes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { baseAxios } from './api';
import { votesErrorMessage } from '@/constants/errorMessage';

export const postVotes = async (idolId) => {
try {
const response = await baseAxios.post('/votes', {
idolId,
});
return response.data;
} catch (error) {
throw new Error(votesErrorMessage);
}
};
13 changes: 13 additions & 0 deletions src/apis/putContribute.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { baseAxios } from './api';
import { contributeErrorMessage } from '@/constants/errorMessage';

export const putContribute = async (id, amount) => {
try {
const response = await baseAxios.put(`/donations/${id}/contribute`, {
amount,
});
return response.data;
} catch (error) {
throw new Error(contributeErrorMessage);
}
};
2 changes: 2 additions & 0 deletions src/constants/errorMessage.js
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
export const loadingErrorMessage = '로딩에 실패하였습니다 \n다시 시도해주세요';
export const contributeErrorMessage = '후원에 실패하였습니다';
export const votesErrorMessage = '투표에 실패하였습니다';
15 changes: 12 additions & 3 deletions src/pages/ListPage/Donation/components/Card/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import CustomButton from '@/components/CustomButton';
import DonationModal from '../DonationModal';
import useModal from '@/hooks/useModal';

const Card = ({ item }) => {
const Card = ({ item, setIsDonate }) => {
const [isOpen, openModal, closeModal] = useModal();

return (
Expand All @@ -14,7 +14,11 @@ const Card = ({ item }) => {
<div className={style.gradient} />
<img src={item.idol.profilePicture} alt={item.idol.name} />
<div className={style.button}>
<CustomButton btnText="후원하기" onClick={openModal} />
<CustomButton
btnText={item.status ? '후원하기' : '목표달성 🎉'}
onClick={openModal}
disabled={!item.status}
/>
</div>
</div>
<div>
Expand All @@ -28,7 +32,12 @@ const Card = ({ item }) => {
targetDonation={item.targetDonation}
/>
</div>
<DonationModal isOpen={isOpen} closeModal={closeModal} item={item} />
<DonationModal
isOpen={isOpen}
closeModal={closeModal}
item={item}
setIsDonate={setIsDonate}
/>
</article>
);
};
Expand Down
43 changes: 33 additions & 10 deletions src/pages/ListPage/Donation/components/DonationModal/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,36 @@ import { inputToNumber } from '@/utils/input';
import { getCredit, getUpdateCredit } from '@/contexts/CreditContext';
import style from './styles.module.scss';
import { toast } from 'react-toastify';
import { putContribute } from '@/apis/putContribute';
import Spinner from '@/assets/icons/Spinner';

const DonationModal = ({ isOpen, closeModal, item }) => {
const DonationModal = ({ isOpen, closeModal, item, setIsDonate }) => {
const [creditInput, setCreditInput] = useState('');
const [isLoading, setIsLoading] = useState(false);

const credit = getCredit();
const setCredit = getUpdateCredit();

// 후원할 값 입력
const handleInputChange = (event) => {
setCreditInput(event.target.value);
const handleInputChange = (e) => {
setCreditInput(e.target.value);
};

// 후원하기 버튼 클릭
const handleDonateClick = () => {
const handleDonateClick = async () => {
if (parseInt(creditInput) && creditInput <= credit) {
setCredit(parseInt(credit - creditInput));
toast(`🎉 ${creditInput} 크레딧 후원 성공!`);
handleCloseModal();
try {
setIsLoading(true);
await putContribute(item.id, creditInput);
setCredit(parseInt(credit - creditInput));
setIsDonate(true);
toast(`🎉 ${creditInput} 크레딧 후원 성공!`);
} catch (error) {
toast.error(error.message);
} finally {
setIsLoading(false);
handleCloseModal();
}
}
};

Expand All @@ -34,6 +46,15 @@ const DonationModal = ({ isOpen, closeModal, item }) => {
closeModal();
};

// 버튼 내용
const buttonContent = isLoading ? (
<div className={style.spinner}>
<Spinner width={40} height={40} fill="white" />
</div>
) : (
'후원하기'
);

return (
<Modal isOpen={isOpen} title="모달" onClose={handleCloseModal}>
<ModalHeader title="후원하기" onClose={handleCloseModal} />
Expand All @@ -58,16 +79,18 @@ const DonationModal = ({ isOpen, closeModal, item }) => {
handleInputChange(e);
}}
/>

<div className={style.message}>
{creditInput > credit && (
<p>갖고 있는 크레딧보다 더 많이 후원할 수 없어요</p>
)}
</div>
<CustomButton
btnText="후원하기"
btnText={buttonContent}
disabled={
!creditInput || creditInput > credit || creditInput[0] === '0'
!creditInput ||
creditInput > credit ||
creditInput[0] === '0' ||
isLoading
}
onClick={handleDonateClick}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,8 @@ $color-error: #ff2626;
@include font-base($color-error, 500, 12px, 14px);
}
}

.spinner {
display: flex;
justify-content: center;
}
8 changes: 5 additions & 3 deletions src/pages/ListPage/Donation/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import Spinner from '@/assets/icons/Spinner';
const Donation = () => {
const [isLoading, loadingError, handleLoad] = useLoad(getDonations);
const [donationList, setDonationList] = useState(null);
const [isDonate, setIsDonate] = useState(false);

const handleDonationLoad = async () => {
const donations = await handleLoad();
Expand All @@ -21,7 +22,8 @@ const Donation = () => {

useEffect(() => {
handleDonationLoad();
}, []);
setIsDonate(false);
}, [isDonate]);

return (
<section className={style.container}>
Expand All @@ -36,10 +38,10 @@ const Donation = () => {
<LoadingError errorMessage={loadingError.message} />
</div>
)}
{!loadingError && donationList && (
{!isLoading && !loadingError && donationList && (
<Carousel customSettings={carouselSettings}>
{donationList.map((item) => {
return <Card item={item} key={item.id} />;
return <Card item={item} key={item.id} setIsDonate={setIsDonate} />;
})}
</Carousel>
)}
Expand Down
124 changes: 87 additions & 37 deletions src/pages/ListPage/MonthlyChart/components/ChartModal/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,18 @@ import { getCredit, getUpdateCredit } from '@/contexts/CreditContext';
import style from './styles.module.scss';
import { toast } from 'react-toastify';
import { FEMALE } from '@/constants/tabTypes';
import { postVotes } from '@/apis/postVotes';
import Spinner from '@/assets/icons/Spinner';
import useLoad from '@/hooks/useLoad';
import { getCharts } from '@/apis/getCharts';
import LoadingError from '@/components/LoadingError';

const ChartModal = ({ isOpen, closeModal, idolList, currentTab }) => {
const ChartModal = ({ isOpen, closeModal, currentTab, setIsVote }) => {
const [idolList, setIdolList] = useState([]);
const [isApiLoading, loadingError, handleLoad] = useLoad(getCharts);
const [selectedIdol, setSelectedIdol] = useState(null);
const [windowWidth, setWindowWidth] = useState(window.innerWidth);
const [isLoading, setIsLoading] = useState(false);
const expandSize = 550;

const credit = getCredit();
Expand All @@ -21,18 +29,37 @@ const ChartModal = ({ isOpen, closeModal, idolList, currentTab }) => {
const title =
currentTab === FEMALE ? '이달의 여자 아이돌' : '이달의 남자 아이돌';

const handleChartLoad = async () => {
const chart = await handleLoad({
gender: currentTab,
pageSize: 30,
});
if (chart) {
setIdolList(chart.idols);
}
};

// 투표할 아이돌 선택
const handleSelectIdol = (idol) => {
setSelectedIdol(idol);
};

// 투표하기 클릭
const handleChartClick = () => {
const handleChartClick = async () => {
if (selectedIdol) {
const newCredit = parseInt(credit - 1000);
if (newCredit >= 0) {
setCredit(newCredit);
toast(`🎉 ${selectedIdol.group} ${selectedIdol.name} 투표 완료!`);
try {
setIsLoading(true);
await postVotes(selectedIdol.id);
setCredit(newCredit);
setIsVote(true);
toast(`🎉 ${selectedIdol.group} ${selectedIdol.name} 투표 완료!`);
} catch (error) {
toast.error(error.message);
} finally {
setIsLoading(false);
}
} else {
toast.error('투표하기 위한 크레딧 부족!');
}
Expand All @@ -52,6 +79,19 @@ const ChartModal = ({ isOpen, closeModal, idolList, currentTab }) => {
};
}, []);

useEffect(() => {
handleChartLoad();
}, []);

// 버튼 내용
const buttonContent = isLoading ? (
<div className={style.spinner}>
<Spinner width={35} height={35} fill="white" />
</div>
) : (
'투표하기'
);

return (
<Modal isOpen={isOpen} title="모달" onClose={closeModal}>
{windowWidth <= expandSize ? (
Expand All @@ -65,43 +105,53 @@ const ChartModal = ({ isOpen, closeModal, idolList, currentTab }) => {
})}
>
<div className={style.chart}>
{idolList.map((idol, index) => {
return (
<div key={idol.id}>
<label className={style.idol}>
<div className={style.info}>
<Profile
size="sm"
imageUrl={idol.profilePicture}
clicked={selectedIdol === idol}
/>
<div className={style.text}>
<span>{index + 1}</span>
<div className={style.name}>
<span>
{idol.group} {idol.name}
</span>
<span>{idol.totalVotes.toLocaleString('ko-KR')}</span>
{isApiLoading && (
<div className={style.loading}>
<Spinner />
</div>
)}
{loadingError && <LoadingError errorMessage={loadingError.message} />}
{!isApiLoading &&
!loadingError &&
idolList.map((idol, index) => {
return (
<div key={idol.id}>
<label className={style.idol}>
<div className={style.info}>
<Profile
size="sm"
imageUrl={idol.profilePicture}
clicked={selectedIdol === idol}
/>
<div className={style.text}>
<span>{idol.rank}</span>
<div className={style.name}>
<span>
{idol.group} {idol.name}
</span>
<span>
{idol.totalVotes.toLocaleString('ko-KR')}
</span>
</div>
</div>
</div>
</div>
<input
className={style.radio}
type="radio"
name="idol"
onClick={() => handleSelectIdol(idol)}
/>
</label>
{index !== idolList.length - 1 && (
<div className={style.division} />
)}
</div>
);
})}
<input
className={style.radio}
type="radio"
name="idol"
onClick={() => handleSelectIdol(idol)}
/>
</label>
{index !== idolList.length - 1 && (
<div className={style.division} />
)}
</div>
);
})}
</div>
<CustomButton
btnText="투표하기"
disabled={!selectedIdol}
btnText={buttonContent}
disabled={!selectedIdol || isLoading}
onClick={handleChartClick}
/>
<p className={style.bottom}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@
overflow-y: auto;
}

.loading {
display: flex;
justify-content: center;
}

.idol {
display: flex;
align-items: center;
Expand Down Expand Up @@ -99,3 +104,8 @@
color: $color-brand-orange;
}
}

.spinner {
display: flex;
justify-content: center;
}
Loading

0 comments on commit 2825653

Please sign in to comment.