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

Feat/post #13

Merged
merged 12 commits into from
Jan 19, 2025
5 changes: 5 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { useAuth } from './hooks/useAuth';
import ExplorePage from './pages/ExplorePage';
import LoginPage from './pages/LoginPage';
import MainPage from './pages/MainPage';
import ProfileEditPage from './pages/ProfileEditPage';
import ProfilePage from './pages/ProfilePage';
import RegisterPage from './pages/RegisterPage';
import type { LoginContextType } from './types/auth';
Expand Down Expand Up @@ -47,6 +48,10 @@ export const App = () => {
path="/:username"
element={auth.isLoggedIn ? <ProfilePage /> : <Navigate to="/" />}
/>
<Route
path="/accounts/edit"
element={auth.isLoggedIn ? <ProfileEditPage /> : <Navigate to="/" />}
/>
</Routes>
</LoginContext.Provider>
);
Expand Down
47 changes: 47 additions & 0 deletions src/api/follow.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
export const followUser = async (token: string, followId: number) => {
try {
const response = await fetch(
`http://3.34.185.81:8000/api/follower/follow?follow_id=${followId}`,
{
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
accept: 'application/json',
},
},
);

if (!response.ok) {
throw new Error('Follow request failed');
}

return true;
} catch (err) {
console.error('Follow error:', err);
return false;
}
};

export const unfollowUser = async (token: string, followId: number) => {
try {
const response = await fetch(
`http://3.34.185.81:8000/api/follower/unfollow?follow_id=${followId}`,
{
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
accept: 'application/json',
},
},
);

if (!response.ok) {
throw new Error('Unfollow request failed');
}

return true;
} catch (error) {
console.error('Error unfollowing user:', error);
return false;
}
};
24 changes: 24 additions & 0 deletions src/api/myProfile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import type { UserProfile } from '../types/user';

export const myProfile = async (token: string) => {
try {
const response = await fetch(
'https://waffle-instaclone.kro.kr/api/user/profile',
{
headers: {
Authorization: `Bearer ${token}`,
accept: 'application/json',
},
},
);

if (response.ok) {
const profileData = (await response.json()) as UserProfile;
return profileData;
}
throw new Error('Profile fetch failed');
} catch (err) {
console.error('Profile fetch error:', err);
return null;
}
};
22 changes: 22 additions & 0 deletions src/api/signup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
interface SignupRequest {
username: string;
password: string;
full_name: string;
email: string;
phone_number: string;
}

export const signup = async (formData: SignupRequest) => {
const response = await fetch('http://3.34.185.81:8000/api/user/signup', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(formData),
});

if (!response.ok) {
const errorData = (await response.json()) as { detail?: string };
throw new Error(errorData.detail ?? '회원가입에 실패했습니다.');
}
};
36 changes: 36 additions & 0 deletions src/api/singin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { myProfile } from './myProfile';

interface SignInResponse {
access_token: string;
refresh_token: string;
}

export const signin = async (username: string, password: string) => {
const response = await fetch(
'https://waffle-instaclone.kro.kr/api/user/signin',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
username,
password,
}),
},
);

if (response.ok) {
const data = (await response.json()) as SignInResponse;
localStorage.setItem('access_token', data.access_token);
localStorage.setItem('refresh_token', data.refresh_token);

return await myProfile(data.access_token);
}

if (response.status === 401) {
throw new Error('아이디 또는 비밀번호가 일치하지 않습니다.');
}

throw new Error('로그인 중 오류가 발생했습니다.');
};
35 changes: 35 additions & 0 deletions src/api/updateProfile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import type { UserProfile } from '../types/user';

type ProfileUpdateData = {
username?: string;
introduce?: string;
profile_image?: File;
};

export const updateProfile = async (
data: ProfileUpdateData,
): Promise<UserProfile> => {
const formData = new FormData();

if (data.username != null) formData.append('username', data.username);
if (data.introduce != null) formData.append('introduce', data.introduce);
if (data.profile_image != null)
formData.append('profile_image', data.profile_image);

const response = await fetch(
'https://waffle-instaclone.kro.kr/api/user/profile/edit',
{
method: 'PATCH',
headers: {
Authorization: `Bearer ${localStorage.getItem('access_token') as string}`,
},
body: formData,
},
);

if (!response.ok) {
throw new Error('Failed to update profile');
}

return response.json() as Promise<UserProfile>;
};
16 changes: 16 additions & 0 deletions src/api/userProfile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import type { UserProfile } from '../types/user';

export async function fetchUserProfile(username: string): Promise<UserProfile> {
try {
const response = await fetch(
`https://waffle-instaclone.kro.kr/api/user/${username}`,
);
if (!response.ok) {
throw new Error('Failed to fetch user data');
}
return (await response.json()) as UserProfile;
} catch (error) {
console.error('Error fetching user data:', error);
throw error;
}
}
20 changes: 16 additions & 4 deletions src/components/feed/Post.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@ import { Heart, MessageCircle, Send } from 'lucide-react';
type PostProps = {
username: string;
imageUrl: string;
location: string;
caption: string;
likes: number;
comments: number;
timestamp: string;
creation_date: string;
};

