Skip to content

Commit

Permalink
Follow & edit
Browse files Browse the repository at this point in the history
  • Loading branch information
IceCandle committed Jan 17, 2025
1 parent 99568af commit cfb6743
Show file tree
Hide file tree
Showing 10 changed files with 593 additions and 81 deletions.
62 changes: 62 additions & 0 deletions src/components/profile/FollowButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';

interface FollowButtonProps {
userId: number;
onFollowChange?: (isFollowing: boolean) => void;
}

const FollowButton = ({ userId, onFollowChange }: FollowButtonProps) => {
const [isFollowing, setIsFollowing] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const navigate = useNavigate();

const handleFollow = async () => {
try {
setIsLoading(true);
const token = localStorage.getItem('access_token');
if (token === null) {
void navigate('/');
return;
}

const endpoint = isFollowing ? 'unfollow' : 'follow';
const response = await fetch(
`http://3.34.185.81:8000/api/follower/${endpoint}?follow_id=${userId}`,
{
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
},
}
);

if (!response.ok) {
throw new Error(`Failed to ${endpoint}`);
}

setIsFollowing(!isFollowing);
onFollowChange?.(!isFollowing);
} catch (error) {
console.error('Error following/unfollowing:', error);
} finally {
setIsLoading(false);
}
};

return (
<button
onClick={() => { void handleFollow(); }}
disabled={isLoading}
className={`px-6 py-2 rounded font-semibold text-sm ${
isFollowing
? 'bg-gray-200 text-gray-800 hover:bg-gray-300'
: 'bg-blue-500 text-white hover:bg-blue-600'
} transition-colors duration-200 disabled:opacity-50`}
>
{isLoading ? 'Loading...' : isFollowing ? 'Following' : 'Follow'}
</button>
);
};

