マッチングアプリ開発

簡単なマッチングアプリDjangoとReactを使用して開発したので記録を残します。(※解説は未完成ですが、順次追加して更新していきます)以下にGitHubのリンクを貼っています。

github.com

今回アプリを開発するにあたって以下の記事を大いに参考にさせていただいたため、ここで感謝をお伝えすると同時に、まずは以下の記事を読んでいただけると助かります。

happy-daikicchi.com

WEBアプリ開発の全体像

WEBアプリ開発と聞くと非常に難しそうなイメージがありますが、意外とシンプルです。僕たちが見ているPCの画面はデータがペタペタ貼られているだけです。どこに、どんなデータを貼るかを決めて実装するのがWEBアプリ開発です。もう少し具体的に説明すると、どのURLに対してどの画面(HTML)を対応させるかを決めて、データベースから欲しい情報を持ってきて画面に貼り付けたり、フォームに入力されたデータをデータベースに追加したりしているのです。

コードレベルまで具体化して説明すると、バックエンドでは、データベースの設計とAPIのエンドポイントを定義します。データベースとは、ユーザー情報(メールアドレス、ユーザー名など)を格納するものや、プロフィール(年齢、趣味など)を格納するもの、ダイレクトメッセージの情報(誰が、誰に、どんなメッセージを、いつ送っているのか)を格納するものなどがあって、これらの情報が定義された箱のようなものです。
APIエンドポイントというのは、http://127.0.0.1:8000/api/user/createのようなものです。例えば、ユーザーの新規登録を行いたいときは、ユーザー情報と登録をしたいということと共にhttp://127.0.0.1:8000/api/user/createにアクセスしたら、ユーザー情報がデータベースに登録されます。このように、画面の操作とデータベースの懸け橋となってくれています。

フロントエンドは、このAPIエンドポイントを駆使して、どの画面にどの情報をどのように表示させるかを決めたり、どのようなフォームを作ってデータベースに情報を追加するか決めたりしています。

環境構築

仮想環境とは...?

仮想環境というのは、一つのコンピューターに別のコンピューターを作ることです。
仮想環境を作る理由は主に3つあります。

  1. 各プロジェクトが必要とするライバラりのバージョンを個別に管理できるから
    PCに直接ライブラリーをインストールしてバージョン管理をすると、あるプロジェクトではバージョン1.5のままでなければいけないのに、あるプロジェクトではバージョン1.6にアップデートしなくてはいけないといった競合が生じたときに対応が難しくなります。そこで、各プロジェクトごとに仮想環境を作ってバージョンを管理することが必要となってきます。
  2. 開発環境と本番環境を同じにしたいから
    開発環境と本番環境が同じであるとは限りません。そのため、様々な本番環境を仮想的に自分のPCに構築して、正しく動作するかを確認する必要があります。
  3. 開発環境の共有が楽だから
    共同で開発をする際に、自分の環境と全く同じ環境を共同開発仲間に設定してもらうのはめんどくさいです。仮想環境を作って仮想環境を共有するば簡単に同じ環境で共同開発ができます。

仮想環境の種類

詳細の説明は省略します。(今後追加するかも...)

仮想マシン

コンピューター上のコンピューターのイメージです。ハードウェアレベルの仮想化技術です。

コンテナ(docker)

OSレベルの仮想化技術です。

anacondaで仮想環境をつくる

ANACONDA.NAVIGATORのEnviromentsでCreateを押して、Nameを決めて、PackageはPythonを選択してください。

PyCharmと仮想環境の紐づけ

  1. プロジェクトルートとなるフォルダを作成します
    例:C:\Users\ユーザー名\projects\matching-app
  2. PyCharmをインストールして起動します
  3. Openを押して作成したプロジェクトルートのフォルダを選択します
  4. ANACONDAで作成した環境を紐づけます
    File>Setting>Project>Python Interpreter>Add Interpreter>Add Local Interpreter 作成した仮想環境を選択してください

これでひとまず環境構築は以上です。

バックエンド

それでは、さっそくバックエンドの開発をしていきます。バックエンドでは何をつくるか覚えているでしょうか。データベースとAPIエンドポイントです。このことを頭に置きながら開発を進めていきましょう。

プロジェクト作成とアプリケーションの追加

プロジェクト作成
  1. ルートディレクトリ配下にbackendfrontendというフォルダを作成
    PyCharmのターミナルを開いてbackendfrontendのフォルダを作成してください。

      mkdir backend
      mkdir frontend
    
  2. プロジェクトを作成
    djangoライブラリーをインストールして、プロジェクトを作成します。
    プロジェクトを作成するコマンドは、django-admin startproject <プロジェクト名> <作成するディレクトリ>です。<作成するディレクトリ>は省略可能ですが、.を指定することで、カレントディレクトリ直下にプロジェクトを展開してくれるようになります。

      pip install django
      django-admin startproject matchingappapi .
    
アプリケーションの追加

任意の名前(以下ではmyapp)でアプリを追加します

django-admin startappp myapp

サーバーの起動
  1. 左下のmanage.pyを右クリックして、run manageをクリック
  2. 右上のmanageからEdit Configurationsを選択
  3. parameterrunserverと記述
  4. 右上の再生ボタン(▶)を押すと、サーバーが起動してアプリが立ち上がる。

setting.pyの編集

環境変数の設定

環境変数はPCが上手く(設定通り)動くために必要な変数のことです。例えば、LANGという環境変数は使用する言語を設定しています。
setting.pyにはSECRET_KEYという環境変数が記述されています。SECRET_KEYはユーザーが登録したパスワードを暗号化したりするなどセキュリティを担保するうえで用いられているため、この値が知られると、情報漏洩のリスクが急激に上がってしまいます。
そこで、SECRET_KEYはハードコーディングせずに、.envファイルに記述して、setting.pyでは.envファイルから呼び出して記述します。.env.gitiginoreに記述することで、GitHubソースコードをアップロードしてもSECRET_KEYは公開されないという状態を作り出せます。(※.gitignoreに記述されたファイルはGitの管理対象外となります)

(余談:暗号化とハッシュ化の違い→前者は可逆で元データを復元できる、後者は不可逆。)

backendのフォルダ直下に.env.gitignoreファイルを作成してください。

SECRET_KEY="ここにデフォルトでハードコーディングされていたSECRET_KEYを貼り付けてください"

.env以外にもGitHubにのせたくないファイルは.gitignoreに記述します。

.env
db.sqlite3
myapp/__pycache__/
myapp/migrations/__pycache__/
matchingappapi/__pycache__/

環境変数を扱うライブラリ(django-environ)をインストールします

