仕上がり
目次
1. ブックマーク機能の実装
2. フォロー機能の実装
1. ブックマーク機能の実装
1. モデルを作る
# backend/tweets/models.py
from django.db import models
from django.contrib.auth import get_user_model
# Create your models here.
User = get_user_model()
class Tweet(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="tweets")
content = models.CharField(max_length=280)
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return f"{self.user.username}: {self.content[:30]}"
class Bookmark(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="bookmarks")
tweet = models.ForeignKey(Tweet, on_delete=models.CASCADE, related_name="bookmarked_by")
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
unique_together = ("user", "tweet")
def __str__(self):
return f"{self.user.username} bookmarked {self.tweet.id}"
2. マイグレーションの実行
3. APIの作成
# backend/tweets/views.py
from rest_framework import viewsets, permissions, status
from rest_framework.views import APIView
from.models import Tweet, Bookmark
from.serializers import TweetSerializer
from channels.layers import get_channel_layer
from asgiref.sync import async_to_sync
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
class BookmarkAPIView(APIView):
permission_classes = [IsAuthenticated]
def post(self, request, tweet_id):
user = request.user
try:
tweet = Tweet.objects.get(id=tweet_id)
except Tweet.DoesNotExist:
return Response({"Error": "Tweet not Found."}, status=status.HTTP_404_NOT_FOUND)
bookmark, created = Bookmark.objects.get_or_create(user=user, tweet=tweet)
if not created:
bookmark.delete()
return Response({"Message": "Bookmark removed."}, status=status.HTTP_200_OK)
return Response({"Message": "Tweet bookmarked."}, status=status.HTTP_201_CREATED)
def get(self, request):
user = request.user
bookmarks = Bookmark.objects.filter(user=user).select_related("tweet")
bookmarked_tweets = [
{
"id": b.tweet.id,
"content": b.tweet.content,
"created_at": b.tweet.created_at
}
for b in bookmarks
]
return Response(bookmarked_tweets, status=status.HTTP_200_OK)
def delete(self, request, tweet_id):
user = request.user
try:
bookmark = Bookmark.objects.get(user=user, tweet_id=tweet_id)
bookmark.delete()
return Response({"Message": "Bookmark removed."}, status=status.HTTP_200_OK)
except Bookmark.DoesNotExist:
return Response({"Error": "Bookmark not found."}, status=status.HTTP_404_NOT_FOUND)
class TweetViewSet(viewsets.ModelViewSet):
queryset = Tweet.objects.all().order_by("-created_at")
serializer_class = TweetSerializer
permission_classes = [permissions.IsAuthenticatedOrReadOnly]
def perform_create(self,serializer):
# 新規作成時に "user" を自動的にセット
tweet = serializer.save(user=self.request.user)
# 新しいツイートが投稿されたらWebsocketで通知
channel_layer = get_channel_layer()
async_to_sync(channel_layer.group_send)(
"tweets",
{
"type": "tweet_message",
"tweet": {
"id": tweet.id,
"content": tweet.content,
"username": tweet.user.username,
"created_at": str(tweet.created_at),
}
}
)
4. URLを作成
# backend/tweets/urls.py
from rest_framework.routers import DefaultRouter
from.views import TweetViewSet, BookmarkAPIView
from django.urls import path
router = DefaultRouter()
router.register(r'tweets', TweetViewSet, basename='tweets')
urlpatterns = router.urls
urlpatterns += [
path("tweets/<int:tweet_id>/bookmark/", BookmarkAPIView.as_view(), name="bookmark_tweet"),
path("bookmarks/", BookmarkAPIView.as_view(), name="list_bookmarks"),
path("tweets/<int:tweet_id>/unbookmark/", BookmarkAPIView.as_view(), name="unbookmark_tweet"),
]
5. tweetserializers.pyでis_bookmarkの作成
# backend/tweets/serializers.py
from rest_framework import serializers
from.models import Tweet,Bookmark
from rest_framework.response import Response
class TweetSerializer(serializers.ModelSerializer):
username = serializers.ReadOnlyField(source='user.username')
user_has_liked = serializers.SerializerMethodField()
likes_count = serializers.SerializerMethodField()
isBookmarked = serializers.SerializerMethodField()
class Meta:
model = Tweet
fields = ["id", "username", "content", "created_at","user_has_liked", "likes_count", "isBookmarked"]
read_only_fields = ["id", "username", "created_at"]
def list(self, request, *args, **kwargs):
queryset = self.get_queryset()
serializer = self.get_serializer(queryset, many=True, context={'request': request})
return Response(serializer.data)
def get_user_has_liked(self, obj):
request = self.context.get("request", None)
if request and request.user.is_authenticated:
return obj.likes.filter(user=request.user).exists()
return False
def get_likes_count(self, obj):
return obj.likes.count()
def get_isBookmarked(self, obj):
request = self.context.get('request', None)
if request and request.user.is_authenticated:
return Bookmark.objects.filter(user=request.user, tweet=obj).exists()
return False
6. Bookmarkコンポーネントの作成
// frontend/src/components/BookmarkToggle.jsx
import React, { useState, useEffect } from "react";
import API from "../api";
const BookmarkToggle = ({ tweetId, initialBookmarked, onToggle }) => {
const [bookmarked, setBookmarked] = useState(initialBookmarked);
useEffect(() => {
setBookmarked(initialBookmarked);
}, [initialBookmarked]);
const handleBookmark = async () => {
try {
const response = await API.post(`tweets/${tweetId}/bookmark/`);
console.log("Bookmark Response:", response.data)
setBookmarked(true);
onToggle(true);
} catch (error) {
console.error("Error:", error);
}
};
const handleUnBookmark = async () => {
try {
const response = await API.delete(`tweets/${tweetId}/unbookmark/`);
console.log("UnBookmark Response:", response.data);
setBookmarked(false);
onToggle(false);
} catch (error) {
console.error("Error unbookmark", error);
}
};
return (
<div>
{bookmarked ? (
<button onClick={handleUnBookmark} className="text-red-500">
UnBookmark
</button>
) : (
<button onClick={handleBookmark} className="text-blue-500">
Bookmark
</button>
)}
</div>
);
};
export default BookmarkToggle;
7. TweetItem にBookmarkToggleを追加
// frontend/src/components/TweetItem.jsx
// src/components/TweetItem.jsx
import React, { useState } from "react";
import CommentList from "./CommentList";
import LikeToggle from "./LikeToggle";
import { Link } from "react-router-dom";
import BookmarkToggle from "./BookmarkToggle";
const TweetItem = ({ tweet, onDelete, onBookmarkToggle }) => {
const [showComments, setShowComments] = useState(false);
const handleDeleteTweet = () => {
if (!window.confirm("Delete this tweet?")) return;
// 親から受け取った onDelete があれば呼ぶ
if (onDelete) {
onDelete(tweet.id);
}
};
return (
<div className="border p-4 mb-4">
<p className="font-bold">
<Link to={`/users/${tweet.username}/profile`} className="text-blue-500 hover:underline">
{tweet.username}
</Link>
</p>
<p>{tweet.content}</p>
<small className="text-gray-500">
{new Date(tweet.created_at).toLocaleString()}
</small>
<div className="mt-2 flex items-center gap-4">
<LikeToggle
tweetId={tweet.id}
initialLiked={tweet.user_has_liked}
initialLikeCount={tweet.likes_count}
/>
<BookmarkToggle
tweetId={tweet.id}
initialBookmarked={tweet.isBookmarked}
onToggle={(newStatus) => onBookmarkToggle(tweet.id, newStatus)}
/>
<button
onClick={() => setShowComments(!showComments)}
className="text-blue-500 mr-2"
>
{showComments ? "Hide Comments" : "Show Comments"}
</button>
<button
onClick={handleDeleteTweet}
className="text-red-500"
>
Delete Tweet
</button>
</div>
{showComments && (
<CommentList tweetId={tweet.id} />
)}
</div>
);
};
export default TweetItem;
8. TweetList.jsxでBookmarkの状態を管理
// frontend/src/pages/TweetList.jsx
import React, { useContext, useState, useEffect } from "react";
import { AuthContext } from "../contexts/AuthContext";
import API from "../api";
import TweetForm from "../components/TweetForm";
import TweetItem from "../components/TweetItem";
function TweetList() {
const { accessToken } = useContext(AuthContext);
const [tweets, setTweets] = useState([]);
const [error, setError] = useState(null);
const [loading, setLoading] = useState(true);
console.log("Access token:", accessToken);
// ツイート一覧を取得
const fetchTweets = async () => {
setLoading(true);
setError(null);
try {
const res = await API.get("tweets/");
setTweets(res.data);
} catch (err) {
console.error("Failed to fetch tweets", err);
setError("Failed to load tweets.Please try again.")
}
setLoading(false);
};
useEffect(() => {
fetchTweets();
}, []);
const handleNewTweet = (newTweet) => {
setTweets([newTweet, ...tweets]);
};
// ツイート削除時のハンドラー
const handleDeleteTweet = (deletedTweetId) => {
setTweets(tweets.filter(tweet => tweet.id !== deletedTweetId))
};
const handleBookmarkToggle = (tweetId, newStatus) => {
setTweets((prevTweets) =>
prevTweets.map((tweet) =>
tweet.id === tweetId ? { ...tweet, isBookmarked: newStatus } : tweet
)
);
};
return (
<div className="container mx-auto mt-4 px-4">
<h2 className="text-2xl font-bold mb-4">Tweets</h2>
{accessToken && (
<div className="mb-6">
<TweetForm onTweetCreated={handleNewTweet} />
</div>
)}
{loading && <p>Loading tweets...</p>}
{error && <div className="bg-red-100 text-red-700 p-2 mb-4 rounded">{error}</div>}
{!loading && tweets.length === 0 && <p>No tweets available.</p>}
<div className="space-y-4">
{tweets.map((tweet) => (
<TweetItem key={tweet.id} tweet={tweet} onDelete={handleDeleteTweet} onBookmarkToggle={handleBookmarkToggle} />
))}
</div>
</div>
);
}
export default TweetList;
9. Bookmarks.jsxを作成して一覧ページの作成
import React, { useState, useEffect } from "react";
import API from "../api";
import TweetItem from "../components/TweetItem";
const Bookmarks = () => {
const [bookmarkedTweets, setBookmarkedTweets] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
// ブックマーク一覧を取得
const fetchBookmarkedTweets = async () => {
setLoading(true);
setError(null);
try {
const res = await API.get("bookmarks/");
setBookmarkedTweets(res.data);
} catch (err) {
console.error("Failed fetch bookmarks", err);
setError("Failed fetch bookmarks");
}
setLoading(false);
};
useEffect(() => {
fetchBookmarkedTweets();
}, []);
return (
<div className="container mx-auto mt-4 px-4">
<h2 className="text-2xl font-bold mb-4">Bookmarks</h2>
{loading && <p>Loading bookmarks...</p>}
{error && <div className="bg-red-100 text-red-700 p-2 mb-4 rounded">{error}</div>}
{!loading && bookmarkedTweets.length === 0 && <p>Bookmarks Not yet.</p>}
<div className="space-y-4">
{bookmarkedTweets.map((tweet) => (
<TweetItem key={tweet.id} tweet={tweet} />
))}
</div>
</div>
);
};
export default Bookmarks;
10. App.jsに追加
2. フォロー機能の実装
1. モデルの作成
# backend/users/models.py
from django.contrib.auth.models import AbstractUser
from django.db import models
from django.contrib.auth import get_user_model
User = get_user_model
class User(AbstractUser):
# DjangoのAbstractUserを継承したカスタムUserモデル。
# AbstractUserは username, email, first_name, last_name, password など
# すでに多くのフィールドが定義されている。
# 追加でプロフィール画像や自己紹介欄などを1つのモデルにまとめてもOK
# 例:
# profile_image = models.ImageField(upload_to='profiles/', null=True, blank=True)
# bio = models.TextField(blank=True)
# ここに追加フィールドを書ける
pass
def __str__(self):
return self.username
class Follow(models.Model):
follower = models.ForeignKey(User, on_delete=models.CASCADE, related_name="following")
following = models.ForeignKey(User, on_delete=models.CASCADE, related_name="followers")
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
unique_together = ("follower", "following")
def __str__(self):
return f"{self.follower.username} follows {self.following.username}"
2. マイグレーションの実行
3. APIの作成
# backend/users/views.py
from rest_framework import generics, permissions
from.models import User, Follow
from.serializers import UserCreateSerializer
from rest_framework.views import APIView
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework import status
# Create your views here.
class UserCreateView(generics.CreateAPIView):
# 新規ユーザーを作成するだけのAPI
queryset = User.objects.all()
serializer_class = UserCreateSerializer
permission_classes = [permissions.AllowAny]
class FollowAPIView(APIView):
permission_classes = [IsAuthenticated]
def post(self, request, username):
follower = request.user
try:
following = User.objects.get(username=username)
except User.DoesNotExist:
return Response({"error": "User not found"}, status=status.HTTP_404_NOT_FOUND)
if follower == following:
return Response({"error": "You cannot follow yourself"}, status=status.HTTP_400_BAD_REQUEST)
follow_relation, created = Follow.objects.get_or_create(follower=follower, following=following)
if created:
return Response({"message": "Followed"}, status=status.HTTP_201_CREATED)
return Response({"message": "Already following"}, status=status.HTTP_200_OK)
def delete(self, request, username):
follower = request.user
try:
follow_relation = Follow.objects.get(follower=follower, following__username=username)
follow_relation.delete()
return Response({"message": "Unfollowed"}, status=status.HTTP_200_OK)
except Follow.DoesNotExist:
return Response({"error": "You are not following this user"}, status=status.HTTP_400_BAD_REQUEST)
class FollowersListView(APIView):
permission_classes = [IsAuthenticated]
def get(self, request, username):
try:
user = User.objects.get(username=username)
except User.DoesNotExist:
return Response({"Error": "User not found."}, status=status.HTTP_404_NOT_FOUND)
followers = user.followers.all()
follower_list = [{"id": f.follower.id, "username": f.follower.username} for f in followers]
return Response(follower_list, status=status.HTTP_200_OK)
class FollowingListView(APIView):
permission_classes = [IsAuthenticated]
def get(self, request, username):
try:
user = User.objects.get(username=username)
except User.DoesNotExist:
return Response({"Error": "User not found."}, status=status.HTTP_404_NOT_FOUND)
following = user.following.all()
following_list = [{"id": f.following.id, "username": f.following.username} for f in following]
return Response(following_list, status=status.HTTP_200_OK)
4. URLの作成
# backend/users/urls.py
from django.urls import path
from.views import UserCreateView,FollowAPIView,FollowersListView,FollowingListView
urlpatterns = [
path('register/', UserCreateView.as_view(), name="user_register"),
path("users/<str:username>/follow/", FollowAPIView.as_view(), name="follow_user"),
path("users/<str:username>/unfollow/", FollowAPIView.as_view(), name="unfollow_user"),
path("users/<str:username>followers/", FollowersListView.as_view(), name="list_followers"),
path("users/<str:username>following/", FollowingListView.as_view(), name="list_following"),
]
5. FollowButtonコンポーネントの作成
// frontend/src/components/FollowButton.jsx
import React, { useState, useEffect } from "react";
import API from "../api";
const FollowButton = ({username, isFollowing, onToggle }) => {
const [following, setFollowing] = useState(isFollowing);
useEffect(() => {
setFollowing(isFollowing);
}, [isFollowing]);
const handleFollow = async () => {
try {
const response = await API.post(`users/${username}/follow/`);
console.log("Follow Response:", response.data)
setFollowing(true);
onToggle(true);
} catch (error) {
console.error("Error:", error);
}
};
const handleUnFollow = async () => {
try {
const response = await API.delete(`users/${username}/unfollow/`);
console.log("UnFollow Response:", response.data);
setFollowing(false);
onToggle(false);
} catch (error) {
console.error("Error unFollow", error);
}
};
return (
<button
onClick={following ? handleUnFollow : handleFollow}
className={`px-4 py-2 text-sm font-semibold rounded-full transition-all duration-200 ease-in-out
${following
? "bg-gray-300 text-gray-700 hover:bg-gray-400"
: "bg-blue-500 text-white hover:bg-blue-600 shadow-md"}`}
>
{following ? "Following" : "Follow"}
</button>
);
};
export default FollowButton;
6. UserItem.jsxに埋め込む
// frontend/src/components/UserItem.jsx
import React from "react";
import FollowButton from "./FollowButton";
const UserItem = ({ user }) => {
return (
<div className="flex items-center justify-between p-4 mb-2 bg-white rounded shadow-sm hover:shadow-md transition-shadow duration-200">
<div className="flex items-center space-x-3">
<span className="font-medium">{user.username}</span>
</div>
<FollowButton
userId={user.id}
isFollowing={user.isFollowing}
onToggle={() => {}}
/>
</div>
);
};
export default UserItem;
7. FollowersList.jsxの作成
// frontend/src/components/FollowersList.jsx
import React, { useState, useEffect } from "react";
import API from "../api";
import UserItem from "../components/UserItem";
const FollowersList = ({userId}) => {
const [followers, setFollowers] = useState([]);
useEffect(()=> {
const fetchFollowers = async () => {
try {
const response = await API.get(`users/${userId}/followers/`);
setFollowers(response.data);
} catch (error) {
console.error("Error fetching followers:", error);
}
};
fetchFollowers();
}, [userId]);
return (
<div className="max-w-md mx-auto mt-8">
<h2 className="text-xl font-bold mb-4">Followers</h2>
{followers.length > 0 ? (
followers.map((follower) => (
<UserItem key={follower.id} user={follower} />
))
): (
<p className="text-gray-600">No followers yet</p>
)}
</div>
);
};
export default FollowersList;
8.FollowingList.jsxの作成
// frontend/src/pages/FollowingList.jsx
import React, { useState, useEffect} from "react";
import API from "../api";
import UserItem from "../components/UserItem";
const FollowingList = ({ userId }) => {
const [followingList, setFollowingList] = useState([]);
useEffect(() => {
const fetchFollowing = async () => {
try {
const response = await API.get(`users/${userId}/following/`);
setFollowingList(response.data);
} catch (error) {
console.error("Error fetching following:", error);
}
};
fetchFollowing();
}, [userId]);
return (
<div className="max-w-md mx-auto mt-8">
<h2 className="text-xl font-bold mb-4">Following</h2>
{followingList.length > 0 ? (
followingList.map((user) => (
<UserItem key={user.id} user={user} />
))
) : (
<p className="text-gray-600">Not following anyone yet</p>
)}
</div>
);
};
export default FollowingList;
9. App.jsに埋め込む
10. UserProfile.jsxに埋め込む
// frontend/src/pages/UserProfile.jsx
import React, { useState, useEffect, useCallback } from "react";
import { useParams } from "react-router-dom";
import { Link } from "react-router-dom";
import FollowButton from "../components/FollowButton";
import API, { REACT_APP_HOST_URL} from "../api";
import FollowersList from "./FollowersList";
import FollowingList from "./FollowingList";
function UserProfile() {
const { username } = useParams();
const [profile, setProfile] = useState({});
const [error, setError] = useState("");
const [loading, setLoading] = useState(true);
const [isFollowing, setIsFollowing] = useState(false);
const [activeTab, setActiveTab] = useState("profile");
const fetchUserProfile = useCallback(async () => {
try {
setLoading(true);
setError("");
const res = await API.get(`users/${username}/profile/`);
console.log("🚀 API Response:", res.data);
setProfile(res.data);
setIsFollowing(res.data.is_following);
// res.dataの例: { "id": 1, "bio": "...", "profile_image": "/media/..." }
} catch (err) {
console.error("Failed to load user profile:", err);
setError("Failed to load user profile.");
} finally {
setLoading(false);
}
}, [username]);
useEffect(() => {
if (!username) return;
fetchUserProfile();
}, [username, fetchUserProfile]);
const handleFollowToggle = () => {
setIsFollowing(!isFollowing);
};
if (loading) return <p>Loading user profile...</p>;
if (error) return <p className="text-red-500">{error}</p>;
if (!profile || !profile.id) return <p>No profile found.</p>;
console.log("profile:", profile);
console.log("profile.id:", profile.id);
const imageUrl = profile?.profile_image
? profile.profile_image.startsWith("http")
? profile.profile_image
: `${REACT_APP_HOST_URL}${profile.profile_image}`
: `${REACT_APP_HOST_URL}static/images/default_profile.png`;
// キャッシュバスター(例: 現在のタイムスタンプ)を付与
const finalImageUrl = `${imageUrl}?v=${new Date().getTime()}`;
// ここで profile.user.username があるかどうかは
// バックエンドのProfileSerializerによりけり
return (
<div className="max-w-md mx-auto p-4 bg-white shadow mt-4">
<h2 className="text-2xl mb-4">{username}'s Profile</h2>
{/* ProfileタブかFollowersタブかFollowingタブを切り替えるボタンを配置 */}
<div className="flex space-x-4 mb-4">
<button
onClick={() => setActiveTab("profile")}
className={`px-3 py-1 rounded
${activeTab === "profile" ? "bg-blue-500 text-white" : "bg-gray-200 text-gray-700"}`}
>
Profile
</button>
<button
onClick={() => setActiveTab("followers")}
className={`px-3 py-1 rounded
${activeTab === "followers" ? "bg-blue-500 text-white" : "bg-gray-200 text-gray-700"}`}
>
Followers
</button>
<button
onClick={() => setActiveTab("following")}
className={`px-3 py-1 rounded
${activeTab === "following" ? "bg-blue-500 text-white" : "bg-gray-200 text-gray-700"}`}
>
Following
</button>
</div>
{/* タブごとの表示内容を切り替え */}
{activeTab === "profile" && (
<div className="flex flex-col items-center">
<img
src={finalImageUrl}
alt="Profile"
className="w-24 h-24 rounded-full object-cover mb-2"
/>
<p className="text-gray-700">{profile.bio}</p>
{profile?.id && (
<div>
<Link to={`/dm/${profile.username}`} className="text-blue-500 hover:underline mb-4 inline-block">
Send DM
</Link>
<FollowButton
userId={profile.id}
username={profile.username}
isFollowing={isFollowing}
onToggle={handleFollowToggle}
/>
</div>
)}
</div>
)}
{activeTab === "followers" && (
<div>
<FollowersList userId={profile.id} />
</div>
)}
{activeTab === "following" && (
<div>
<FollowingList userId={profile.id} />
</div>
)}
</div>
);
}
export default UserProfile;
10. MyProfile.jsxに埋め込む
// frontend/src/pages/MyProfile.jsx
import React, { useContext, useState, useEffect } from "react";
import { AuthContext } from "../contexts/AuthContext";
import API, { REACT_APP_HOST_URL, } from "../api";
import FollowersList from "./FollowersList";
import FollowingList from "./FollowingList";
function Profile() {
const { user } = useContext(AuthContext); // ログインユーザー情報
const [bio, setBio] = useState("");
const [profileImage, setProfileImage] = useState(null);
const [error, setError] = useState(null);
const [success, setSuccess] = useState(null);
const [loading, setLoading] = useState(true);
const [activeTab, setActiveTab] = useState("profile");
useEffect(() => {
fetchMyProfile();
}, []);
const fetchMyProfile = async () => {
try {
setLoading(true);
setError(null);
const res = await API.get("profile/me/");
// res.data: { "id": ..., "bio": "...", "profile_image": "/media/..."}
setBio(res.data.bio || "");
// プロフィール画像表示用に setProfileImage には「ファイル」ではなく初期値不要
// 既存の画像URLはres.data.profile_image
} catch (err) {
console.error("Failed to fetch my profile:", err);
setError("Failed to load profile.");
} finally {
setLoading(false);
}
};
const handleSubmit = async (e) => {
e.preventDefault();
setError(null);
setSuccess(null);
try {
const formData = new FormData();
formData.append("bio", bio);
if (profileImage) {
formData.append("profile_image", profileImage);
}
const res = await API.patch("profile/me/", formData, {
headers: {
"Content-Type": "multipart/form-data",
},
});
console.log("Profile update response:", res.data);
setSuccess("Profile updated successfully!");
} catch (err) {
console.error("Profile update failed", err);
setError("Failed to update profile. Please try again.");
}
};
const handleImageChange = (e) => {
if (e.target.files && e.target.files[0]) {
setProfileImage(e.target.files[0]);
}
};
if (loading) return <p>Loading profile...</p>;
if (error) return <p className="text-red-500">{error}</p>;
const imageUrl = user?.profile_image
? user.profile_image.startsWith("http")
? user.profile_image
: `${REACT_APP_HOST_URL}${user.profile_image}`
: `${REACT_APP_HOST_URL}static/images/default_profile.png`;
// キャッシュバスター(例: 現在のタイムスタンプ)を付与
const finalImageUrl = `${imageUrl}?v=${new Date().getTime()}`;
return (
<div className="flex justify-center items-center min-h-screen bg-gray-100">
<div className="bg-white p-6 rounded shadow-md w-full max-w-md h-[600px] overflow-y-auto">
<h2 className="text-2xl font-bold mb-4 text-center">My Profile</h2>
{success && <div className="bg-green-100 text-green-700 p-2 mb-4 rounded">{success}</div>}
{/* タブボタン */}
<div className="flex space-x-4 mb-4">
<button
onClick={() => setActiveTab("profile")}
className={activeTab === "profile" ? "bg-blue-500 text-white px-3 py-1 rounded" : "bg-gray-200 text-gray-700 px-3 py-1 rounded"}
>
Profile
</button>
<button
onClick={() => setActiveTab("followers")}
className={activeTab === "followers" ? "bg-blue-500 text-white px-3 py-1 rounded" : "bg-gray-200 text-gray-700 px-3 py-1 rounded"}
>
Followers
</button>
<button
onClick={() => setActiveTab("following")}
className={activeTab === "following" ? "bg-blue-500 text-white px-3 py-1 rounded" : "bg-gray-200 text-gray-700 px-3 py-1 rounded"}
>
Following
</button>
</div>
{/* タブの中身 */}
{activeTab === "profile" && (
<form onSubmit={handleSubmit}>
<div className="mb-4 flex flex-col items-center">
<img
src={finalImageUrl}
alt="Profile"
className="w-24 h-24 rounded-full object-cover mb-2"
/>
<input type="file" accept="image/*" onChange={handleImageChange} />
</div>
<div className="mb-4">
<label className="block text-gray-700">Bio</label>
<textarea
className="w-full px-3 py-2 border rounded"
value={bio}
onChange={(e) => setBio(e.target.value)}
placeholder="Tell us about yourself"
rows="3"
/>
</div>
<button
type="submit"
className="w-full bg-blue-500 text-white py-2 rounded hover:bg-blue-600 transition duration-200"
>
Update Profile
</button>
</form>
)}
{activeTab === "followers" && (
<div>
<FollowersList userId={user.id} />
</div>
)}
{activeTab === "following" && (
<div>
<FollowingList userId={user.id} />
</div>
)}
</div>
</div>
);
}
export default Profile;