export default FollowButton;
144 changes: 113 additions & 31 deletions src/components/profile/ProfileInfo.tsx
Original file line number Diff line number Diff line change
@@ -1,57 +1,139 @@
import { Settings } from 'lucide-react';
import { Link as LinkIcon } from 'lucide-react';
import { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';

type ProfileInfoProps = {
import FollowButton from './FollowButton';

interface ProfileInfoProps {
userId: number;
username: string;
posts: number;
followers: number;
following: number;
fullName: string;
bio: string;
};
website: string | null;
profileImage: string;
}

const ProfileInfo = ({
userId,
username,
posts,
followers,
following,
fullName,
bio,
website,
profileImage,
}: ProfileInfoProps) => {
const [isCurrentUser, setIsCurrentUser] = useState(false);
const [currentFollowers, setCurrentFollowers] = useState(followers);
const navigate = useNavigate();

useEffect(() => {
const checkCurrentUser = async () => {
try {
const token = localStorage.getItem('access_token');
if (token === null) {
void navigate('/');
return;
}

const response = await fetch('http://3.34.185.81:8000/api/user/profile', {
headers: {
Authorization: `Bearer ${token}`,
},
});

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

interface UserProfile {
user_id: number;
}

const data: UserProfile = await response.json() as UserProfile;
setIsCurrentUser(data.user_id === userId);
} catch (error) {
console.error('Error checking current user:', error);
}
};

void checkCurrentUser();
}, [userId, navigate]);

const handleFollowChange = (isFollowing: boolean) => {
setCurrentFollowers((prev) => (isFollowing ? prev + 1 : prev - 1));
};

const handleEditProfile = () => {
void navigate(`/settings`);
};

return (
<div className="mb-8">
<div className="flex items-center mb-4">
<img
src="/placeholder.svg"
alt={username}
className="w-20 h-20 rounded-full mr-4"
/>
<div className="flex-1">
<div className="flex items-center mb-2">
<h1 className="text-xl font-semibold mr-4">{username}</h1>
<button className="bg-blue-500 text-white px-4 py-1 rounded-md text-sm font-semibold mr-2">
Follow
</button>
<Settings className="w-6 h-6" />
<div className="flex flex-col md:flex-row items-center md:items-start">
<div className="w-20 h-20 md:w-36 md:h-36 rounded-full overflow-hidden mb-4 md:mb-0 md:mr-8 flex-shrink-0">
<img
src={profileImage.length > 0 ? profileImage : '/placeholder.svg'}
alt={username}
className="w-full h-full object-cover"
/>
</div>

<div className="flex-1 text-center md:text-left">
<div className="flex flex-col md:flex-row md:items-center md:space-x-4 mb-4">
<h2 className="text-xl mb-4 md:mb-0">{username}</h2>
<div className="flex justify-center md:justify-start space-x-2">
{isCurrentUser ? (
<button
onClick={handleEditProfile}
className="bg-gray-100 px-4 py-1.5 rounded font-medium text-sm"
>
Edit Profile
</button>
) : (
<FollowButton userId={userId} onFollowChange={handleFollowChange} />
)}
</div>
</div>
<div className="flex space-x-4 text-sm">
<span>
<strong>{posts}</strong> posts
</span>
<span>
<strong>{followers}</strong> followers
</span>
<span>
<strong>{following}</strong> following
</span>

<div className="flex justify-center md:justify-start space-x-8 mb-4">
<div>
<span className="font-semibold">{posts}</span>
<span className="ml-1 text-gray-700">posts</span>
</div>
<div>
<span className="font-semibold">{currentFollowers}</span>
<span className="ml-1 text-gray-700">followers</span>
</div>
<div>
<span className="font-semibold">{following}</span>
<span className="ml-1 text-gray-700">following</span>
</div>
</div>

<div className="space-y-2">
<h1 className="font-semibold">{fullName}</h1>
{bio.length > 0 && <p className="whitespace-pre-line">{bio}</p>}
{website !== null && website.length > 0 && (
<a
href={website}
target="_blank"
rel="noopener noreferrer"
className="text-blue-900 font-medium flex items-center justify-center md:justify-start"
>
<LinkIcon className="w-4 h-4 mr-1" />
{website}
</a>
)}
</div>
</div>
</div>
<div className="md:hidden">
<h2 className="font-semibold">{fullName}</h2>
<p>{bio}</p>
</div>
</div>
);
};

export default ProfileInfo;
export default ProfileInfo;
77 changes: 77 additions & 0 deletions src/components/profile/ProfilePictureUpload.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { Camera } from 'lucide-react';
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';

interface ProfilePictureUploadProps {
currentImage: string;
onSuccess: (newImageUrl: string) => void;
}

const ProfilePictureUpload = ({ currentImage, onSuccess }: ProfilePictureUploadProps) => {
const [isUploading, setIsUploading] = useState(false);
const navigate = useNavigate();

const handleImageUpload = async (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (file == null) return;

try {
setIsUploading(true);
const token = localStorage.getItem('access_token');
if (token == null) {
void navigate('/');
return;
}

const formData = new FormData();
formData.append('profile_image', file);

const response = await fetch('http://3.34.185.81:8000/api/user/profile/edit', {
method: 'PATCH',
headers: {
'Authorization': `Bearer ${token}`,
},
body: formData,
});

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

const data = (await response.json()) as { profile_image: string };
onSuccess(data.profile_image);
} catch (error) {
console.error('Error uploading profile picture:', error);
} finally {
setIsUploading(false);
}
};

return (
<div className="relative group">
<img
src={currentImage.length > 0 ? currentImage : '/placeholder.svg'}
alt="Profile"
className="w-full h-full object-cover rounded-full"
/>
<label className="absolute inset-0 flex items-center justify-center bg-black bg-opacity-0 group-hover:bg-opacity-50 rounded-full cursor-pointer transition-all">
<div className="text-white opacity-0 group-hover:opacity-100 flex flex-col items-center">
<Camera className="w-6 h-6 mb-1" />
<span className="text-xs">Change Photo</span>
</div>
<input
type="file"
accept="image/*"
onChange={(e) => { void handleImageUpload(e); }}
className="hidden"
disabled={isUploading}
/>
</label>
{isUploading && (
<div className="absolute inset-0 flex items-center justify-center bg-black bg-opacity-50 rounded-full">
<div className="w-6 h-6 border-2 border-white border-t-transparent rounded-full animate-spin"></div>
</div>
)}
</div>
);
};

export default ProfilePictureUpload;
49 changes: 26 additions & 23 deletions src/components/profile/ProfileTabs.tsx
Original file line number Diff line number Diff line change
@@ -1,35 +1,38 @@
import { Bookmark, Grid } from 'lucide-react';
import { Grid } from 'lucide-react';
import { useState } from 'react';

import type { APIPost } from '../../types/post';
import PostGrid from '../shared/PostGrid';
import PostModal from '../shared/PostModal';

const ProfileTabs = () => {
const [activeTab, setActiveTab] = useState('posts');
interface ProfileTabsProps {
postIds: number[];
}

const ProfileTabs = ({ postIds }: ProfileTabsProps) => {
const [selectedPost, setSelectedPost] = useState<APIPost | null>(null);

return (
<div>
<div className="flex justify-around border-t">
<button
className={`flex-1 py-2 ${activeTab === 'posts' ? 'border-t-2 border-black' : ''}`}
onClick={() => {
setActiveTab('posts');
}}
>
<Grid className="w-6 h-6 mx-auto" />
</button>
<button
className={`flex-1 py-2 ${activeTab === 'saved' ? 'border-t-2 border-black' : ''}`}
onClick={() => {
setActiveTab('saved');
}}
>
<Bookmark className="w-6 h-6 mx-auto" />
</button>
<div className="border-t">
<div className="flex justify-center">
<button className="flex items-center py-4 space-x-1 text-sm font-semibold border-t border-black">
<Grid className="w-4 h-4" />
<span>POSTS</span>
</button>
</div>
</div>
{activeTab === 'posts' && <PostGrid />}
{activeTab === 'saved' && <div className="text-center py-8">저장됨</div>}

<PostGrid
postIds={postIds}
onPostClick={(post: APIPost) => { setSelectedPost(post); }}
/>

{selectedPost !== null && (
<PostModal post={selectedPost} onClose={() => { setSelectedPost(null); }} />
)}
</div>
);
};

export default ProfileTabs;
export default ProfileTabs;
Loading

0 comments on commit cfb6743

Please sign in to comment.