pip install django-environ
env = environ.Env()
env.read_env(os.path.join(BASE_DIR, '.env'))
SECRET_KEY = env('SECRET_KEY')
デフォルトの定数の説明と変更
  1. BASE_DIR
    基準となるディレクトリを定義します。ここでは、setting.pyディレクトリの親の親のディレクトリ、つまり、manage.pyと同じ階層のディレクトリが指定されています。

      BASE_DIR = Path(__file__).resolve().parent.parent
    
  2. DEBUG
    DEBUGTrueにすると、エラーが発生したときにエラーメッセージが表示されるために、開発環境ではTrueにします。本番環境でもTrueにすると、エラーメッセージから情報漏洩する可能性があるため、本番環境においてはFalseにします。

      DEBUG = True
    
  3. ALLOWED_HOSTS
    Webサービスを配信するサーバーのドメイン名、IPアドレス、ホスト名をリストで指定します。
    DEBUG=TrueかつALLOWED_HOSTS=[]の場合は、ALLOWED_HOSTS=['localhost, '127.0.0.1', '[::1]']が自動的に有効になるため、空のリストのままのせて位で大丈夫です。
    (余談:localhostドメイン名であって、自分自身を指すIPアドレス127.0.0.1(IPv4の場合)、[:11](IPv6の場合)を指す。)

      ALLOWED_HOSTS=[]
    
  4. INSTALLED_APPS
    インストールしたり、作成たりしたアプリを追加。

     INSTALLED_APPS = [
          'myapp' #←これを追加
      ]
    
  5. MIDDLEWARE
    ミドルウェアの登録。そのままで。 (余談:ミドルウェアとは、)

  6. ROOT_URLCONF

  7. TEMPLATES

  8. WSGI_APPLICATION

  9. DATABASES

  10. AUTH_PASSWORD_VALIDATORS

  11. LANGUAGE_CODE
    日本語に変更。

      LANGUAGE_CODE = "ja"
    
  12. TIME_ZONE
    東京に変更。

      TIME_ZONE = "Asia/Tokyo"
    
  13. USE_I18N

  14. USE_TZ

他にも変更する定数や、新たに追加する定数はありますが、今追加しても何のために必要なのかイメージしずらいと思いますので、必要となった時に適宜追加していきます。

models.pyの編集

モデルとは何か
UserManagerの設定
モデルの設定
コード
from django.contrib.auth.models import (
    AbstractBaseUser,
    BaseUserManager,
    PermissionsMixin,
)
from django.db import models
from django.utils import timezone
from django.conf import settings
from django.core.validators import MinValueValidator, MaxValueValidator


# ユーザーの作成、取得、更新、削除などのDB操作を抽象化したクラス
class CustomUserManager(BaseUserManager):
    def create_user(self, email, password=None, **extra_fields):
        if not email:
            raise ValueError('The Email must be set')

        email = self.normalize_email(email)
        user = self.model(email=email, **extra_fields)
        user.set_password(password)
        user.save(using=self._db)

        return user

    def create_superuser(self, email, password=None, **extra_fields):
        extra_fields.setdefault('is_staff', True)
        extra_fields.setdefault('is_superuser', True)

        return self.create_user(email, password, **extra_fields)


# ユーザー情報のDBの設計
class CustomUser(AbstractBaseUser, PermissionsMixin):
    email = models.EmailField(unique=True)
    is_active = models.BooleanField(default=True)
    is_staff = models.BooleanField(default=False)
    date_joined = models.DateTimeField(default=timezone.now)
    objects = CustomUserManager()
    USERNAME_FIELD = "email"


# プロフィールのDB設計
class Profile(models.Model):
    user = models.OneToOneField(settings.AUTH_USER_MODEL, primary_key=True, on_delete=models.CASCADE,
                                related_name='profile')
    last_name = models.CharField("姓", default="", max_length=100)
    first_name = models.CharField("名", default="", max_length=100)
    is_kyc = models.BooleanField("本人確認", default=False)
    age = models.PositiveSmallIntegerField(
        "年齢",
        default=20,
        validators=[
            MinValueValidator(18, "18歳未満は登録できません"),
            MaxValueValidator(35, "36歳以上は登録できません")
        ]
    )
    SEX = [('male', '男性'), ('female', '女性')]
    sex = models.CharField("性別", max_length=16, choices=SEX, default="")
    hobby = models.TextField("趣味", max_length=1000)
    elementary_school = models.CharField("小学校", max_length=100, blank=True, null=True, default="")
    middle_school = models.CharField("中学校", max_length=100, blank=True, null=True, default="")
    high_school = models.CharField("高校", max_length=100, blank=True, null=True, default="")
    university = models.CharField("大学", max_length=100, blank=True, null=True, default="")


# 外出ボタンのDB設計
class GoOut(models.Model):
    user = models.OneToOneField(settings.AUTH_USER_MODEL, primary_key=True, on_delete=models.CASCADE, )
    go_out = models.BooleanField("外出ボタン", default=False)


# マッチング情報のDB設計
class Matching(models.Model):
    approaching = models.ForeignKey(
        settings.AUTH_USER_MODEL, related_name='approaching', default='',
        on_delete=models.CASCADE,
    )
    approached = models.ForeignKey(
        settings.AUTH_USER_MODEL, related_name='approached', default='',
        on_delete=models.CASCADE,
    )

    class Meta:
        unique_together = (('approaching', 'approached'),)


# message情報のDB設計
class DirectMessage(models.Model):
    sender = models.ForeignKey(
        settings.AUTH_USER_MODEL, related_name='sender',
        on_delete=models.CASCADE, default='',
    )
    receiver = models.ForeignKey(
        settings.AUTH_USER_MODEL, related_name='receiver',
        on_delete=models.CASCADE, default='',
    )
    message = models.CharField(verbose_name="メッセージ", max_length=200, default='',)
    created_at = models.DateTimeField("送信時間", auto_now_add=True)

admin.pyの編集

管理画面とは?
コード
from django.contrib import admin
from .models import CustomUser, Profile, GoOut ,Matching, DirectMessage


class CustomUserAdmin(admin.ModelAdmin):
    list_display = (
        "id",
        "email",
        "is_staff",
        "is_active",
        "date_joined",
    )


class ProfileAdmin(admin.ModelAdmin):
    list_display = ('user', 'last_name', 'first_name', 'is_kyc', 'age', 'sex', 'elementary_school',
                    'middle_school', 'high_school', 'university', )


class GoOutAdmin(admin.ModelAdmin):
    list_display = ('user', 'go_out',)


class MatchingAdmin(admin.ModelAdmin):
    list_display = ('approaching', 'approached',)


class DirectMessageAdmin(admin.ModelAdmin):
    list_display = ('sender', 'receiver', 'message', 'created_at')


admin.site.register(CustomUser, CustomUserAdmin)
admin.site.register(Profile, ProfileAdmin)
admin.site.register(GoOut, GoOutAdmin)
admin.site.register(Matching, MatchingAdmin)
admin.site.register(DirectMessage, DirectMessageAdmin)

urls.pyの編集

URLのルーティングを決めます

コード(matchingappapi/urls.py)
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path("admin/", admin.site.urls),
    path('api/', include('myapp.urls')),
    path('authen/', include('djoser.urls.jwt')),
]
コード(myapp/urls.py)
from rest_framework.routers import DefaultRouter
from django.urls import path, include
from .views import (CreateUserView,
                    UserView,
                    ProfileViewSet,
                    EditProfileView,
                    OtherProfileViewSet,
                    FavoriteProfileViewSet,
                    MyGoOutViewSet,
                    MyGoOutEditView,
                    TrueGoOutUserViewSet,
                    ApproachedMeViewSet,
                    MyApproachingViewSet,
                    ApproachingDeleteView,
                    DirectMessageViewSet,
                    InboxListView)

app_name = 'myapp'

router = DefaultRouter()
router.register('profile', ProfileViewSet)
router.register('other_profile', OtherProfileViewSet)
router.register('favorite_profile', FavoriteProfileViewSet)
router.register('goout', MyGoOutViewSet)
router.register('true_goout', TrueGoOutUserViewSet)
router.register('approached_me', ApproachedMeViewSet)
router.register('my_favorite', MyApproachingViewSet)
router.register('dm-message', DirectMessageViewSet)
router.register('dm-inbox', InboxListView)

