0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ツイートアプリを作成しながら学習 1-6【Django + React】

Posted at

仕上がり

image.png

image.png

目次

1. ブックマーク機能の実装

2. フォロー機能の実装

1. ブックマーク機能の実装

:computer: 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}"

:computer: 2. マイグレーションの実行

:computer: 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),
                }
            }
        )

:computer: 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"),
]

:computer: 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

:computer: 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;

:computer: 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;

:computer: 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;

:computer: 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;

:computer: 10. App.jsに追加

2. フォロー機能の実装

:computer: 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}"

:computer: 2. マイグレーションの実行

:computer: 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)

:computer: 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"),
]

:computer: 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;

:computer: 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;

:computer: 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;

:computer: 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;

:computer: 9. App.jsに埋め込む

:computer: 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;

:computer: 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;

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?