davidcr01 / WordlePlus

Repository to store all the documentation, files and structure of my Final Degree Project (TFG in Spanish). The main goal is to develop, as full-stack web developer, a remodel of the Wordle game, including more features and functionalities using Ionic, Django REST Api and PostgreSQL.
1 stars 0 forks source link

Main page of the platform (S4) #23

Closed davidcr01 closed 1 year ago

davidcr01 commented 1 year ago

Description

Is necessary to develop the main page of the platform, where the user will have the player information in a banner, the category, and the play button that redirects to the Classic Wordle.

Tasks

This issue resolves the H17 completely.

davidcr01 commented 1 year ago

Update Report

The mockup done in Figma is the following: image

HTML

The HTML code of the main page (tab1) is the following:


<ion-content>
  <div id="container">
  <div class="header-container">
    <ion-card class="first-card">
      <div class="avatar-container">
        <img [src]="avatarImage" alt="Avatar">
      </div>
      <div class="user-info">
        <div class="username-container">
          <div class="username">{{ username }}</div>
        </div>
        <div class="victories-container">
          <div class="victory">
            <img src="../../assets/icon/wins-classic.png" alt="Victories Classic">
            <span>{{ victoriesClassic }}</span>
          </div>
          <div class="victory">
            <img src="../../assets/icon/wins-pvp.png" alt="Victories PvP">
            <span>{{ victoriesPvp }}</span>
          </div>
          <div class="victory">
            <img src="../../assets/icon/wins-tournaments.png" alt="Victories Tournaments">
            <span>{{ victoriesTournaments }}</span>
          </div>
          <div class="victory">
            <img src="../../assets/icon/experience.png" alt="Experience">
            <span>{{ xP }}</span>
          </div>
        </div>
      </div>

      <div class="special-button-container">
        <ion-button class="special-button">
          <ion-icon name="ribbon" class="icon"></ion-icon>
        </ion-button>
        <ion-button class="special-button">
          <ion-icon name="mail" class="icon"></ion-icon>
        </ion-button>
      </div>
    </ion-card>  
  </div>

  <ion-card class="second-card">
    <ion-card-content class="rank-text">
      <div class="image-overlay">
        <ion-img [src]="backgroundImage" class="translucent-bg" id="background-image"></ion-img>
        <ion-img [src]="rankImage" alt="Rank Image" class="rank-image"></ion-img>
      </div>
      <div class="title-container">
        <ion-card-title class="rank-name">{{ rank }}</ion-card-title>
      </div>
    </ion-card-content>
  </ion-card>

  <div class="button-container">
    <ion-button expand="block" size="large" id="classic-game">CLASSIC</ion-button>
    <ion-button expand="block" size="large" id="pvp-game">1VS1</ion-button>
  </div>
</div>
</ion-content>

This code is divided into three parts: the header container, where all the player information is collected; the second card, where is displayed the image rank of the player, the image background and the rank name; and the buttons zone, to play a classic game or the multiplayer.

⚠️ One important thing about this page is to make it responsive as we are not using too many ionic elements. This is due to the page's structure, as we need to create a specific page that may not be adapted to the ionic components.

To achieve this is very important to specify sizes as percentages or use vh or vw. These are units that depend on the height and width of the screen.

TS logic

To display the important information of the player in the screen, first is necessary to edit the login page logic, as is the page that fetches all the information of the user. In this case, the CustomObtainAuthToken class has been modified:

class CustomObtainAuthToken(ObtainAuthToken):
    """
    API endpoint that creates token with expiration date.
    """
    def post(self, request, *args, **kwargs):
        serializer = self.serializer_class(data=request.data)
        serializer.is_valid(raise_exception=True)

        user = serializer.validated_data['user']
        token, created = Token.objects.get_or_create(user=user)

        if not created and token.created < timezone.now() - timedelta(seconds=settings.TOKEN_EXPIRED_AFTER_SECONDS):
            # Token expired, generate a new one
            token.delete()
            token = Token.objects.create(user=user)

        # Serialize the token along with any other data you want to include in the response
        response_data = {
            'token': token.key,
            'user_id': user.id,
            'username': user.username,
            'player_id': user.player.id if hasattr(user, 'player') else None,  # Include the player ID if it exists
            'wins': user.player.wins if hasattr(user, 'player') else None,  # Include wins if player exists
            'wins_pvp': user.player.wins_pvp if hasattr(user, 'player') else None,  # Include wins_pvp if player exists
            'wins_tournament': user.player.wins_tournament if hasattr(user, 'player') else None,  # Include wins_tournament if player exists
            'xp': user.player.xp if hasattr(user, 'player') else None  # Include xp if player exists
        }

        return Response(response_data, status=status.HTTP_200_OK)

Now, when the user is logged in, its player information is also returned if exists. Now, in the login page, this information is stored in the StorageService:

login() {
    const credentials = {
      username: this.loginForm.get('username').value,
      password: this.loginForm.get('password').value,
    }

    this.apiService.login(credentials).subscribe(
      async (response) => {
        // Store the token in the local storage
        this.errorMessage = ''
        const encryptedToken = this.encryptionService.encryptData(response.token);

        await this.storageService.setAccessToken(encryptedToken);
        await this.storageService.setUserID(response.user_id);
        await this.storageService.setUsername(response.username);

        if (response.player_id !== null) {
          await this.storageService.setPlayerID(response.player_id);
          await this.storageService.setWins(response.wins);
          await this.storageService.setWinsPVP(response.wins_pvp);
          await this.storageService.setWinsTournament(response.wins_tournament);
          await this.storageService.setXP(response.xp);
          // Rank is calculated in the frontend
        }

And new methods in the StorageService have been defined, very similar to the previous one, defined for the username, player and user id.

💡 One important this about this is the calculation of the rank of the player. Before, this was thought to be calculated in the backend, but by this, everytime the user plays a game, the xp should be checked and the category should be updated is necessary. To avoid this, the frontend calculates the rank by consulting the xP of the player. This prevents the backend to make too many calculations.

  async setXP(xP: number) {
    await this._storage?.set('xp', xP);

    let rank = '';

    if (xP >= 0 && xP < 2000) {
      rank = 'IRON';
    } else if (xP >= 2000 && xP < 5000) {
      rank = 'BRONZE';
    } else if (xP >= 5000 && xP < 9000) {
      rank = 'SILVER';
    } else if (xP >= 9000 && xP < 14000) {
      rank = 'GOLD';
    } else if (xP >= 10400) {
      rank = 'PLATINUM';
    }
    await this._storage?.set('rank', rank);
  }

With his, every time the xp is fetched, the rank is calculated and changed.

davidcr01 commented 1 year ago

Update Report

Main page TS logic

Some important this about this page are:

ngOnInit() {
    if (window.innerWidth <= 767) {
      this.backgroundImage = '../../assets/background_wordle_vertical.png';
    } else {
      this.backgroundImage = '../../assets/background_wordle_horizontal.png';
    }
  }
async ionViewWillEnter() {
    this.username = await this.storageService.getUsername();
    this.victoriesClassic = await this.storageService.getWins();
    this.victoriesPvp = await this.storageService.getWinsPVP();
    this.victoriesTournaments = await this.storageService.getWinsTournament();
    this.xP = await this.storageService.getXP();
    this.rank = await this.storageService.getRank();
    this.rankImage = this.getRankImage(this.rank);
    const storedAvatarUrl = await this.storageService.getAvatarUrl();

    if (storedAvatarUrl) {
      this.avatarImage = storedAvatarUrl;
    } else {
      this.loadAvatarImage();
    }
  }

Avatar logic

The avatar logic is something important of this issue. The strategy with the avatars are the following:

To perform this, a new view in DRF has been added:

class AvatarView(APIView):
    permission_classes = [permissions.IsAuthenticated]

    def get(self, request, user_id):
        try:
            user = get_object_or_404(CustomUser, id=user_id)
            if request.user == user:
                if user.avatar:
                    with open(user.avatar.path, 'rb') as f:
                        image_data = f.read()
                        base64_image = base64.b64encode(image_data).decode('utf-8')
                        return Response({'avatar': base64_image}, status=200)
                else:
                    return Response({'detail': 'Avatar not available.'}, status=404)
            else:
                return Response({'detail': 'You do not have permission to get the avatar.'}, status=403)
        except CustomUser.DoesNotExist:
            return Response({'detail': 'The specified user does not exist.'}, status=404)

    def post(self, request, user_id):
        try:
            user = get_object_or_404(CustomUser, id=user_id)
            if request.user == user: 
                avatar = request.FILES.get('avatar')
                if avatar:
                    user.avatar = avatar
                    user.save()
                    return Response({'detail': 'Avatar uploaded correctly.'}, status=200)
                else:
                    return Response({'detail': 'No avatar image attached.'}, status=400)
            else:
                return Response({'detail': 'You do not have permission to upload an avatar.'}, status=403)
        except CustomUser.DoesNotExist:
            return Response({'detail': 'The specified user does not exist.'}, status=404)

The POST option is not used by now. The GET method opens the avatar stored in the backend, and returns it in base64, as long as the user who is requesting the avatar is the owner.

Avatar testing:

If we try to get the avatar without the access token: image

If we try to get the avatar with a token but we are not the owner of the image: image

With the correct token: image

Notice that the image is returned in base64.

With this, we define a new function in the tab1 logic:

async loadAvatarImage() {
    (await this.apiService.getAvatarImage()).subscribe(
      image => {
        if (image) {
          this.avatarImage = 'data:image/png;base64,' + image;
          this.storageService.setAvatarUrl(this.avatarImage);
        } else {
          this.avatarImage = '../../assets/avatar.png'; // Default avatar image
        }
      },
      error => {
        console.error('Error loading avatar image:', error);
        this.avatarImage = '../../assets/avatar.png';
      }
    );
  }

This function requests the avatar to the backend and stores it temporarily in the frontend by using the StorageService. With this, when reloading the page, a good practice is to check if we already have the avatar stored and request it if necessary:

const storedAvatarUrl = await this.storageService.getAvatarUrl();

    if (storedAvatarUrl) {
      this.avatarImage = storedAvatarUrl;
    } else {
      this.loadAvatarImage();
    }

If no avatar is returned, a default avatar is loaded. This avatar is stored in the frontend as it will be always the same.

Results

The results are the following:

Notice is a little bit different from the original mockup. Besides, notice that the avatar is different (the one stored in the backend) image

Notice that the default avatar is loaded, and instead of loading the rank image, an admin image is loaded.

The following video shows how the player info is correctly updated if, for example, the user plays a classic game. Notice how the victories, xp and rank image (and name) is changed:

https://github.com/davidcr01/WordlePlus/assets/72193239/bd89d703-47ee-492b-bb24-2a5e00ea5dc7