urlpatterns = [
    path('users/create', CreateUserView.as_view(), name='users-create'),
    path('users/<pk>', UserView.as_view(), name='users'),
    path('users/edit_profile/<pk>', EditProfileView.as_view(), name='users-profile'),
    path('users/goout/<pk>', MyGoOutEditView.as_view(), name='edit-goout'),
    path('delete_favorite', ApproachingDeleteView.as_view(), name='delete-favorite'),
    path('', include(router.urls)),
]

serializers.pyの編集

コード
from rest_framework import serializers
from django.contrib.auth import get_user_model
from .models import Profile, GoOut, Matching, DirectMessage


class UserSerializer(serializers.ModelSerializer):
    class Meta:
        model = get_user_model()
        fields = ('email', 'password', 'id')
        extra_kwargs = {'password': {'write_only': True, 'min_length': 8}}

    def create(self, validated_data):
        user = get_user_model().objects.create_user(**validated_data)
        return user

    def update(self, use_instance, validated_data):
        for attr, value in validated_data.items():
            if attr == 'password':
                use_instance.set_password(value)
            else:
                setattr(use_instance, attr, value)
        use_instance.save()
        return use_instance


class ProfileSerializer(serializers.ModelSerializer):
    class Meta:
        model = Profile
        fields = (
            'user', 'last_name', 'first_name', 'is_kyc', 'age', 'sex', 'hobby', 'elementary_school', 'middle_school',
            'high_school', 'university',
        )
        extra_kwargs = {'user': {'read_only': True}}


class GoOutSerializer(serializers.ModelSerializer):
    class Meta:
        model = GoOut
        fields = ('user', 'go_out', )
        extra_kwargs = {'user': {'read_only': True}}


class MatchingSerializer(serializers.ModelSerializer):
    class Meta:
        model = Matching
        fields = ('id', 'approaching', 'approached',)
        extra_kwargs = {'approaching': {'read_only': True}}


class DirectMessageSerializer(serializers.ModelSerializer):
    class Meta:
        model = DirectMessage
        fields = ('id', 'sender', 'receiver', 'message','created_at', )
        extra_kwargs = {'sender': {'read_only': True}}

views.pyの編集

コード
from rest_framework.generics import CreateAPIView, RetrieveUpdateAPIView, DestroyAPIView
from rest_framework.permissions import AllowAny
from rest_framework.viewsets import ModelViewSet
from rest_framework.viewsets import ReadOnlyModelViewSet
from rest_framework.response import Response
from rest_framework import status
from django.db.models import Q
from .models import CustomUser, Profile, GoOut, Matching, DirectMessage
from .serializers import UserSerializer, ProfileSerializer, GoOutSerializer, MatchingSerializer, DirectMessageSerializer


# ユーザーの登録
class CreateUserView(CreateAPIView):
    serializer_class = UserSerializer
    permission_classes = (AllowAny,)


# 自分のユーザー情報(メールとパスワード)の取得と更新
class UserView(RetrieveUpdateAPIView):
    queryset = CustomUser.objects.all()
    serializer_class = UserSerializer

    def get_queryset(self):
        return self.queryset.filter(id=self.request.user.id)


# 自分のプロフィールの取得と作成(GoOutはデフォルトでFalseが登録される)
class ProfileViewSet(ModelViewSet):
    queryset = Profile.objects.all()
    serializer_class = ProfileSerializer

    def get_queryset(self):
        return self.queryset.filter(user=self.request.user)

    def perform_create(self, serializer):
        profile = serializer.save(user=self.request.user)
        go_out_defaults = {
            "user": profile.user,
            "go_out": False,
        }
        GoOut.objects.create(**go_out_defaults)
        return Response(serializer.data, status=status.HTTP_201_CREATED)


# 自分のプロフィールの取得と編集
class EditProfileView(RetrieveUpdateAPIView):
    queryset = Profile.objects.all()
    serializer_class = ProfileSerializer

    def get_queryset(self):
        return self.queryset.filter(user=self.request.user)


# 他の人のプロフィールの表示
class OtherProfileViewSet(ModelViewSet):
    queryset = Profile.objects.all()
    serializer_class = ProfileSerializer

    allowed_methods = ('GET',)

    def get_queryset(self):
        return self.queryset.exclude(pk=self.request.user.pk)


# LIKEした人のプロフィールの表示
class FavoriteProfileViewSet(ModelViewSet):
    queryset = Profile.objects.all()
    serializer_class = ProfileSerializer

    allowed_methods = ('GET',)

    def get_queryset(self):
        # リクエストパラメータからユーザーIDのリストを取得
        user_ids = self.request.query_params.getlist('user_ids')

        # ユーザーIDが存在しない場合は、空のリストを返す
        if not user_ids:
            return []

        # 指定されたユーザーIDのプロフィールのみを取得
        return self.queryset.filter(pk__in=user_ids)


# 自分のGoOutの状態と取得と更新
class MyGoOutViewSet(ModelViewSet):
    queryset = GoOut.objects.all()
    serializer_class = GoOutSerializer

    def get_queryset(self):
        return self.queryset.filter(user=self.request.user)


# 自分のGoOut状態の変更と取得
class MyGoOutEditView(RetrieveUpdateAPIView):
    queryset = GoOut.objects.all()
    serializer_class = GoOutSerializer

    def get_queryset(self):
        return self.queryset.filter(user=self.request.user)


# GoOutがTrueなユーザーの取得
class TrueGoOutUserViewSet(ModelViewSet):
    queryset = GoOut.objects.all()
    serializer_class = GoOutSerializer

    def get_queryset(self):
        return super().get_queryset().filter(go_out=True)


# 自分がアプローチされているリストの取得と作成
class ApproachedMeViewSet(ModelViewSet):
    queryset = Matching.objects.all()
    serializer_class = MatchingSerializer

    def get_queryset(self):
        return self.queryset.filter(approached=self.request.user)

    def perform_create(self, serializer):
        # POSTリクエストを送った時にapproachingフィールドに、requestユーザーを登録する
        serializer.save(approaching=self.request.user)


# 自分がアプローチしているリストの取得と作成
class MyApproachingViewSet(ModelViewSet):
    queryset = Matching.objects.all()
    serializer_class = MatchingSerializer

    def get_queryset(self):
        return self.queryset.filter(approaching=self.request.user)

    def perform_create(self, serializer):
        serializer.save(approaching=self.request.user)


class ApproachingDeleteView(DestroyAPIView):
    queryset = Matching.objects.all()
    serializer_class = MatchingSerializer

    def get_object(self):
        # リクエストパラメータから approached を取得
        approached = self.request.GET.get('approached')
        if not approached:
            return Response({'error': 'approached パラメータが必要です'}, status=400)

        # リクエストを送信しているユーザーを取得
        user = self.request.user

        # クエリセットをフィルタリング
        queryset = self.queryset.filter(
            Q(approaching=user.id) & Q(approached=approached)
        )

        # フィルタリング結果からオブジェクトを取得
        obj = queryset.get()

        return obj

    def get(self, request, *args, **kwargs):
        # リクエストパラメータから approached を取得
        approached = request.GET.get('approached')
        if not approached:
            return Response({'error': 'approached パラメータが必要です'}, status=400)

        # リクエストを送信しているユーザーを取得
        user = request.user

        # クエリセットをフィルタリング
        queryset = self.queryset.filter(
            Q(approaching=user.id) & Q(approached=approached)
        )

        # シリアライザでクエリセットをシリアル化
        serializer = self.serializer_class(queryset, many=True)

        # シリアル化されたデータを返す
        return Response(serializer.data)