const Post = ({
Expand All @@ -15,7 +16,7 @@ const Post = ({
caption,
likes,
comments,
timestamp,
creation_date,
}: PostProps) => (
<div className="bg-white border rounded-md">
<div className="flex items-center p-4">
Expand All @@ -26,7 +27,18 @@ const Post = ({
/>
<span className="ml-3 font-semibold">{username}</span>
</div>
<img src={imageUrl} alt="Post" className="w-full" />
<img
src={
imageUrl.startsWith('http')
? imageUrl
: `https://waffle-instaclone.kro.kr/${imageUrl}`
}
alt="Post"
className="w-full"
onError={(e) => {
e.currentTarget.src = '/placeholder.svg';
}}
/>
<div className="p-4">
<div className="flex space-x-4 mb-4">
<Heart className="w-6 h-6" />
Expand All @@ -38,7 +50,7 @@ const Post = ({
<span className="font-semibold">{username}</span> {caption}
</p>
<p className="text-gray-500 text-sm mt-2">View all {comments} comments</p>
<p className="text-gray-400 text-xs mt-1">{timestamp}</p>
<p className="text-gray-400 text-xs mt-1">{creation_date}</p>
</div>
</div>
);
Expand Down
11 changes: 10 additions & 1 deletion src/components/feed/Posts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,16 @@ const Posts = ({ posts, postsPerPage }: PostsProps) => {
return (
<div className="space-y-8">
{currentPosts.map((post) => (
<Post key={post.id} {...post} />
<Post
key={post.post_id}
username={post.user_id.toString()}
imageUrl={post.file_url[0] as string}
caption={post.post_text}
likes={0}
location={post.location}
creation_date={post.creation_date}
comments={0}
/>
))}
<div className="flex justify-center mt-8 mb-16 md:mb-8">
<button
Expand Down
84 changes: 72 additions & 12 deletions src/components/layout/SideBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,39 +7,100 @@ import {
Search,
User,
} from 'lucide-react';
import { useContext, useState } from 'react';
import { Link } from 'react-router-dom';
import { useContext, useEffect, useState } from 'react';
import { Link, useLocation } from 'react-router-dom';

import { LoginContext } from '../../App';
import CreatePostModal from '../modals/CreatePostModal';
import { NavItem } from './NavItem';

const SideBar = () => {
const [isMenuOpen, setIsMenuOpen] = useState(false);
const [activeItem, setActiveItem] = useState('home');
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
const location = useLocation();

const context = useContext(LoginContext);

if (context === null) {
throw new Error('LoginContext is not provided');
}

useEffect(() => {
const path = location.pathname;
if (path === '/') {
setActiveItem('home');
} else if (path === '/explore') {
setActiveItem('explore');
} else if (path === `/${String(context.myProfile?.username)}`) {
setActiveItem('profile');
} else {
setActiveItem('');
}
}, [location.pathname, context.myProfile, context.myProfile?.username]);

const handleCreateClick = (itemName: string) => {
setActiveItem(itemName);
setIsCreateModalOpen(true);
};

return (
<div className="hidden md:flex md:flex-col h-full px-4 py-8">
<div className="mb-8">
<img src="/instagram-logo.png" alt="Instagram" className="w-24" />
<img
src="https://a.slack-edge.com/production-standard-emoji-assets/14.0/apple-small/[email protected]"
alt="Logo"
className="w-16"
/>
</div>

<div className="flex flex-col flex-1 space-y-2">
<Link to="/">
<NavItem icon={<Home />} label="Home" active />
<NavItem
icon={<Home />}
label="Home"
active={activeItem === 'home'}
/>
</Link>
<NavItem icon={<Search />} label="Search" active={false} />
<NavItem
icon={<Search />}
label="Search"
active={activeItem === 'search'}
/>
<Link to="/explore">
<NavItem icon={<Compass />} label="Explore" active={false} />
<NavItem
icon={<Compass />}
label="Explore"
active={activeItem === 'explore'}
/>
</Link>
<NavItem icon={<Heart />} label="Notifications" active={false} />
<NavItem icon={<PlusSquare />} label="Create" active={false} />
<Link to="/username">
<NavItem icon={<User />} label="Profile" active={false} />
<NavItem
icon={<Heart />}
label="Notifications"
active={activeItem === 'notifications'}
/>
<NavItem
icon={<PlusSquare />}
label="Create"
active={activeItem === 'create'}
onClick={() => {
handleCreateClick('create');
}}
/>
{isCreateModalOpen && (
<CreatePostModal
isOpen={isCreateModalOpen}
onClose={() => {
setIsCreateModalOpen(false);
}}
/>
)}
<Link to={`/${String(context.myProfile?.username)}`}>
<NavItem
icon={<User />}
label="Profile"
active={activeItem === 'profile'}
/>
</Link>
</div>

Expand All @@ -57,8 +118,7 @@ const SideBar = () => {
<button
className="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 w-full text-left"
onClick={() => {
// TODO: 로그아웃 로직 구현
context.handleIsLoggedIn(false);
context.handleIsLoggedIn(false, context.myProfile);
setIsMenuOpen(false);
}}
>
Expand Down
Loading
Loading