juni
TeamProject_아웃소싱 프로젝트 본문
import { useEffect, useState } from 'react';
import { useAddVideo } from '../lib/supabase/videoApi';
import { searchYouTubeVideos } from '../lib/api/youtubeAPI';
import { ToastContainer, toast } from 'react-toastify';
import { supabase } from '../lib/supabase/supabase';
const MainPage = () => {
const [query, setQuery] = useState('');
const [searchResults, setSearchResults] = useState([]);
const [pageToken, setPageToken] = useState('');
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(false);
const addVideoMutation = useAddVideo();
const searchVideos = async (query, pageToken = '', reset = false) => {
setLoading(true);
try {
const youtubeVideos = await searchYouTubeVideos(query, pageToken);
setSearchResults((prevResults) =>
reset
? youtubeVideos.items.map((video) => ({
video_title: video.snippet.title,
video_id: video.id.videoId
}))
: [
...prevResults,
...youtubeVideos.items.map((video) => ({
video_title: video.snippet.title,
video_id: video.id.videoId
}))
]
);
setPageToken(youtubeVideos.nextPageToken || '');
} catch (error) {
console.error('Error fetching data from YouTube API:', error);
}
setLoading(false);
};
const handleSearch = async (e) => {
e.preventDefault();
searchVideos(query, '', true);
};
const handleAddVideo = (video) => {
if (!user) {
toast.error('로그인이 필요합니다.');
return;
}
const videoLike = {
...video,
video_like: user.id
};
try {
if (!toast.isActive('addVideo')) {
toast.success('해당 영상이 저장되었습니다.', {
toastId: 'addVideo'
});
addVideoMutation.mutate(videoLike);
}
} catch (error) {
console.error('Error adding video:', error);
}
};
useEffect(() => {
const fetchUserAndSelection = async () => {
try {
const {
data: { user }
} = await supabase.auth.getUser();
if (!user) {
toast.error('로그인이 필요합니다.');
return;
}
const { data, error } = await supabase.from('users').select('selection').eq('id', user.id).single();
if (error || !data) {
toast.error('설문조사를 완료해야 합니다.');
return;
}
const selectionQuery = `${data.selection.level} ${data.selection.topics.join('|')}`;
setUser({ ...user, selection: data.selection });
setQuery(selectionQuery);
searchVideos(selectionQuery, '', true); // Reset results and search
} catch (error) {
console.error('Error in fetchUserAndSelection:', error);
toast.error('사용자 정보를 가져오는 중 오류가 발생했습니다.');
}
};
fetchUserAndSelection();
}, []);
return (
<div>
<form onSubmit={handleSearch}>
<h1 className="mb-5 flex justify-center font-['DungGeunMo'] text-6xl">DevTube</h1>
<h2 className="mb-5 flex justify-center font-['DungGeunMo'] text-xl">더 많은 내용을 검색하세요!</h2>
<div className="mb-8 flex items-center justify-center">
<input
autoFocus
className="mb-2 box-border rounded border-2 p-1"
type="text"
onChange={(e) => setQuery(e.target.value)}
placeholder=" 오늘은 무슨 공부를 할까?"
/>
<button
className="border-3 mb-2 ml-4 flex cursor-pointer items-center justify-center rounded bg-yellow-300 p-2 text-sm font-bold text-black no-underline hover:underline"
type="submit"
>
검색
</button>
</div>
</form>
<div className="grid grid-cols-3 gap-4">
{searchResults.map((video) => (
<div key={video.video_id}>
<h3 dangerouslySetInnerHTML={{ __html: video.video_title }} className="mb-2 w-full truncate font-bold"></h3>
<div className="aspect-h-9 aspect-w-16">
<iframe
className="h-full w-full"
allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
></iframe>
</div>
<button className="mt-2 font-bold text-green-500" onClick={() => handleAddVideo(video)}>
저장하기
</button>
</div>
))}
</div>
{pageToken && (
<div className="mt-4 flex justify-center">
<button
className="border-3 flex cursor-pointer items-center justify-center rounded bg-black p-2 text-sm font-bold text-white no-underline hover:underline"
onClick={() => searchVideos(query, pageToken)}
disabled={loading}
>
{loading ? '로딩 중...' : '더보기'}
</button>
</div>
)}
</div>
);
};
export default MainPage;
영상을 6개 단위로 더보기 기능을 추가하여 많은 정보를 효과적으로 보여주기 성공
import axios from 'axios';
const apiKey = import.meta.env.VITE_YOUTUBE_API_KEY;
const cgi_1 = 27;
const cgi_2 = 28;
export const searchYouTubeVideos = async (query, pageToken = '') => {
const url = `https://www.googleapis.com/youtube/v3/search?part=snippet&type=video&maxResults=6&videoCategoryId=${(cgi_1, cgi_2)}&q=${query}&key=${apiKey}&pageToken=${pageToken}`;
try {
const response = await axios.get(url);
return response.data;
} catch (error) {
console.error('Error fetching data from YouTube API:', error);
throw error;
}
};
import { ToastContainer, toast } from 'react-toastify';
import { useVideos, useDeleteVideo } from '../lib/supabase/videoApi';
import { Link, useNavigate } from 'react-router-dom';
import { useEffect, useRef, useState } from 'react';
import { supabase } from '../lib/supabase/supabase';
import useIdStore from '../zustand/idStore';
import { updateUserNickname } from '../lib/supabase/userApi';
const MyPage = () => {
const [user, setUser] = useState(null);
const { data: videos, error: fetchError, isLoading } = useVideos();
const deleteVideoMutation = useDeleteVideo();
const { id } = useIdStore((state) => state);
const [nickname, setNickname] = useState('');
const nicknameInput = useRef();
const navigate = useNavigate();
const likeVideos = videos ? videos.filter((video) => video.video_like === user?.id) : [];
const handleDelete = (id) => {
try {
toast.success('해당 영상이 삭제되었습니다.');
deleteVideoMutation.mutate(id);
} catch (error) {
console.error('Error deleting video:', error);
}
};
const updateNickname = async (value, userId) => {
if (!(value.length >= 4 && value.length <= 10)) {
toast.error('닉네임은 4자리 이상, 10자리 이하여야 합니다.');
nicknameInput.current.focus();
return;
}
if (!confirm(`닉네임을 '${value}'로(으로) 변경하시겠습니까?`)) {
nicknameInput.current.focus();
return;
}
const { data, error } = await updateUserNickname(value, userId);
if (error) {
toast.error('닉네임 변경에 실패했습니다. 다시 로그인하시길 바랍니다.');
nicknameInput.current.focus();
return;
}
if (data) {
navigate(0);
return;
}
};
useEffect(() => {
const fetchUser = async () => {
const {
data: { user }
} = await supabase.auth.getUser();
setUser(user);
};
fetchUser();
}, []);
if (isLoading) {
return <div>Loading...</div>;
}
if (fetchError) {
return <div>Error: {fetchError.message}</div>;
}
return (
<>
<div className="mb-20 flex w-full flex-col gap-8">
<h1 className="font-['DungGeunMo'] text-4xl font-bold">닉네임 변경</h1>
<div className="flex gap-5">
<input
className="min-w-72 border-2 border-black p-2 outline-none"
type="text"
placeholder="닉네임을 입력해주세요"
value={nickname}
ref={nicknameInput}
onChange={(e) => {
setNickname(e.target.value);
}}
/>
<button
className="border-2 border-slate-300 bg-slate-100 pl-5 pr-5 hover:bg-slate-200"
onClick={() => updateNickname(nickname, id)}
>
완료
</button>
<button
className="border-2 border-red-500 bg-red-300 pl-5 pr-5 hover:bg-red-400"
onClick={() => navigate('/')}
>
메인으로
</button>
</div>
</div>
<div className="mt-8">
{/* <h1 className="flex justify-center font-bold">Saved Videos</h1> */}
<h1 className="mb-8 flex justify-start font-['DungGeunMo'] text-4xl font-bold">저장한 영상</h1>
<div className="grid grid-cols-3 gap-10">
{likeVideos.map((video) => (
<div key={video.id}>
{/* <h3 dangerouslySetInnerHTML={{ __html: video.video_title }} className="w-full truncate"></h3> */}
<h3
dangerouslySetInnerHTML={{ __html: video.video_title }}
className="mb-2 w-full truncate font-bold"
></h3>
<div className="aspect-h-9 aspect-w-16">
<iframe
className="h-full w-full"
allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
></iframe>
</div>
<button className="mt-2 font-bold text-red-500" onClick={() => handleDelete(video.id)}>
삭제하기
</button>
</div>
))}
</div>
</div>
</>
);
};
export default MyPage;
닉네임 수정도 가능
'프로젝트 > 미니 프로젝트' 카테고리의 다른 글
SingleProject_Next.js 포켓몬 도감 (0) | 2024.07.04 |
---|---|
SingleProject_TS-Favorite-Countries (0) | 2024.06.27 |
TeamProject_아웃소싱 프로젝트 (0) | 2024.06.19 |
TeamProject_아웃소싱 프로젝트 (0) | 2024.06.19 |
버전 관리 예시 (0) | 2024.06.17 |