class DirectMessageViewSet(ModelViewSet):
    queryset = DirectMessage.objects.all()
    serializer_class = DirectMessageSerializer

    def get_queryset(self):
        receiver_id = self.request.query_params.get('receiver_id')
        return self.queryset.filter(Q(sender=self.request.user) & Q(receiver=receiver_id))

    def perform_create(self, serializer):
        serializer.save(sender=self.request.user)


class InboxListView(ReadOnlyModelViewSet):
    queryset = DirectMessage.objects.all()
    serializer_class = DirectMessageSerializer

    def get_queryset(self):
        sender_id = self.request.query_params.get('sender_id')
        return self.queryset.filter(Q(receiver=self.request.user) & Q(sender=sender_id))

フロントエンド

React環境の構築

App.jsの編集

import React from 'react';
import {BrowserRouter as Router, Route, Routes} from 'react-router-dom';
import SignUp  from './components/SignUp';
import Login  from './components/Login';
import Home  from './components/Home';
import { CookiesProvider, withCookies} from 'react-cookie';
import CreateProfile from "./components/CreateProfile";
import EditProfile from "./components/EditProfile";
import Choice from "./components/Choice";
import Favorite from "./components/Favorite";
import DirectMessage from "./components/DirectMessage";
import Matching from "./components/Matching";


export const apiURL = 'http://127.0.0.1:8000';

const App = () => {
    return(
        <>
            <Router>
                <CookiesProvider>
                    <Routes>
                        <Route path="/" element={<SignUp />} />
                        <Route path="profile/create" element={<CreateProfile />} />
                        <Route path="profile/edit" element={<EditProfile />} />
                        <Route path="login" element={<Login />} />
                        <Route path="home" element={<Home />} />
                        <Route path="choice" element={<Choice />} />
                        <Route path="favorite" element={<Favorite />} />
                        <Route path="matching" element={<Matching />} />
                        <Route path="dm/:userId" element={<DirectMessage />} />
                    </Routes>
                </CookiesProvider>
            </Router>
        </>
    )
}

export default withCookies(App)

新規登録画面

import React, {useState}  from 'react';
import axios from 'axios';
import {Box, Button, Container, TextField, Typography} from '@mui/material'
import { apiURL } from '../App'
import {withCookies} from "react-cookie";

// ユーザーの新規登録
const SignUp = (props) => {
    const [ email, setEmail ] = useState("");
    const [ password, setPassword ] = useState("");

    const handleCreate = (event) => {
        event.preventDefault();
        const form_data = new FormData();

        form_data.append('email', email);
        form_data.append('password', password);

        axios.post(`${apiURL}/api/users/create`, form_data, {
            headers: {
                'Content-Type': 'application/json'
            },
        })
            .then( res => {
                console.log(res.data)
                const certificationUri = `${apiURL}/authen/jwt/create`;

                axios.post(certificationUri, form_data, {
                    headers: {
                        'Content-Type': 'application/json'
                    },
                })
                    .then( res => {
                        props.cookies.set('token', res.data.access)
                        console.log("success")
                        window.location.href="/profile/create";
                    })
                    .catch( () => {
                        console.log("certification error");
                    });
            })

            .catch( () => {
                console.log("error");
            });
    }

  return(
    <Container maxWidth="xs">
      <Box sx={{
          marginTop: 8,
          display: "flex",
          flexDirection: "column",
          alignItems: "center",
      }}
      >

        <Typography component="h2" variant="h5">
          新規登録
        </Typography>

        <Box component="form" noValidate sx={{mt:1}} onSubmit={handleCreate}>
          <TextField
              margin="normal"
              required
              fullWidth
              id="email"
              label="メールアドレス"
              name="email"
              autoComplete="email"
              autoFocus
              value={email}
              onChange={(e) => {setEmail(e.target.value)}}
          />

          <TextField
              margin="normal"
              required
              fullWidth
              name="password"
              label="パスワード"
              type="password"
              id="password"
              values={password}
              onChange={(e) => setPassword(e.target.value)}
          />

          <Button
              type="submit"
              fullWidth
              variant="contained"
              sx={{mt:3, mb:2}}
          >
              新規登録
          </Button>

          <Button variant="text" href="/login" sx={{ mt: 1 }}>
            ログインはこちら
          </Button>

        </Box>

      </Box>
    </Container>
   )

 }

export default withCookies(SignUp)

プロフィールの新規作成

import { withCookies } from "react-cookie";
import React, { useState } from "react";
import { apiURL } from "../App";
import axios from "axios";
import {Box, Button, Container, Grid, InputLabel, MenuItem, Paper, Select, TextField} from "@mui/material";

const CreateProfile = (props) => {
  // Profileの情報を格納
  const [last_name, setLast_name] = useState("");
  const [first_name, setFirst_name] = useState("");
  const [age, setAge] = useState("");
  const [sex, setSex] = useState("");
  const [hobby, setHobby] = useState("");
  const [elementary_school, setElementary_school] = useState("");
  const [middle_school, setMiddle_school] = useState("");
  const [high_school, setHigh_school] = useState("");
  const [university, setUniversity] = useState("");

  const handleCreate = (event) => {
    event.preventDefault();
    const form_data = new FormData();

    form_data.append("is_kyc", true);
    form_data.append("last_name", last_name);
    form_data.append("first_name", first_name);
    form_data.append("age", age);
    form_data.append("sex", sex);
    form_data.append("hobby", hobby);
    form_data.append("elementary_school", elementary_school);
    form_data.append("middle_school", middle_school);
    form_data.append("high_school", high_school);
    form_data.append("university", university);

    axios.post(`${apiURL}/api/profile/`, form_data, {
        headers: {
          Authorization: `JWT ${props.cookies.get("token")}`,
        },
      })
        .then((res) => {
          console.log(res.data);
          window.location.href = "/home";
      })
        .catch((error) => {
          console.log(error);
      });
  };

  return (
      <Container>
        <Box sx={{my:4}} component="h1">
          プロフィール作成
        </Box>

        <Paper sx={{p:10}}>
          <Box component="form" sx={{mt:1}} onSubmit={handleCreate}>
            <Grid>
              <TextField
                margin="normal"
                label="姓"
                id="last_name"
                name="last_name"
                value={last_name}
                onChange={(e) => setLast_name(e.target.value)}
                fullWidth
                required
              />
            </Grid>
            <Grid>
              <TextField
                  margin="normal"
                  label="名前"
                  id="first_name"
                  name="first_name"
                  value={first_name}
                  onChange={(e) => setFirst_name(e.target.value)}
                  fullWidth
                  required
              />
            </Grid>

            <Grid>
              <TextField
                  margin="normal"
                  label="年齢"
                  id="age"
                  name="age"
                  value={age}
                  onChange={(e) => setAge(e.target.value)}
                  fullWidth
                  required
              />
            </Grid>

            <InputLabel id="sex-label">性別</InputLabel>
            <Select
                labelId="sex-label"
                id="sex"
                name="sex"
                value={sex}
                onChange={(e) => setSex(e.target.value)}
                label="性別"
            >
              <MenuItem value="male">男性</MenuItem>
              <MenuItem value="female">女性</MenuItem>
            </Select>

            <Grid>
              <TextField
                  margin="normal"
                  label="趣味"
                  id="hobby"
                  name="hobby"
                  value={hobby}
                  onChange={(e) => setHobby(e.target.value)}
                  fullWidth
                  required
              />
            </Grid>

            <Grid>
              <TextField
                  margin="normal"
                  label="小学校"
                  id="elementary_school"
                  name="elementary_school"
                  value={elementary_school}
                  onChange={(e) => setElementary_school(e.target.value)}
                  fullWidth
                  required
              />
            </Grid>

            <Grid>
              <TextField
                  margin="normal"
                  label="中学校"
                  id="middle_school"
                  name="middle_school"
                  value={middle_school}
                  onChange={(e) => setMiddle_school(e.target.value)}
                  fullWidth
                  required
              />
            </Grid>

            <Grid>
              <TextField
                  margin="normal"
                  label="高校"
                  id="high_school"
                  name="high_school"
                  value={high_school}
                  onChange={(e) => setHigh_school(e.target.value)}
                  fullWidth
                  required
              />
            </Grid>

            <Grid>
              <TextField
                  margin="normal"
                  label="大学"
                  id="university"
                  name="university"
                  value={university}
                  onChange={(e) => setUniversity(e.target.value)}
                  fullWidth
                  required
              />
            </Grid>

            <Button
                type="submit"
                fullWidth
                variant="contained"
                sx={{mt:3, mb:2}}
            >
              作成
            </Button>
            <Button variant="text" href="/home" sx={{ mt: 1 }}>
              ホームに戻る
            </Button>
          </Box>
        </Paper>
    </Container>
  );
};

