diff --git a/.gitignore b/.gitignore index 81a18a7..5f281b5 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ Wordle+/django/djapi/__pycache__ Wordle+/django/djapi/migrations/ Wordle+/ionic/ionic-app/src/env.json Wordle+/ionic/ionic-app/src/assets/words.json +Wordle+/django/avatars/avatars/ diff --git a/Wordle+/django/avatars/avatars/guts-icon.jpg b/Wordle+/django/avatars/avatars/guts-icon.jpg deleted file mode 100644 index 6e9ca3f..0000000 Binary files a/Wordle+/django/avatars/avatars/guts-icon.jpg and /dev/null differ diff --git a/Wordle+/django/avatars/avatars/guts-icon_pPjMhWI.jpg b/Wordle+/django/avatars/avatars/guts-icon_pPjMhWI.jpg deleted file mode 100644 index 6e9ca3f..0000000 Binary files a/Wordle+/django/avatars/avatars/guts-icon_pPjMhWI.jpg and /dev/null differ diff --git a/Wordle+/django/djapi/views.py b/Wordle+/django/djapi/views.py index 96a035a..58edbfb 100644 --- a/Wordle+/django/djapi/views.py +++ b/Wordle+/django/djapi/views.py @@ -1,4 +1,5 @@ -import base64 +import base64, os +import imghdr from django.contrib.auth.models import Group from .models import CustomUser, Player, ClassicWordle from rest_framework import viewsets, permissions, status @@ -13,6 +14,8 @@ from django.utils import timezone from django.conf import settings from django.shortcuts import get_object_or_404 +from django.core.files.base import ContentFile +from django.http import JsonResponse class CustomUserViewSet(viewsets.ModelViewSet): @@ -184,8 +187,6 @@ def create(self, request): serializer.save(player=player) return Response(serializer.data, status=201) - - class AvatarView(APIView): permission_classes = [permissions.IsAuthenticated] @@ -194,33 +195,36 @@ def get(self, request, user_id): 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) + avatar_data = user.avatar.read() + return JsonResponse({'avatar': avatar_data.decode('utf-8')}, status=200, safe=False) 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() + if request.user == user: + avatar_data = request.data.get('avatar') + if avatar_data: + # Delete the existing avatar if it exists + if user.avatar: + user.avatar.delete() + + # Save the avatar image without encoding or decoding + filename = f'{user_id}_avatar.png' + user.avatar.save(filename, ContentFile(avatar_data.encode('utf-8'))) 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) - + return Response({'detail': 'The specified user does not exist.'}, status=404) + class NotificationsViewSet(viewsets.ModelViewSet): queryset = Notifications.objects.all() serializer_class = NotificationsSerializer diff --git a/Wordle+/ionic/ionic-app/src/app/pages/edit-user/edit-user.page.html b/Wordle+/ionic/ionic-app/src/app/pages/edit-user/edit-user.page.html index a92194a..e8301e9 100644 --- a/Wordle+/ionic/ionic-app/src/app/pages/edit-user/edit-user.page.html +++ b/Wordle+/ionic/ionic-app/src/app/pages/edit-user/edit-user.page.html @@ -5,8 +5,11 @@ -
+
+ + Change Info +
@@ -36,5 +39,22 @@
+ + + + Change Avatar + + + + Current Avatar + + + + + + + Upload New Avatar + +
diff --git a/Wordle+/ionic/ionic-app/src/app/pages/edit-user/edit-user.page.scss b/Wordle+/ionic/ionic-app/src/app/pages/edit-user/edit-user.page.scss index 922dbab..ccfbaa9 100644 --- a/Wordle+/ionic/ionic-app/src/app/pages/edit-user/edit-user.page.scss +++ b/Wordle+/ionic/ionic-app/src/app/pages/edit-user/edit-user.page.scss @@ -7,4 +7,19 @@ ion-content{ padding-top: 1vh; padding-bottom: 1vh; border: none; +} + +.edit-container{ + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; +} + +ion-card{ + margin: 5vh; +} + +ion-button{ + --background: var(--ion-color-turquoise-dark); } \ No newline at end of file diff --git a/Wordle+/ionic/ionic-app/src/app/pages/edit-user/edit-user.page.ts b/Wordle+/ionic/ionic-app/src/app/pages/edit-user/edit-user.page.ts index b50a8cf..1c2cf85 100644 --- a/Wordle+/ionic/ionic-app/src/app/pages/edit-user/edit-user.page.ts +++ b/Wordle+/ionic/ionic-app/src/app/pages/edit-user/edit-user.page.ts @@ -1,4 +1,4 @@ -import { Component, OnInit } from '@angular/core'; +import { Component, ElementRef, OnInit, ViewChild } from '@angular/core'; import { FormGroup, FormBuilder, Validators } from '@angular/forms'; import { Router } from '@angular/router'; import { ApiService } from 'src/app/services/api.service'; @@ -14,18 +14,23 @@ import { ToastService } from 'src/app/services/toast.service'; export class EditUserPage implements OnInit { userInfo: any = {}; userInfoForm: FormGroup; + avatarPreview: string | null = null; + @ViewChild('avatarInput', { static: false }) avatarInput!: ElementRef; - constructor(private apiService: ApiService, - private storageService: StorageService, + constructor( + private apiService: ApiService, + private storageService: StorageService, private router: Router, private toastService: ToastService, - private formBuilder: FormBuilder) { - this.userInfoForm = this.formBuilder.group({ - email: ['', [Validators.required, Validators.email]], - firstName: ['', [Validators.required, Validators.maxLength(20)]], - lastName: ['', [Validators.required, Validators.maxLength(20)]], - }); - } + private formBuilder: FormBuilder + ) { + this.userInfoForm = this.formBuilder.group({ + email: ['', [Validators.required, Validators.email]], + firstName: ['', [Validators.required, Validators.maxLength(20)]], + lastName: ['', [Validators.required, Validators.maxLength(20)]], + avatar: [null] + }); + } ngOnInit() { this.getUserInfo(); @@ -40,6 +45,10 @@ export class EditUserPage implements OnInit { lastName: this.userInfo.last_name, }); }); + const avatarUrl = await this.storageService.getAvatarUrl(); + if (avatarUrl) { + this.avatarPreview = avatarUrl; + } } async saveUserInfo() { @@ -48,23 +57,55 @@ export class EditUserPage implements OnInit { const firstName = this.userInfoForm.get('firstName').value; const lastName = this.userInfoForm.get('lastName').value; const body = { - 'email': email, - 'first_name': firstName, - 'last_name': lastName + email: email, + first_name: firstName, + last_name: lastName }; - (await this.apiService.updateUserInfo(body)).subscribe( () => { - this.toastService.showToast('Information updated succesfully!', 2000, 'top'); + this.toastService.showToast('Information updated successfully!', 2000, 'top'); this.router.navigate(['/tabs/settings']); }, (error) => { - this.toastService.showToast('An error was generated', 2000, 'top'); + this.toastService.showToast('An error occurred', 2000, 'top'); console.error('Error saving the information:', error); } ); } else { - console.error('Formulario inválido'); + console.error('Invalid form'); } } + + async uploadAvatar() { + const file = this.avatarInput.nativeElement.files[0]; + if (file) { + const reader = new FileReader(); + reader.onloadend = () => { + const avatarData = reader.result as string; + console.log(avatarData); + this.saveAvatar(avatarData); + }; + reader.readAsDataURL(file); + } + } + + async saveAvatar(avatarData: string) { + try { + await (await this.apiService.saveAvatarImage(avatarData)).toPromise(); + this.storageService.setAvatarUrl(avatarData); + this.toastService.showToast('Avatar updated successfully!', 2000, 'top'); + this.getUserInfo(); // Refresh user info to update avatar preview + this.router.navigate(['/tabs/main'], { queryParams: { avatar: 'true' } }); + } catch (error) { + console.error('Error uploading avatar:', error); + } + } + + readAndPreviewAvatar(file: File) { + const reader = new FileReader(); + reader.onloadend = () => { + this.avatarPreview = reader.result as string; + }; + reader.readAsDataURL(file); + } } diff --git a/Wordle+/ionic/ionic-app/src/app/pages/home/home.page.ts b/Wordle+/ionic/ionic-app/src/app/pages/home/home.page.ts index 9705173..b05d91b 100644 --- a/Wordle+/ionic/ionic-app/src/app/pages/home/home.page.ts +++ b/Wordle+/ionic/ionic-app/src/app/pages/home/home.page.ts @@ -22,7 +22,7 @@ export class HomePage { } else { this.router.navigateByUrl('/tabs'); } - }, 2000); + }, 4000); } diff --git a/Wordle+/ionic/ionic-app/src/app/pages/login/login.page.ts b/Wordle+/ionic/ionic-app/src/app/pages/login/login.page.ts index 736891f..3530141 100644 --- a/Wordle+/ionic/ionic-app/src/app/pages/login/login.page.ts +++ b/Wordle+/ionic/ionic-app/src/app/pages/login/login.page.ts @@ -56,6 +56,7 @@ export class LoginPage implements OnInit { this.apiService.login(credentials).subscribe( async (response) => { + // Store the token in the local storage this.errorMessage = '' const encryptedToken = this.encryptionService.encryptData(response.token); @@ -72,7 +73,8 @@ export class LoginPage implements OnInit { await this.storageService.setXP(response.xp); // Rank is calculated in the frontend } - this.router.navigateByUrl(''); + this.isLoading = false; + this.router.navigate(['/tabs/main'], { queryParams: { avatar: 'true' } }); }, (error) => { console.error('Log in error', error); diff --git a/Wordle+/ionic/ionic-app/src/app/pages/register/register.page.ts b/Wordle+/ionic/ionic-app/src/app/pages/register/register.page.ts index 7df82a0..d9d662f 100644 --- a/Wordle+/ionic/ionic-app/src/app/pages/register/register.page.ts +++ b/Wordle+/ionic/ionic-app/src/app/pages/register/register.page.ts @@ -23,7 +23,7 @@ export class RegisterPage implements OnInit{ ngOnInit() { // Define the fields of the form this.registerForm = this.formBuilder.group({ - username: ['', [Validators.required, Validators.minLength(4)], Validators.maxLength(10)], + username: ['', [Validators.required, Validators.minLength(4),Validators.maxLength(10)]], email: ['', [Validators.required, Validators.email]], first_name: ['', Validators.required], last_name: ['', Validators.required], diff --git a/Wordle+/ionic/ionic-app/src/app/services/api.service.ts b/Wordle+/ionic/ionic-app/src/app/services/api.service.ts index 9b116bb..dbd83ea 100644 --- a/Wordle+/ionic/ionic-app/src/app/services/api.service.ts +++ b/Wordle+/ionic/ionic-app/src/app/services/api.service.ts @@ -66,7 +66,8 @@ import { EncryptionService } from './encryption.service'; } async saveAvatarImage(imageData: string): Promise> { - const url = `${this.baseURL}/api/avatar/`; + const userId = await this.storageService.getUserID(); + const url = `${this.baseURL}/api/avatar/${userId}/`; const accessToken = await this.storageService.getAccessToken(); if (!accessToken) { return throwError('Access token not found'); @@ -76,7 +77,7 @@ import { EncryptionService } from './encryption.service'; Authorization: `Token ${decryptedToken}`, 'Content-Type': 'application/json' }); - const body = { image_data: imageData }; + const body = { avatar: imageData }; return this.http.post(url, body, { headers }); } diff --git a/Wordle+/ionic/ionic-app/src/app/services/notification.service.ts b/Wordle+/ionic/ionic-app/src/app/services/notification.service.ts index 15ea85f..07a984f 100644 --- a/Wordle+/ionic/ionic-app/src/app/services/notification.service.ts +++ b/Wordle+/ionic/ionic-app/src/app/services/notification.service.ts @@ -18,7 +18,6 @@ export class NotificationService { resolve(this.notifications); } else { const storedNotifications = await this.storageService.getNotifications(); - console.log(storedNotifications); if (storedNotifications) { this.notifications = storedNotifications; resolve(this.notifications); diff --git a/Wordle+/ionic/ionic-app/src/app/services/storage.service.ts b/Wordle+/ionic/ionic-app/src/app/services/storage.service.ts index c8c4d25..c72db1f 100644 --- a/Wordle+/ionic/ionic-app/src/app/services/storage.service.ts +++ b/Wordle+/ionic/ionic-app/src/app/services/storage.service.ts @@ -143,8 +143,8 @@ export class StorageService { } // Avatar - async setAvatarUrl(imageData: string) { - await this._storage?.set('avatarUrl', imageData); + async setAvatarUrl(image: string) { + await this._storage?.set('avatarUrl', image); } async getAvatarUrl(): Promise { diff --git a/Wordle+/ionic/ionic-app/src/app/tab1/tab1.page.ts b/Wordle+/ionic/ionic-app/src/app/tab1/tab1.page.ts index 346ac48..336643b 100644 --- a/Wordle+/ionic/ionic-app/src/app/tab1/tab1.page.ts +++ b/Wordle+/ionic/ionic-app/src/app/tab1/tab1.page.ts @@ -29,7 +29,7 @@ export class Tab1Page implements OnInit{ private storageService: StorageService, private apiService: ApiService, public popoverController: PopoverController, - private notificationService: NotificationService + private notificationService: NotificationService, ) {} // Change background img depending on the width @@ -39,22 +39,19 @@ export class Tab1Page implements OnInit{ } else { this.backgroundImage = '../../assets/background_wordle_horizontal.png'; } - - // Only fetchs the avatar if necessary - const storedAvatarUrl = await this.storageService.getAvatarUrl(); - if (storedAvatarUrl) { - this.avatarImage = storedAvatarUrl; - } else { - await this.loadAvatarImage(); - } + this.getAvatarImage(); // Optional param to update the player info: useful when // finishing a game this.route.queryParams.subscribe(async params => { const refresh = params['refresh']; + const avatar = params['avatar']; if (refresh === 'true') { await this.ionViewWillEnter(); } + if (avatar === 'true') { + this.getAvatarImage(); + } }); } @@ -70,6 +67,16 @@ export class Tab1Page implements OnInit{ this.notificationService.refreshNotifications(); } + async getAvatarImage() { + // Only fetchs the avatar if necessary + const storedAvatarUrl = await this.storageService.getAvatarUrl(); + if (storedAvatarUrl) { + this.avatarImage = storedAvatarUrl; + } else { + await this.loadAvatarImage(); + } + } + // Popover of word length selection async handleSelectionPopover(event: any) { const popover = await this.popoverController.create({ @@ -90,14 +97,13 @@ export class Tab1Page implements OnInit{ }); await popover.present(); - } async loadAvatarImage() { (await this.apiService.getAvatarImage()).subscribe( image => { if (image) { - this.avatarImage = 'data:image/png;base64,' + image; + this.avatarImage = image; this.storageService.setAvatarUrl(this.avatarImage); } else { this.avatarImage = '../../assets/avatar.png'; // Default avatar image