Skip to content

Commit

Permalink
Feat/post (#13)
Browse files Browse the repository at this point in the history
Co-authored-by: 박세준 <[email protected]>
  • Loading branch information
LikeACloud7 and 박세준 authored Jan 19, 2025
1 parent eaf49b8 commit 0d067ea
Show file tree
Hide file tree
Showing 28 changed files with 925 additions and 234 deletions.
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

0 comments on commit 0d067ea

Please sign in to comment.