export default withCookies(CreateProfile);

ログイン画面

import React, { useState } from "react";
import axios from "axios";
import {
  Box,
  Button,
  Container,
  TextField,
  Typography,
} from "@mui/material";
import { apiURL } from "../App";
import { withCookies } from "react-cookie";

// ユーザーのログイン処理
const Login = (props) => {
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");

  const handleLogin = (event) => {
    event.preventDefault();
    let form_data = new FormData();

    form_data.append("email", email);
    form_data.append("password", password);

    axios.post(`${apiURL}/authen/jwt/create`, form_data, {
      headers: {
        "Content-Type": "application/json",
      },
    })
        .then((res) => {
          props.cookies.set("token", res.data.access);
          window.location.href = "/home";
        })
        .catch(() => {
          console.log("error");
        });
  };

  return (
    <Container maxWidth="xs">
      <Box sx={{ marginTop: 8, display: "flex", flexDirection: "column", alignItems: "center" }}>
        <Typography component="h2" variant="h5">
          ログイン
        </Typography>

        <Box component="form" noValidate sx={{ mt: 1 }} onSubmit={handleLogin}>
          <TextField
            margin="normal"
            required
            fullWidth
            id="email"
            label="メールアドレス"
            name="email"
            autoComplete="email"
            autoFocus
            value={email}
            onChange={(e) => setEmail(e.target.value)}
          />

          <TextField
            margin="normal"
            required
            fullWidth
            name="password"
            label="パスワード"
            type="password"
            id="password"
            values={password}
            onChange={(e) => setPassword(e.target.value)}
          />

          <Button type="submit" fullWidth variant="contained" sx={{ mt: 3, mb: 2 }}>
            ログイン
          </Button>

          <Button variant="text" href="/" sx={{ mt: 1 }}>
            新規登録はこちら
          </Button>
        </Box>
      </Box>
    </Container>
  );
};

export default withCookies(Login);

ホーム画面

import React, { useState, useEffect } from 'react';
import { withCookies } from 'react-cookie';
import axios from 'axios';
import { apiURL } from '../App';
import {Box, Button, Container, Grid, Link, Paper, Typography} from '@mui/material'

const Home = (props) => {
    const [myProfileList, setMyProfileList] = useState([]);
    const editProfileDirectory = "/profile/edit"

    // 自分のプロファイルの取得
    useEffect(() => {
        axios.get(`${apiURL}/api/profile`, {
            headers: {
                'Authorization': `JWT ${props.cookies.get('token')}`
                }
            })
            .then(res => {
                setMyProfileList(res.data);
            })
            .catch(error => {
                console.error(error);
            });
        }, [props.cookies]);

    return (
        <Container maxWidth="sm">
            <Box sx={{ my: 4 }}>
                <Typography variant="h4" component="h1" gutterBottom>
                    Home
                </Typography>
                <Paper sx={{ p: 2 }}>
                    {myProfileList.map((profile, index) => (
                        <Grid container key={index}>
                            <Grid item xs={12}>
                                <Typography variant="h6" component="h2">
                                    こんにちは {profile.last_name} {profile.first_name}さん
                                </Typography>
                            </Grid>
                            <Grid item xs={12} sx={{ mt: 2 }}>
                                <Link href={editProfileDirectory}>
                                    <Button variant="outlined">プロフィール編集</Button>
                                </Link>
                            </Grid>
                        </Grid>
                    ))}
                </Paper>
                <Box sx={{ mt: 4 }}>
                    <Grid container spacing={2}>
                        <Grid item xs={12} sm={6}>
                            <Link href="/choice">
                                <Button variant="contained" fullWidth>仲間探し</Button>
                            </Link>
                        </Grid>
                        <Grid item xs={12} sm={6}>
                            <Link href="/matching">
                                <Button variant="contained" fullWidth>マッチング成立</Button>
                            </Link>
                        </Grid>
                    </Grid>
                </Box>

                <Box sx={{ mt:4 }}>
                    <Grid>
                        <Link href="/login">
                            <Button variant="outlined" fullWidth>ログアウト</Button>
                        </Link>
                    </Grid>
                </Box>
            </Box>
        </Container>
    );
}

export default withCookies(Home);

プロフィールの編集

import { withCookies } from 'react-cookie';
import {
    Box,
    Button,
    Container,
    Grid,
    InputLabel,
    Link,
    MenuItem,
    Paper,
    Select,
    TextField,
    Typography
} from "@mui/material";
import React, {useEffect, useState} from "react";
import {apiURL} from "../App";
import axios from "axios";

const EditProfile = (props) => {
    // Profileの情報を格納
    const [user, setUser] = useState("");
    const [last_name, setLast_name] = useState("");
    const [first_name, setFirst_name] = useState("");
    const [age, setAge] = useState("");
    const [sex, setSex] = useState("");
    const [hobby, setHobby] = useState("");
    const [elementary_school, setElementary_school] = useState("");
    const [middle_school, setMiddle_school] = useState("");
    const [high_school, setHigh_school] = useState("");
    const [university, setUniversity] = useState("");

    const getProfile = () => {
        axios.get(`${apiURL}/api/profile`, {
                    headers: {
                        'Authorization': `JWT ${props.cookies.get('token')}`
                    }
                })
                    .then(res => {
                        setUser(res.data[0].user)
                        setLast_name(res.data[0].last_name)
                        setFirst_name(res.data[0].first_name)
                        setAge(res.data[0].age)
                        setSex(res.data[0].sex)
                        setHobby(res.data[0].hobby)
                        setElementary_school((res.data[0].elementary_school))
                        setMiddle_school((res.data[0].middle_school))
                        setHigh_school((res.data[0].high_school))
                        setUniversity((res.data[0].university))
                        console.log(res.data)
                    })
                    .catch(error => {
                        console.error(error);
                    });
    }

    useEffect(() => {
        getProfile()
    }, []);

    const handleEdit = (event) => {
        event.preventDefault();
        const form_data = new FormData();

        form_data.append("is_kyc", true);
        form_data.append("last_name", last_name);
        form_data.append("first_name", first_name);
        form_data.append("age", age);
        form_data.append("sex", sex);
        form_data.append("hobby", hobby);
        form_data.append("elementary_school", elementary_school);
        form_data.append("middle_school", middle_school);
        form_data.append("high_school", high_school);
        form_data.append("university", university);

        axios.patch(`${apiURL}/api/users/edit_profile/${user}`, form_data, {
            headers: {
                Authorization: `JWT ${props.cookies.get("token")}`,
            },
        })
            .then((res) => {
                console.log(res.data);
                window.location.href = "/home";
            })
            .catch((error) => {
                console.log(error);
            });
    }

    return(
        <Container maxWidth="md">
            <Box sx={{my:4}} component="h1">
                    プロフィール編集
            </Box>
            <Paper sx={{p:10}}>
                <Box component="form" sx={{mt:1}} onSubmit={handleEdit}>
                    <Grid>
                        <TextField
                            margin="normal"
                            label="姓"
                            id="last_name"
                            name="last_name"
                            value={last_name}
                            onChange={(e) => setLast_name(e.target.value)}
                            fullWidth
                            required
                        />
                    </Grid>

                    <Grid>
                        <TextField
                            margin="normal"
                            label="名前"
                            id="first_name"
                            name="first_name"
                            value={first_name}
                            onChange={(e) => setFirst_name(e.target.value)}
                            fullWidth
                            required
                        />
                    </Grid>

                    <Grid>
                        <TextField
                            margin="normal"
                            label="年齢"
                            id="age"
                            name="age"
                            value={age}
                            onChange={(e) => setAge(e.target.value)}
                            fullWidth
                            required
                        />
                    </Grid>

                    <InputLabel id="sex-label">性別</InputLabel>
                    <Select
                        labelId="sex-label"
                        id="sex"
                        name="sex"
                        value={sex}
                        onChange={(e) => setSex(e.target.value)}
                        label="性別"
                    >
                        <MenuItem value="male">男性</MenuItem>
                        <MenuItem value="female">女性</MenuItem>
                    </Select>

                    <Grid>
                        <TextField
                            margin="normal"
                            label="趣味"
                            id="hobby"
                            name="hobby"
                            value={hobby}
                            onChange={(e) => setHobby(e.target.value)}
                            fullWidth
                            required
                        />
                    </Grid>

                    <Grid>
                        <TextField
                            margin="normal"
                            label="小学校"
                            id="elementary_school"
                            name="elementary_school"
                            value={elementary_school}
                            onChange={(e) => setElementary_school(e.target.value)}
                            fullWidth
                            required
                        />
                    </Grid>

                    <Grid>
                        <TextField
                            margin="normal"
                            label="中学校"
                            id="middle_school"
                            name="middle_school"
                            value={middle_school}
                            onChange={(e) => setMiddle_school(e.target.value)}
                            fullWidth
                            required
                        />
                    </Grid>

                    <Grid>
                        <TextField
                            margin="normal"
                            label="高校"
                            id="high_school"
                            name="high_school"
                            value={high_school}
                            onChange={(e) => setHigh_school(e.target.value)}
                            fullWidth
                            required
                        />
                    </Grid>

                    <Grid>
                        <TextField
                            margin="normal"
                            label="大学"
                            id="university"
                            name="university"
                            value={university}
                            onChange={(e) => setUniversity(e.target.value)}
                            fullWidth
                            required
                        />
                    </Grid>

                    <Button
                        type="submit"
                        fullWidth
                        variant="contained"
                        sx={{mt:3, mb:2}}
                    >
                        変更
                    </Button>

                    <Button variant="text" href="/home" sx={{ mt: 1 }}>
                        ホームに戻る
                    </Button>
                </Box>
            </Paper>
        </Container>
    )
}

export default withCookies(EditProfile)

好きな人の選択画面

import { withCookies } from "react-cookie";
import {apiURL} from "../App";
import React, {useEffect, useState} from "react";
import axios from "axios";
import {Box, Button, Card, Container, Grid, Link, Paper, Table, Typography} from "@mui/material";

const Choice = (props) => {
    const [otherProfileList, setOtherProfileList] = useState([]);
    const [favoriteList, setFavoriteList] = useState([])

    const getOtherProfile = () => {
        axios.get(`${apiURL}/api/other_profile`, {
            headers: {
                'Authorization': `JWT ${props.cookies.get('token')}`
            }
        })
            .then(res => {
                setOtherProfileList(res.data)
                console.log(res.data)
            })
            .catch(error => {
                console.error(error);
            });
    }

    const getFavorite = () => {
         axios.get(`${apiURL}/api/my_favorite/`, {
                    headers:{
                        'Authorization': `JWT ${props.cookies.get('token')}`
                    }
                })
                    .then(res => {
                        console.log(res.data)
                        setFavoriteList(res.data.map(item => item.approached))
                    })
                    .catch(error => {
                        console.log(error)
                    })
    }

    const handleLike = (profile) => {
        axios.post(`${apiURL}/api/my_favorite/`, {"approached":profile.user}, {
            headers:{
                'Authorization': `JWT ${props.cookies.get('token')}`
            }
        })
            .then(res => {
                console.log(res.data)
                getFavorite()
            })
            .catch(error => {
                console.log(error)
            })
    }

    useEffect(() => {
        getOtherProfile()
        getFavorite()
    }, []);

    const handleDelete = (profile) => {
        axios.delete(`${apiURL}/api/delete_favorite?approached=${profile.user}`, {
            headers:{
                'Authorization': `JWT ${props.cookies.get('token')}`
            }
        })
            .then(res => {
                console.log(res.data)
                getFavorite()
            })
            .catch(error => {
                console.log(error)
            })
    };

    return (
        <Container maxWidth="md">
            <Box sx={{ my: 4 }}>
                <Typography variant="h4" component="h1" gutterBottom>
                    気になる人にLIKEボタンを押そう!
                </Typography>

                {otherProfileList.map((profile, index) => (
                    <Card sx={{ my:4 }} key={index}>
                        <Grid container justifyContent="center" alignItems="center">
                            <Grid  item xs={6}>
                                <Table sx={{my:4, mx:4}}>
                                    <tbody>
                                    <tr>
                                        <th>名前</th>
                                        <td>{profile.last_name} {profile.first_name}</td>
                                    </tr>
                                    <tr>
                                        <th>年齢</th>
                                        <td>{profile.age}歳</td>
                                    </tr>
                                    <tr>
                                        <th>性別</th>
                                        <td>{profile.sex === "male" ? "男性" : "女性"}</td>
                                    </tr>
                                    <tr>
                                        <th>趣味</th>
                                        <td>{profile.hobby}</td>
                                    </tr>
                                    </tbody>
                                </Table>
                            </Grid>
                            <Grid item xs={6}>
                                {favoriteList.includes(profile.user) ? (
                                    <div>
                                        <Button  variant="outlined" onClick={() => handleDelete(profile)}>
                                            LIKEを取り消す
                                        </Button>
                                    </div>
                                ) : (
                                    <Button variant="contained" onClick={() => handleLike(profile)}>
                                        LIKE
                                    </Button>
                                )}
                            </Grid>
                        </Grid>
                    </Card>
                ))}
                <Box sx={{ mt: 2 }}>
                    <Link href="/home">
                        <Button variant="outlined">Homeに戻る</Button>
                    </Link>
                </Box>
            </Box>
        </Container>
    )
}

export default withCookies(Choice)

LIKEを押した人の一覧

import { withCookies } from "react-cookie";
import {Box, Button, Card, Container, Grid, Link, Typography} from "@mui/material";
import React, {useEffect, useState} from "react";
import axios from "axios";
import {apiURL} from "../App";

const Favorite = (props) => {
    const [favoriteList, setFavoriteList] = useState([])
    const [favoriteProfileList, setFavoriteProfileList] = useState([]);

    const getFavorite = () => {
        axios.get(`${apiURL}/api/my_favorite/`, {
            headers:{
                'Authorization': `JWT ${props.cookies.get('token')}`
            }
        })
            .then(res => {
                setFavoriteList(res.data.map(item => item.approached))
            })
            .catch(error => {
                console.log(error)
            })
    }

    const getFavoriteProfile = () => {
        const queryString = favoriteList.map(id => `user_ids=${id}`).join('&');
        const getFavoriteUrl = `${apiURL}/api/favorite_profile?${queryString}`;

        axios.get(getFavoriteUrl, {
            headers: {
                'Authorization': `JWT ${props.cookies.get('token')}`
                }
            })
            .then(res => {
                setFavoriteProfileList(res.data)
            })
            .catch(error => {
                console.error(error);
            });
    }

    useEffect(() => {
        getFavorite()
        getFavoriteProfile()
    }, [favoriteList]);

    return(
        <Container maxWidth="md">
            <Box sx={{my:4}}>
                <Typography variant="h4" component="h1" gutterBottom>
                    LIKEした人
                </Typography>
            </Box>

            {favoriteProfileList.map((profile, index) => (
                <Card sx={{my:4}}>
                    <Typography sx={{my:4, mx:4}}>
                        {profile.last_name} {profile.first_name}さん
                    </Typography>
                </Card>
            ))}

            <Box sx={{ mt: 2 }}>
                <Link href="/home">
                    <Button variant="outlined">Homeに戻る</Button>
                </Link>
            </Box>
        </Container>
    )
}

export default withCookies(Favorite)

マッチング可否の画面

import {withCookies} from "react-cookie";
import {Box, Button, Card, Container, Grid, Link, Typography} from "@mui/material";
import React, {useEffect, useState} from "react";
import axios from "axios";
import {apiURL} from "../App";

const Matching = (props) => {
    const [favoriteList, setFavoriteList] = useState([])
    const [approachedMeList, setApproachedMeList] = useState([])
    const [matchingList, setMatchingList] = useState([])
    const [matchingUserProfileList, setMatchingUserProfileList] = useState([])

    // 自分がLIKEを押した人
    const getFavorite = () => {
        axios.get(`${apiURL}/api/my_favorite/`, {
            headers:{
                'Authorization': `JWT ${props.cookies.get('token')}`
            }
        })
            .then(res => {
                setFavoriteList(res.data.map(item => item.approached))
            })
            .catch(error => {
                console.log(error)
            })
    }

    // 自分にLIKEを押してくれた人
    const getApproachedMe = () => {
        axios.get(`${apiURL}/api/approached_me/`, {
            headers:{
                'Authorization': `JWT ${props.cookies.get('token')}`
            }
        })
            .then(res => {
                setApproachedMeList(res.data.map(item => item.approaching))
            })
            .catch(error => {
                console.log(error)
            })
    }

    // 両想いのユーザーのProfile
    const matchingProfile = () => {
        const queryString = matchingList.map(id => `user_ids=${id}`).join('&');
        const getMatchingUrl = `${apiURL}/api/favorite_profile?${queryString}`;

        axios.get(getMatchingUrl, {
            headers: {
                'Authorization': `JWT ${props.cookies.get('token')}`
            }
        })
            .then(res => {
                setMatchingUserProfileList(res.data)
            })
            .catch(error => {
                console.error(error);
            });
    }

    useEffect(() => {
        getFavorite()
        getApproachedMe()

        // 両想いの人のフィルタリング
        setMatchingList(favoriteList.filter(element => approachedMeList.includes(element)))
    }, [approachedMeList]);

    useEffect(() => {
        matchingProfile()
    }, [matchingList]);

    return(
        <Container>
            <Box sx={{my:4}}>
                <Typography variant="h4" component="h4" gutterBottom>
                    マッチングが成立した人
                </Typography>

                {matchingUserProfileList.map((profile, index) => (
                    <Card sx={{my:4, maxWidth: 400}} key={index}>
                        <Grid container justifyContent="center" alignItems="center">
                            <Grid item xs={6}>
                                <Typography sx={{my:4, mx:4}}>
                                    {profile.last_name} {profile.first_name}さん
                                </Typography>
                            </Grid>
                            <Grid item xa={6}>
                                <Typography sx={{my:4, mx:4}}>
                                    <Link href={`/dm/${profile.user}`}>
                                        <Button>DM</Button>
                                    </Link>
                                </Typography>
                            </Grid>
                        </Grid>
                    </Card>
                ))}

            </Box>

            <Box sx={{ mt: 2 }}>
                <Link href="/home">
                    <Button variant="outlined">Homeに戻る</Button>
                </Link>
            </Box>
        </Container>
    )
}

export default withCookies(Matching)

ダイレクトメッセージの画面

import { withCookies } from "react-cookie";
import {Box, Button, Divider, Grid, Link, List, ListItem, TextField, Typography} from "@mui/material";
import React, {useEffect, useRef, useState} from "react";
import { useParams } from "react-router-dom";
import { apiURL } from "../App";
import axios from "axios";

const DirectMessage = (props) => {
  const { userId } = useParams();
  const [friendName, setFriendName] = useState("");
  const [messages, setMessages] = useState([]); // Combined messages state
  const [input, setInput] = useState("")

  const fetchData = async () => {
      try {
        const friendResponse = await axios.get(`${apiURL}/api/favorite_profile?user_ids=${userId}`, {
          headers: {
            Authorization: `JWT ${props.cookies.get("token")}`,
          },
        });

        setFriendName(friendResponse.data[0].last_name);

        const sentMessagesResponse = await axios.get(`${apiURL}/api/dm-message?receiver_id=${userId}`, {
          headers: {
            Authorization: `JWT ${props.cookies.get("token")}`,
          },
        });

        const receivedMessagesResponse = await axios.get(`${apiURL}/api/dm-inbox?sender_id=${userId}`, {
          headers: {
            Authorization: `JWT ${props.cookies.get("token")}`,
          },
        });

        // Combine messages with proper sender identification
        const combinedMessages = [
          ...sentMessagesResponse.data.map((message) => ({ ...message, isSent: true })),
          ...receivedMessagesResponse.data.map((message) => ({ ...message, isSent: false })),
        ];

        // Sort messages chronologically by 'created_at' field
        combinedMessages.sort((a, b) => new Date(a.created_at) - new Date(b.created_at));

        setMessages(combinedMessages);
      } catch (error) {
        console.error(error);
      }
    };

  useEffect(() => {
    fetchData().then(r => console.log(r));
  }, [userId]);

  const handleSendMessage = (event) => {
    event.preventDefault();
    const form_data = new FormData();

    form_data.append("message", input);
    form_data.append("receiver", userId);

    axios.post(`${apiURL}/api/dm-message/`, form_data, {
      headers:{
        Authorization: `JWT ${props.cookies.get("token")}`,
      }
    })
        .then(res => {
          console.log(res.data)
          fetchData().then(r => console.log(r));
          setInput("")
        })
        .catch(error => {
          console.log("e")
        })
  }

  const styles = {
  container: {
    display: "flex",
    flexDirection: "column",
    height: "100vh",
  },
  header: {
    padding: "16px",
    backgroundColor: "#f5f5f5",
  },
  messageList: {
    flex: 1,
    overflow: "scroll",
  },
  message: {
    padding: "8px 16px",
    maxWidth: "80%",
    borderRadius: "8px",
    backgroundColor: "#f5f5f5",
  },
  myMessage: {
    backgroundColor: "#fff",
    alignSelf: "flex-end",
  },
  inputArea: {
    padding: "16px",
  },
  input: {
    width: "95%",
    padding: "8px 16px",
    borderRadius: "10px",
    // border: "1px solid #ccc",
  },
  button: {
    marginRight: "10px",
  },
};

  return (
      <Box sx={styles.container}>
        <Box sx={styles.header}>
          <Typography variant="h6">{friendName}さんとのメッセージ</Typography>
        </Box>
        <Box sx={styles.messageList}>
          <List>
            {messages.map((message, index) => (
                <ListItem key={index}>
                  <Box  sx={styles.message} className={message.isSent ? styles.myMessage : ""}>
                    {message.isSent ? "あなた:" : `${friendName}:`}{message.message}
                  </Box>
                  <Divider />
                </ListItem>
            ))}
          </List>
        </Box>
        <Box sx={styles.inputArea}>
          <Grid container justifyContent="center" alignItems="center">
            <Grid item xs={10}>
             <TextField
              id="message"
              name="message"
              value={input}
              onChange={(e) => setInput(e.target.value)}
              sx={styles.input}
              multiline
              rows={1}
          />
          </Grid>
          <Grid item xs={2}>
            <Button variant="contained" type="submit" sx={styles.button} onClick={handleSendMessage}>
              送信
            </Button>
          </Grid>
          </Grid>
        </Box>
        <Box sx={{ padding: "16px" }}>
          <Link href="/home">
            <Button variant="outlined">Homeに戻る</Button>
          </Link>
        </Box>
      </Box>
  );
};

export default withCookies(DirectMessage);

今後の課題

参考サイト

GitHubの使い方(初心者)

GitHubを使うために情報収集したので簡単にまとめました。詳細は参考サイトをご覧ください。

GitHubとは?

結論

プログラムのソースコードをオンラインで共有・管理するサービス

具体的な機能

バージョン管理

複数のプラグラマーが共同で開発をする際に、「最新版のソースコードってどれ?」、「どこを修正したの?修正前はどんなコードだったっけ?」といったことが問題となります。このような問題を解決するためにGitというソースコードのバージョン管理が行えるシステムがあります。 GitHubはこのGitをオンラインで使えるようにしたサービスです。

GitHubを使う上で必要な基礎知識

リポジトリ

リポジトリとは、ソースコードのデータや変更履歴の保存場所です。 自分のPC内にある「ローカルリポジトリ」と、ネットワーク上にある「リモートリポジトリ」があります。

コミット・プッシュ

コミットとは、「リポジトリへファイルを保存したり、変更履歴を保存したりする操作」のことです。
プッシュとは、「ローカルで変更した内容をリモートリポジトリへ反映させる」ことです。

ブランチ

ブランチとは、「開発の本流から分岐し、本流の開発を邪魔することなく作業を続ける機能」のことです。

フォーク

フォークとは、「他のプログラマーリポジトリをコピーする行為、またはコピーしたそのもの」のことです。

プルリクエス

プルリクエストとは、「ローカルリポジトリで変更した内容を、他のプログラマーに通知する機能、または通知そのもの」のことです。 ブランチしたりフォークしたりして変更した内容をオリジナルリポジトリに反映させたいときに利用します。

マージ

マージとは、「プルリクエストされた内容を自分のリポジトリに反映させる」ことです。

GitHubってどうやって使うの?

さあ、さっそく本題のGitHubの使い方に行きましょう。
Gitのインストールと、GitHubのアカウントは作られているとします。

リモートリポジトリの作成

「Create Repository」ボタンを押して、レポジトリの名前や公開範囲を設定します。
※README.mdファイルを追加すると、これから説明する手順を行ってもエラーが出ます。このエラーについては、下記の記事で解説しているためそちらをご参照ください。

ueccchi.hatenablog.com

ローカルリポジトリの作成

GitBashを開いて、PC上のプロジェクトのルートディレクトリに移動します。

cd ./github/sample

デフォルトのブランチの名前を"main"に変更します。

git config --global init.defaultBranch main

カレントディレクトリをローカルリポジトリにします(2回目以降は不要)

git init

ローカルリポジトリにコミットする

作成したローカルリポジトリディレクトリ配下に、ファイル(例えば、index.html)を保存します。 これらのファイルをインデックス(変更内容を一時的に保存しておく場所)へ追加します。 全てのファイルを保存したいときは、git add .とします。

git add index.html

以下のコマンドを実行し、インデックスに追加したファイルをローカルリポジトリへコミットします。

git commit -m"first commit"

リモートリポジトリにプッシュする

ローカルリポジトリとリモートリポジトリを紐づけます。originはリモートリポジトリのデフォルトの名前です。(2回目以降は不要)

git remote add origin https://github.com/ユーザー名/リモートリポジトリ名.git

ブッシュをします。

git push origin main

ブランチの作成などはまた後日まとめたいと思います。

参考サイト

www.kagoya.jp qiita.com

GitHubのエラー解決(error: failed to push some refs to 'https://github.com/ユーザー名/リポジトリ名.git')

はじめてGitHub使ってプッシュをしようとしたら、error: failed to push some refs to 'https://github.com/ユーザー名/リポジトリ名.git'というエラーが出たので、解決した記録を残します。

やりたかったこと

GitHubでリモートリポジトリを作る

ローカルリポジトリを作ってファイルをコミット

リモートリポジトリとローカルリポジトリを繋げる

リモートリポジトリへプッシュ

※以下がGitBashで実行したコード

mkdir github
cd github
mkdir test
cd test

git init

git add index.html
git commit -m"1st commit"

git remote add origin https://github.com/ユーザー名/リモートリポジトリ名.git
git push origin master

躓いたこと

リモートリポジトリのデフォルトのブランチ名がmainであるのに対して、ローカルリポジトリのデフォルトのブランチ名はmasterであるため、ローカルリポジトリをリモートリポジトリへプッシュすると、リモートリポジトリに新しくmasterというブランチが作成されて、そこにロカールリポジトリが反映されてしまいます。
リモートリポジトリのmainに反映させたいんだけどな...と思い、「ロカールリポジトリのデフォルトのブランチの名前をmainにすれば解決だ!」と思い、以下のコードを実行しました。

git config --global init.defaultBranch main

これで、"git push origin main"を実行しようとしたら、以下のエラーが出ました...

原因

上記のエラーは、「リモートの変更内容がローカルにないからプッシュできないよ...」という内容らしいです。 いやいや、リモートリポジトリは新規で作ったから変更とかしてないよなと思ったのですが、最初に追加した"README.md"が原因だと分かりました。

解決策

リモートリポジトリのREADME.mdをローカルリポジトリに反映させることが最終目標です! まずは、リモートリポジトリから最新の変更を取得するGitコマンドであるgit fetchを実行します。

git fetch

git fetchは、リモートのmainブランチの最新情報を、ローカルのorigin/mainブランチに反映させます。 そして、ローカルのorigin/mainブランチからローカルのmainブランチへ最新情報を反映させるためにgit mergeを実行します。

git merge --allow-unrelated-histories origin/main

※マージするときは、ブランチの根本が共通している必要があります。共通していないブランチをマージするときは、"--allow-unrelated-histories"オプションを指定する必要があります。

※mergeすると以下のメッセージが表示されるが以下の手順を操作すれば問題ないです

  1. escボタンを押す
  2. :wqと入力する
  3. Enterボタンを押す

再度、git push origin mainを実行すれば上手くいきます!!!

参考サイト

qiita.com qiita.com qiita.com dev.classmethod.jp codeclub965.com