From db297c06ce306f14c3e00399145147600325e9cb Mon Sep 17 00:00:00 2001 From: sosokker Date: Sun, 5 Nov 2023 03:47:43 +0700 Subject: [PATCH 1/7] Connect Amazon S3 to store image --- backend/core/settings.py | 25 ++++++++++++++++++++++++- requirements.txt | 4 +++- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/backend/core/settings.py b/backend/core/settings.py index ba1eb69..cb43b70 100644 --- a/backend/core/settings.py +++ b/backend/core/settings.py @@ -226,4 +226,27 @@ LOGOUT_REDIRECT_URL = '/' AUTH_USER_MODEL = "users.CustomUser" -ACCOUNT_EMAIL_REQUIRED = True \ No newline at end of file +ACCOUNT_EMAIL_REQUIRED = True + +# Storages + +AWS_ACCESS_KEY_ID = config('AMAZON_S3_ACCESS_KEY') +AWS_SECRET_ACCESS_KEY = config('AMAZON_S3_SECRET_ACCESS_KEY') +AWS_STORAGE_BUCKET_NAME = config('BUCKET_NAME') +AWS_DEFAULT_ACL = 'public-read' +AWS_S3_CUSTOM_DOMAIN = f'{AWS_STORAGE_BUCKET_NAME}.s3.amazonaws.com' +AWS_S3_OBJECT_PARAMETERS = {'CacheControl': 'max-age=86400'} + +MEDIA_URL = '/mediafiles/' +MEDIA_ROOT = os.path.join(BASE_DIR, 'mediafiles') + +STORAGES = { + "default": { + "BACKEND": "storages.backends.s3.S3Storage", + "OPTIONS": { + }, + }, + "staticfiles": { + "BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage", + }, +} \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index e5171c0..2f9a5c4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,4 +11,6 @@ djangorestframework-simplejwt>=5.3 django-cors-headers>=4.3 google_api_python_client>=2.1 google_auth_oauthlib>=1.1 -google-auth-httplib2>=0.1 \ No newline at end of file +google-auth-httplib2>=0.1 +django-storages[s3]>=1.14 +Pillow>=10.1 \ No newline at end of file From 3d9a56f0df7cec45fe29b5250de038d0a6e29fb2 Mon Sep 17 00:00:00 2001 From: sosokker Date: Sun, 5 Nov 2023 03:48:10 +0700 Subject: [PATCH 2/7] Update Model field and add Profile Cutomize API --- .../migrations/0003_customuser_profile_pic.py | 18 ++++++++++ backend/users/models.py | 1 + backend/users/serializers.py | 13 ++++++-- backend/users/urls.py | 3 +- backend/users/views.py | 33 +++++++++++++++++-- 5 files changed, 63 insertions(+), 5 deletions(-) create mode 100644 backend/users/migrations/0003_customuser_profile_pic.py diff --git a/backend/users/migrations/0003_customuser_profile_pic.py b/backend/users/migrations/0003_customuser_profile_pic.py new file mode 100644 index 0000000..1604578 --- /dev/null +++ b/backend/users/migrations/0003_customuser_profile_pic.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.6 on 2023-11-04 19:15 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0002_customuser_refresh_token'), + ] + + operations = [ + migrations.AddField( + model_name='customuser', + name='profile_pic', + field=models.ImageField(blank=True, default='profile_pics/default.png', null=True, upload_to='profile_pics'), + ), + ] diff --git a/backend/users/models.py b/backend/users/models.py index 64d976d..5b5b176 100644 --- a/backend/users/models.py +++ b/backend/users/models.py @@ -13,6 +13,7 @@ class CustomUser(AbstractBaseUser, PermissionsMixin): first_name = models.CharField(max_length=150, blank=True) start_date = models.DateTimeField(default=timezone.now) about = models.TextField(_('about'), max_length=500, blank=True) + profile_pic = models.ImageField(upload_to='profile_pics', null=True, blank=True, default='profile_pics/default.png') is_staff = models.BooleanField(default=False) is_active = models.BooleanField(default=True) diff --git a/backend/users/serializers.py b/backend/users/serializers.py index a25e8ec..531d912 100644 --- a/backend/users/serializers.py +++ b/backend/users/serializers.py @@ -7,12 +7,12 @@ class CustomUserSerializer(serializers.ModelSerializer): Serializer for CustomUser model. """ email = serializers.EmailField(required=True) - username = serializers.CharField(required=True) + username = serializers.CharField() password = serializers.CharField(min_length=8, write_only=True) class Meta: model = CustomUser - fields = ('email', 'password') + fields = ('email', 'username', 'password') extra_kwargs = {'password': {'write_only': True}} def create(self, validated_data): @@ -25,3 +25,12 @@ class CustomUserSerializer(serializers.ModelSerializer): instance.set_password(password) instance.save() return instance + + +class UpdateProfileSerializer(serializers.ModelSerializer): + """ + Serializer for updating user profile. + """ + class Meta: + model = CustomUser + fields = ('profile_pic', 'first_name', 'about') \ No newline at end of file diff --git a/backend/users/urls.py b/backend/users/urls.py index 2b44459..474da31 100644 --- a/backend/users/urls.py +++ b/backend/users/urls.py @@ -1,6 +1,7 @@ from django.urls import path -from users.views import CustomUserCreate +from users.views import CustomUserCreate, CustomUserProfileUpdate urlpatterns = [ path('user/create/', CustomUserCreate.as_view(), name="create_user"), + path('user/update/', CustomUserProfileUpdate.as_view(), name='update_user') ] \ No newline at end of file diff --git a/backend/users/views.py b/backend/users/views.py index 6f020b6..0cebbae 100644 --- a/backend/users/views.py +++ b/backend/users/views.py @@ -1,11 +1,13 @@ """This module defines API views for user creation""" from rest_framework import status -from rest_framework.permissions import AllowAny +from rest_framework.permissions import AllowAny, IsAuthenticated + from rest_framework.response import Response from rest_framework.views import APIView +from rest_framework.parsers import MultiPartParser -from .serializers import CustomUserSerializer +from users.serializers import CustomUserSerializer, UpdateProfileSerializer class CustomUserCreate(APIView): @@ -25,3 +27,30 @@ class CustomUserCreate(APIView): if user: return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + +class CustomUserProfileUpdate(APIView): + """ + Custom User Profile Update View. + """ + parser_classes = (MultiPartParser,) + permission_classes = (IsAuthenticated,) + + def get(self, request): + user = request.user + image_url = user.profile_pic.url + username = user.username + + data = { + 'image_url': image_url, + 'username': username + } + + return Response(data) + + def post(self, request): + serializer = UpdateProfileSerializer(data=request.data) + if serializer.is_valid(): + serializer.save() + return Response(serializer.data) + return Response(serializer.errors, status=400) \ No newline at end of file From 97e1557a56e928dedbc633b2cddee64d68cb76ae Mon Sep 17 00:00:00 2001 From: sosokker Date: Sun, 5 Nov 2023 15:46:14 +0700 Subject: [PATCH 3/7] Add DaisyUI / Remove material deprecated --- frontend/package.json | 5 +- frontend/pnpm-lock.yaml | 304 ++++++--------------------- frontend/src/components/testAuth.jsx | 2 +- frontend/tailwind.config.js | 35 ++- 4 files changed, 76 insertions(+), 270 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index 7ff9906..793cfe0 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -12,8 +12,6 @@ "dependencies": { "@emotion/react": "^11.11.1", "@emotion/styled": "^11.11.0", - "@material-ui/core": "^4.12.4", - "@material-ui/icons": "^4.11.3", "@mui/icons-material": "^5.14.15", "@mui/material": "^5.14.15", "@mui/system": "^5.14.15", @@ -27,15 +25,16 @@ "react": "^18.2.0", "react-bootstrap": "^2.9.1", "react-dom": "^18.2.0", - "react-google-login": "^5.2.2", "react-icons": "^4.11.0", "react-router-dom": "^6.17.0" }, "devDependencies": { + "@tailwindcss/typography": "^0.5.10", "@types/react": "^18.2.15", "@types/react-dom": "^18.2.7", "@vitejs/plugin-react": "^4.0.3", "autoprefixer": "^10.4.16", + "daisyui": "^3.9.4", "eslint": "^8.45.0", "eslint-plugin-react": "^7.32.2", "eslint-plugin-react-hooks": "^4.6.0", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 18613e9..67325cd 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -11,12 +11,6 @@ dependencies: '@emotion/styled': specifier: ^11.11.0 version: 11.11.0(@emotion/react@11.11.1)(@types/react@18.2.33)(react@18.2.0) - '@material-ui/core': - specifier: ^4.12.4 - version: 4.12.4(@types/react@18.2.33)(react-dom@18.2.0)(react@18.2.0) - '@material-ui/icons': - specifier: ^4.11.3 - version: 4.11.3(@material-ui/core@4.12.4)(@types/react@18.2.33)(react-dom@18.2.0)(react@18.2.0) '@mui/icons-material': specifier: ^5.14.15 version: 5.14.15(@mui/material@5.14.15)(@types/react@18.2.33)(react@18.2.0) @@ -56,9 +50,6 @@ dependencies: react-dom: specifier: ^18.2.0 version: 18.2.0(react@18.2.0) - react-google-login: - specifier: ^5.2.2 - version: 5.2.2(react-dom@18.2.0)(react@18.2.0) react-icons: specifier: ^4.11.0 version: 4.11.0(react@18.2.0) @@ -67,6 +58,9 @@ dependencies: version: 6.17.0(react-dom@18.2.0)(react@18.2.0) devDependencies: + '@tailwindcss/typography': + specifier: ^0.5.10 + version: 0.5.10(tailwindcss@3.3.5) '@types/react': specifier: ^18.2.15 version: 18.2.33 @@ -79,6 +73,9 @@ devDependencies: autoprefixer: specifier: ^10.4.16 version: 10.4.16(postcss@8.4.31) + daisyui: + specifier: ^3.9.4 + version: 3.9.4 eslint: specifier: ^8.45.0 version: 8.52.0 @@ -364,10 +361,6 @@ packages: stylis: 4.2.0 dev: false - /@emotion/hash@0.8.0: - resolution: {integrity: sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==} - dev: false - /@emotion/hash@0.9.1: resolution: {integrity: sha512-gJB6HLm5rYwSLI6PQa+X1t5CFGrv1J1TWG+sOyMCeKz2ojaj6Fnl/rZEspogG+cvqbt4AE/2eIyD2QfLKTBNlQ==} dev: false @@ -785,132 +778,6 @@ packages: '@jridgewell/sourcemap-codec': 1.4.15 dev: true - /@material-ui/core@4.12.4(@types/react@18.2.33)(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-tr7xekNlM9LjA6pagJmL8QCgZXaubWUwkJnoYcMKd4gw/t4XiyvnTkjdGrUVicyB2BsdaAv1tvow45bPM4sSwQ==} - engines: {node: '>=8.0.0'} - deprecated: Material UI v4 doesn't receive active development since September 2021. See the guide https://mui.com/material-ui/migration/migration-v4/ to upgrade to v5. - peerDependencies: - '@types/react': ^16.8.6 || ^17.0.0 - react: ^16.8.0 || ^17.0.0 - react-dom: ^16.8.0 || ^17.0.0 - peerDependenciesMeta: - '@types/react': - optional: true - dependencies: - '@babel/runtime': 7.23.2 - '@material-ui/styles': 4.11.5(@types/react@18.2.33)(react-dom@18.2.0)(react@18.2.0) - '@material-ui/system': 4.12.2(@types/react@18.2.33)(react-dom@18.2.0)(react@18.2.0) - '@material-ui/types': 5.1.0(@types/react@18.2.33) - '@material-ui/utils': 4.11.3(react-dom@18.2.0)(react@18.2.0) - '@types/react': 18.2.33 - '@types/react-transition-group': 4.4.8 - clsx: 1.2.1 - hoist-non-react-statics: 3.3.2 - popper.js: 1.16.1-lts - prop-types: 15.8.1 - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) - react-is: 16.13.1 - react-transition-group: 4.4.5(react-dom@18.2.0)(react@18.2.0) - dev: false - - /@material-ui/icons@4.11.3(@material-ui/core@4.12.4)(@types/react@18.2.33)(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-IKHlyx6LDh8n19vzwH5RtHIOHl9Tu90aAAxcbWME6kp4dmvODM3UvOHJeMIDzUbd4muuJKHmlNoBN+mDY4XkBA==} - engines: {node: '>=8.0.0'} - peerDependencies: - '@material-ui/core': ^4.0.0 - '@types/react': ^16.8.6 || ^17.0.0 - react: ^16.8.0 || ^17.0.0 - react-dom: ^16.8.0 || ^17.0.0 - peerDependenciesMeta: - '@types/react': - optional: true - dependencies: - '@babel/runtime': 7.23.2 - '@material-ui/core': 4.12.4(@types/react@18.2.33)(react-dom@18.2.0)(react@18.2.0) - '@types/react': 18.2.33 - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) - dev: false - - /@material-ui/styles@4.11.5(@types/react@18.2.33)(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-o/41ot5JJiUsIETME9wVLAJrmIWL3j0R0Bj2kCOLbSfqEkKf0fmaPt+5vtblUh5eXr2S+J/8J3DaCb10+CzPGA==} - engines: {node: '>=8.0.0'} - deprecated: Material UI v4 doesn't receive active development since September 2021. See the guide https://mui.com/material-ui/migration/migration-v4/ to upgrade to v5. - peerDependencies: - '@types/react': ^16.8.6 || ^17.0.0 - react: ^16.8.0 || ^17.0.0 - react-dom: ^16.8.0 || ^17.0.0 - peerDependenciesMeta: - '@types/react': - optional: true - dependencies: - '@babel/runtime': 7.23.2 - '@emotion/hash': 0.8.0 - '@material-ui/types': 5.1.0(@types/react@18.2.33) - '@material-ui/utils': 4.11.3(react-dom@18.2.0)(react@18.2.0) - '@types/react': 18.2.33 - clsx: 1.2.1 - csstype: 2.6.21 - hoist-non-react-statics: 3.3.2 - jss: 10.10.0 - jss-plugin-camel-case: 10.10.0 - jss-plugin-default-unit: 10.10.0 - jss-plugin-global: 10.10.0 - jss-plugin-nested: 10.10.0 - jss-plugin-props-sort: 10.10.0 - jss-plugin-rule-value-function: 10.10.0 - jss-plugin-vendor-prefixer: 10.10.0 - prop-types: 15.8.1 - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) - dev: false - - /@material-ui/system@4.12.2(@types/react@18.2.33)(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-6CSKu2MtmiJgcCGf6nBQpM8fLkuB9F55EKfbdTC80NND5wpTmKzwdhLYLH3zL4cLlK0gVaaltW7/wMuyTnN0Lw==} - engines: {node: '>=8.0.0'} - peerDependencies: - '@types/react': ^16.8.6 || ^17.0.0 - react: ^16.8.0 || ^17.0.0 - react-dom: ^16.8.0 || ^17.0.0 - peerDependenciesMeta: - '@types/react': - optional: true - dependencies: - '@babel/runtime': 7.23.2 - '@material-ui/utils': 4.11.3(react-dom@18.2.0)(react@18.2.0) - '@types/react': 18.2.33 - csstype: 2.6.21 - prop-types: 15.8.1 - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) - dev: false - - /@material-ui/types@5.1.0(@types/react@18.2.33): - resolution: {integrity: sha512-7cqRjrY50b8QzRSYyhSpx4WRw2YuO0KKIGQEVk5J8uoz2BanawykgZGoWEqKm7pVIbzFDN0SpPcVV4IhOFkl8A==} - peerDependencies: - '@types/react': '*' - peerDependenciesMeta: - '@types/react': - optional: true - dependencies: - '@types/react': 18.2.33 - dev: false - - /@material-ui/utils@4.11.3(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-ZuQPV4rBK/V1j2dIkSSEcH5uT6AaHuKWFfotADHsC0wVL1NLd2WkFCm4ZZbX33iO4ydl6V0GPngKm8HZQ2oujg==} - engines: {node: '>=8.0.0'} - peerDependencies: - react: ^16.8.0 || ^17.0.0 - react-dom: ^16.8.0 || ^17.0.0 - dependencies: - '@babel/runtime': 7.23.2 - prop-types: 15.8.1 - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) - react-is: 16.13.1 - dev: false - /@mui/base@5.0.0-beta.21(@types/react@18.2.33)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-eTKWx3WV/nwmRUK4z4K1MzlMyWCsi3WJ3RtV4DiXZeRh4qd4JCyp1Zzzi8Wv9xM4dEBmqQntFoei716PzwmFfA==} engines: {node: '>=12.0.0'} @@ -1173,6 +1040,18 @@ packages: tslib: 2.6.2 dev: false + /@tailwindcss/typography@0.5.10(tailwindcss@3.3.5): + resolution: {integrity: sha512-Pe8BuPJQJd3FfRnm6H0ulKIGoMEQS+Vq01R6M5aCrFB/ccR/shT+0kXLjouGC1gFLm9hopTFN+DMP0pfwRWzPw==} + peerDependencies: + tailwindcss: '>=3.0.0 || insiders' + dependencies: + lodash.castarray: 4.4.0 + lodash.isplainobject: 4.0.6 + lodash.merge: 4.6.2 + postcss-selector-parser: 6.0.10 + tailwindcss: 3.3.5 + dev: true + /@types/babel__core@7.20.3: resolution: {integrity: sha512-54fjTSeSHwfan8AyHWrKbfBWiEUrNTZsUwPTDSNaaP1QDQIZbeNUg3a59E9D+375MzUw/x1vx2/0F5LBz+AeYA==} dependencies: @@ -1525,11 +1404,6 @@ packages: resolution: {integrity: sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw==} dev: false - /clsx@1.2.1: - resolution: {integrity: sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==} - engines: {node: '>=6'} - dev: false - /clsx@2.0.0: resolution: {integrity: sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==} engines: {node: '>=6'} @@ -1554,6 +1428,10 @@ packages: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} dev: true + /colord@2.9.3: + resolution: {integrity: sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==} + dev: true + /combined-stream@1.0.8: resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} engines: {node: '>= 0.8'} @@ -1598,12 +1476,12 @@ packages: which: 2.0.2 dev: true - /css-vendor@2.0.8: - resolution: {integrity: sha512-x9Aq0XTInxrkuFeHKbYC7zWY8ai7qJ04Kxd9MnvbC1uO5DagxoHQjm4JvG+vCdXOoFtCjbL2XSZfxmoYa9uQVQ==} + /css-selector-tokenizer@0.8.0: + resolution: {integrity: sha512-Jd6Ig3/pe62/qe5SBPTN8h8LeUg/pT4lLgtavPf7updwwHpvFzxvOQBHYj2LZDMjUnBzgvIUSjRcf6oT5HzHFg==} dependencies: - '@babel/runtime': 7.23.2 - is-in-browser: 1.1.3 - dev: false + cssesc: 3.0.0 + fastparse: 1.1.2 + dev: true /cssesc@3.0.0: resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} @@ -1611,13 +1489,22 @@ packages: hasBin: true dev: true - /csstype@2.6.21: - resolution: {integrity: sha512-Z1PhmomIfypOpoMjRQB70jfvy/wxT50qW08YXO5lMIJkrdq4yOTR+AW7FqutScmB9NkLwxo+jU+kZLbofZZq/w==} - dev: false - /csstype@3.1.2: resolution: {integrity: sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==} + /daisyui@3.9.4: + resolution: {integrity: sha512-fvi2RGH4YV617/6DntOVGcOugOPym9jTGWW2XySb5ZpvdWO4L7bEG77VHirrnbRUEWvIEVXkBpxUz2KFj0rVnA==} + engines: {node: '>=16.9.0'} + dependencies: + colord: 2.9.3 + css-selector-tokenizer: 0.8.0 + postcss: 8.4.31 + postcss-js: 4.0.1(postcss@8.4.31) + tailwindcss: 3.3.5 + transitivePeerDependencies: + - ts-node + dev: true + /debug@4.3.4: resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} engines: {node: '>=6.0'} @@ -1995,6 +1882,10 @@ packages: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} dev: true + /fastparse@1.1.2: + resolution: {integrity: sha512-483XLLxTVIwWK3QTrMGRqUfUpoOs/0hbQrl2oz4J0pAcm3A3bu84wxTFqGqkJzewCLdME38xJLJAxBABfQT8sQ==} + dev: true + /fastq@1.15.0: resolution: {integrity: sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==} dependencies: @@ -2255,10 +2146,6 @@ packages: react-is: 16.13.1 dev: false - /hyphenate-style-name@1.0.4: - resolution: {integrity: sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ==} - dev: false - /ignore@5.2.4: resolution: {integrity: sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==} engines: {node: '>= 4'} @@ -2384,10 +2271,6 @@ packages: is-extglob: 2.1.1 dev: true - /is-in-browser@1.1.3: - resolution: {integrity: sha512-FeXIBgG/CPGd/WUxuEyvgGTEfwiG9Z4EKGxjNMRqviiIIfsmgrpnHLffEDdwUHqNva1VEW91o3xBT/m8Elgl9g==} - dev: false - /is-map@2.0.2: resolution: {integrity: sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg==} dev: true @@ -2531,68 +2414,6 @@ packages: hasBin: true dev: true - /jss-plugin-camel-case@10.10.0: - resolution: {integrity: sha512-z+HETfj5IYgFxh1wJnUAU8jByI48ED+v0fuTuhKrPR+pRBYS2EDwbusU8aFOpCdYhtRc9zhN+PJ7iNE8pAWyPw==} - dependencies: - '@babel/runtime': 7.23.2 - hyphenate-style-name: 1.0.4 - jss: 10.10.0 - dev: false - - /jss-plugin-default-unit@10.10.0: - resolution: {integrity: sha512-SvpajxIECi4JDUbGLefvNckmI+c2VWmP43qnEy/0eiwzRUsafg5DVSIWSzZe4d2vFX1u9nRDP46WCFV/PXVBGQ==} - dependencies: - '@babel/runtime': 7.23.2 - jss: 10.10.0 - dev: false - - /jss-plugin-global@10.10.0: - resolution: {integrity: sha512-icXEYbMufiNuWfuazLeN+BNJO16Ge88OcXU5ZDC2vLqElmMybA31Wi7lZ3lf+vgufRocvPj8443irhYRgWxP+A==} - dependencies: - '@babel/runtime': 7.23.2 - jss: 10.10.0 - dev: false - - /jss-plugin-nested@10.10.0: - resolution: {integrity: sha512-9R4JHxxGgiZhurDo3q7LdIiDEgtA1bTGzAbhSPyIOWb7ZubrjQe8acwhEQ6OEKydzpl8XHMtTnEwHXCARLYqYA==} - dependencies: - '@babel/runtime': 7.23.2 - jss: 10.10.0 - tiny-warning: 1.0.3 - dev: false - - /jss-plugin-props-sort@10.10.0: - resolution: {integrity: sha512-5VNJvQJbnq/vRfje6uZLe/FyaOpzP/IH1LP+0fr88QamVrGJa0hpRRyAa0ea4U/3LcorJfBFVyC4yN2QC73lJg==} - dependencies: - '@babel/runtime': 7.23.2 - jss: 10.10.0 - dev: false - - /jss-plugin-rule-value-function@10.10.0: - resolution: {integrity: sha512-uEFJFgaCtkXeIPgki8ICw3Y7VMkL9GEan6SqmT9tqpwM+/t+hxfMUdU4wQ0MtOiMNWhwnckBV0IebrKcZM9C0g==} - dependencies: - '@babel/runtime': 7.23.2 - jss: 10.10.0 - tiny-warning: 1.0.3 - dev: false - - /jss-plugin-vendor-prefixer@10.10.0: - resolution: {integrity: sha512-UY/41WumgjW8r1qMCO8l1ARg7NHnfRVWRhZ2E2m0DMYsr2DD91qIXLyNhiX83hHswR7Wm4D+oDYNC1zWCJWtqg==} - dependencies: - '@babel/runtime': 7.23.2 - css-vendor: 2.0.8 - jss: 10.10.0 - dev: false - - /jss@10.10.0: - resolution: {integrity: sha512-cqsOTS7jqPsPMjtKYDUpdFC0AbhYFLTcuGRqymgmdJIeQ8cH7+AgX7YSgQy79wXloZq2VvATYxUOUQEvS1V/Zw==} - dependencies: - '@babel/runtime': 7.23.2 - csstype: 3.1.2 - is-in-browser: 1.1.3 - tiny-warning: 1.0.3 - dev: false - /jsx-ast-utils@3.3.5: resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} engines: {node: '>=4.0'} @@ -2637,6 +2458,14 @@ packages: p-locate: 5.0.0 dev: true + /lodash.castarray@4.4.0: + resolution: {integrity: sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==} + dev: true + + /lodash.isplainobject@4.0.6: + resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} + dev: true + /lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} dev: true @@ -2872,10 +2701,6 @@ packages: engines: {node: '>= 6'} dev: true - /popper.js@1.16.1-lts: - resolution: {integrity: sha512-Kjw8nKRl1m+VrSFCoVGPph93W/qrSO7ZkqPpTf7F4bk/sqcfWK019dWBUpE/fBOsOQY1dks/Bmcbfn1heM/IsA==} - dev: false - /postcss-import@15.1.0(postcss@8.4.31): resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==} engines: {node: '>=14.0.0'} @@ -2925,6 +2750,14 @@ packages: postcss-selector-parser: 6.0.13 dev: true + /postcss-selector-parser@6.0.10: + resolution: {integrity: sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==} + engines: {node: '>=4'} + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + dev: true + /postcss-selector-parser@6.0.13: resolution: {integrity: sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ==} engines: {node: '>=4'} @@ -3018,19 +2851,6 @@ packages: scheduler: 0.23.0 dev: false - /react-google-login@5.2.2(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-JUngfvaSMcOuV0lFff7+SzJ2qviuNMQdqlsDJkUM145xkGPVIfqWXq9Ui+2Dr6jdJWH5KYdynz9+4CzKjI5u6g==} - deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. - peerDependencies: - react: ^16 || ^17 - react-dom: ^16 || ^17 - dependencies: - '@types/react': 18.2.33 - prop-types: 15.8.1 - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) - dev: false - /react-icons@4.11.0(react@18.2.0): resolution: {integrity: sha512-V+4khzYcE5EBk/BvcuYRq6V/osf11ODUM2J8hg2FDSswRrGvqiYUYPRy4OdrWaQOBj4NcpJfmHZLNaD+VH0TyA==} peerDependencies: @@ -3396,10 +3216,6 @@ packages: any-promise: 1.3.0 dev: true - /tiny-warning@1.0.3: - resolution: {integrity: sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==} - dev: false - /to-fast-properties@2.0.0: resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} engines: {node: '>=4'} diff --git a/frontend/src/components/testAuth.jsx b/frontend/src/components/testAuth.jsx index 9677133..9a822c7 100644 --- a/frontend/src/components/testAuth.jsx +++ b/frontend/src/components/testAuth.jsx @@ -1,6 +1,6 @@ import React, { useState, useEffect } from 'react'; import axiosapi from '../api/axiosapi'; -import Button from '@material-ui/core/Button'; +import { Button } from '@mui/material'; import { useNavigate } from 'react-router-dom'; function TestAuth() { diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js index b5867e2..c85efdb 100644 --- a/frontend/tailwind.config.js +++ b/frontend/tailwind.config.js @@ -1,30 +1,21 @@ /** @type {import('tailwindcss').Config} */ + +const defaultTheme = require('tailwindcss/defaultTheme') + export default { content: ["./src/**/*.{js,jsx}"], theme: { - colors: { - 'blue': '#1fb6ff', - 'purple': '#7e5bef', - 'pink': '#ff49db', - 'orange': '#ff7849', - 'green': '#13ce66', - 'yellow': '#ffc82c', - 'gray-dark': '#273444', - 'gray': '#8492a6', - 'gray-light': '#d3dce6', - }, - fontFamily: { - sans: ['Graphik', 'sans-serif'], - serif: ['Merriweather', 'serif'], - }, extend: { - spacing: { - '8xl': '96rem', - '9xl': '128rem', + fontFamily: { + 'sans': ['"Proxima Nova"', ...defaultTheme.fontFamily.sans], }, - borderRadius: { - '4xl': '2rem', - } - } + }, + }, + plugins: [require("daisyui"), + require("@tailwindcss/typography"), + require("daisyui") + ], + daisyui: { + themes: ["light", "night"], }, } From 93c8df313fcfa70e22bb4f38adc0123ad3529a8b Mon Sep 17 00:00:00 2001 From: sosokker Date: Sun, 5 Nov 2023 15:47:02 +0700 Subject: [PATCH 4/7] Modify Login Page --- frontend/src/App.jsx | 5 +- .../authentication/AuthenticationPage.jsx | 206 ------------------ .../components/authentication/LoginPage.jsx | 143 ++++++++++++ 3 files changed, 145 insertions(+), 209 deletions(-) delete mode 100644 frontend/src/components/authentication/AuthenticationPage.jsx create mode 100644 frontend/src/components/authentication/LoginPage.jsx diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 7333c4a..c18349d 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -3,12 +3,11 @@ import { BrowserRouter, Route, Routes, Link } from 'react-router-dom'; import TestAuth from './components/testAuth'; import IconSideNav from './components/IconSideNav'; -import AuthenticantionPage from './components/authentication/AuthenticationPage'; +import LoginPage from './components/authentication/LoginPage'; import SignUpPage from './components/authentication/SignUpPage'; import NavBar from './components/Nav/Navbar'; import Home from './components/Home'; - const App = () => { return ( @@ -16,7 +15,7 @@ const App = () => { }/> - }/> + }/> }/> }/> diff --git a/frontend/src/components/authentication/AuthenticationPage.jsx b/frontend/src/components/authentication/AuthenticationPage.jsx deleted file mode 100644 index 0955342..0000000 --- a/frontend/src/components/authentication/AuthenticationPage.jsx +++ /dev/null @@ -1,206 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import { useNavigate } from 'react-router-dom'; -import { useGoogleLogin } from '@react-oauth/google'; -import Avatar from '@mui/material/Avatar'; -import Button from '@mui/material/Button'; -import CssBaseline from '@mui/material/CssBaseline'; -import TextField from '@mui/material/TextField'; -import FormControlLabel from '@mui/material/FormControlLabel'; -import Checkbox from '@mui/material/Checkbox'; -import Link from '@mui/material/Link'; -import Divider from '@mui/material/Divider'; -import Paper from '@mui/material/Paper'; -import Box from '@mui/material/Box'; -import Grid from '@mui/material/Grid'; -import LockOutlinedIcon from '@mui/icons-material/LockOutlined'; -import Typography from '@mui/material/Typography'; -import { createTheme, ThemeProvider } from '@mui/material/styles'; - -import refreshAccessToken from './refreshAcesstoken'; -import axiosapi from '../../api/axiosapi'; - - -function Copyright(props) { - return ( - - {'Copyright © '} - - TurTask - {' '} - {new Date().getFullYear()} - {'.'} - - ); -} - - -const defaultTheme = createTheme(); - -export default function SignInSide() { - - const Navigate = useNavigate(); - - useEffect(() => { - if (!refreshAccessToken()) { - Navigate("/"); - } - }, []); - - const [email, setEmail] = useState(""); - const [username, setUsername] = useState(""); - const [password, setPassword] = useState(""); - - const handleUsernameChange = (event) => { - setUsername(event.target.value); - } - - const handleEmailChange = (event) => { - setEmail(event.target.value); - } - - const handlePasswordChange = (event) => { - setPassword(event.target.value); - } - - const handleSubmit = (event) => { - event.preventDefault(); - - // Send a POST request to the authentication API - axiosapi.apiUserLogin({ - email: email, - username: username, - password: password - }).then(res => { - // On successful login, store tokens and set the authorization header - localStorage.setItem('access_token', res.data.access); - localStorage.setItem('refresh_token', res.data.refresh); - axiosapi.axiosInstance.defaults.headers['Authorization'] = "Bearer " + res.data.access; - Navigate('/'); - }).catch(err => { - console.log('Login failed'); // Handle login failure - console.log(err) - }); - } - - const googleLoginImplicit = useGoogleLogin({ - flow: 'auth-code', - redirect_uri: 'postmessage', - onSuccess: async (response) => { - try { - const loginResponse = await axiosapi.googleLogin(response.code); - if (loginResponse && loginResponse.data) { - const { access_token, refresh_token } = loginResponse.data; - - // Save the tokens in localStorage - localStorage.setItem('access_token', access_token); - localStorage.setItem('refresh_token', refresh_token); - Navigate('/'); - } - } catch (error) { - console.error('Error with the POST request:', error); - } - }, - onError: errorResponse => console.log(errorResponse), - }); - - - return ( - - - - - t.palette.mode === 'light' ? t.palette.grey[50] : t.palette.grey[900], - backgroundSize: 'cover', - backgroundPosition: 'center', - }} - /> - - - - - - - Sign in - - - - - } - label="Remember me" - /> - - OR - - - - - - - Forgot password? - - - - - {"Don't have an account? Sign Up"} - - - - - - - - - - ); -} \ No newline at end of file diff --git a/frontend/src/components/authentication/LoginPage.jsx b/frontend/src/components/authentication/LoginPage.jsx new file mode 100644 index 0000000..242a9f1 --- /dev/null +++ b/frontend/src/components/authentication/LoginPage.jsx @@ -0,0 +1,143 @@ +import React, { useEffect, useState } from "react"; +import { useNavigate } from "react-router-dom"; +import { useGoogleLogin } from "@react-oauth/google" + +import refreshAccessToken from './refreshAcesstoken'; +import axiosapi from '../../api/axiosapi'; + +function LoginPage() { + const Navigate = useNavigate(); + + useEffect(() => { + if (!refreshAccessToken()) { + Navigate("/"); + } + }, []); + + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + + const handleEmailChange = event => { + setEmail(event.target.value); + }; + + const handlePasswordChange = event => { + setPassword(event.target.value); + }; + + const handleSubmit = event => { + event.preventDefault(); + + // Send a POST request to the authentication API + axiosapi + .apiUserLogin({ + email: email, + password: password, + }) + .then(res => { + // On successful login, store tokens and set the authorization header + localStorage.setItem("access_token", res.data.access); + localStorage.setItem("refresh_token", res.data.refresh); + axiosapi.axiosInstance.defaults.headers["Authorization"] = "Bearer " + res.data.access; + Navigate("/"); + }) + .catch(err => { + console.log("Login failed"); + console.log(err); + }); + }; + + const googleLoginImplicit = useGoogleLogin({ + flow: "auth-code", + redirect_uri: "postmessage", + onSuccess: async response => { + try { + const loginResponse = await axiosapi.googleLogin(response.code); + if (loginResponse && loginResponse.data) { + const { access_token, refresh_token } = loginResponse.data; + + localStorage.setItem("access_token", access_token); + localStorage.setItem("refresh_token", refresh_token); + Navigate("/"); + } + } catch (error) { + console.error("Error with the POST request:", error); + } + }, + onError: errorResponse => console.log(errorResponse), + }); + + return ( + +
+ {/* Left Section (Login Box) */} +
+
+

Log in to your account

+ {/* Email Input */} +
+ + +
+ {/* Password Input */} +
+ + +
+ {/* Login Button */} + +
OR
+ {/* Login with Google Button */} + + {/* Forgot Password Link */} + +
+
+ + {/* Right Section (Blurred Image Background) */} +
+
+ +
+ Text Overlay +
+
+
+ + ); +} + +export default LoginPage; From 0ce32b961371bb6b5bb53f1dd3dcd157990c4c37 Mon Sep 17 00:00:00 2001 From: sosokker Date: Sun, 5 Nov 2023 18:59:04 +0700 Subject: [PATCH 5/7] Add JWT expiration --- backend/core/settings.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/backend/core/settings.py b/backend/core/settings.py index cb43b70..141f023 100644 --- a/backend/core/settings.py +++ b/backend/core/settings.py @@ -10,6 +10,7 @@ For the full list of settings and their values, see https://docs.djangoproject.com/en/4.2/ref/settings/ """ +from datetime import timedelta import os from pathlib import Path from decouple import config, Csv @@ -80,6 +81,11 @@ REST_FRAMEWORK = { REST_USE_JWT = True +SIMPLE_JWT = { + 'ACCESS_TOKEN_LIFETIME': timedelta(days=3), + 'REFRESH_TOKEN_LIFETIME': timedelta(days=30), +} + GOOGLE_CLIENT_ID = config('GOOGLE_CLIENT_ID', default='fake-client-id') GOOGLE_CLIENT_SECRET = config('GOOGLE_CLIENT_SECRET', default='fake-client-secret') From 47d4d97ce781d39fba02a28d9dc49d70dee93359 Mon Sep 17 00:00:00 2001 From: sosokker Date: Sun, 5 Nov 2023 19:01:05 +0700 Subject: [PATCH 6/7] Add Profile update page --- backend/users/serializers.py | 15 ++- backend/users/views.py | 10 +- frontend/src/App.jsx | 6 +- frontend/src/api/UserProfileApi.jsx | 21 ++++ frontend/src/components/ProfileUpdatePage.jsx | 107 ++++++++++++++++++ 5 files changed, 151 insertions(+), 8 deletions(-) create mode 100644 frontend/src/api/UserProfileApi.jsx create mode 100644 frontend/src/components/ProfileUpdatePage.jsx diff --git a/backend/users/serializers.py b/backend/users/serializers.py index 531d912..962d789 100644 --- a/backend/users/serializers.py +++ b/backend/users/serializers.py @@ -31,6 +31,19 @@ class UpdateProfileSerializer(serializers.ModelSerializer): """ Serializer for updating user profile. """ + profile_pic = serializers.ImageField(required=False) + first_name = serializers.CharField(max_length=255, required=False) + about = serializers.CharField(required=False) + class Meta: model = CustomUser - fields = ('profile_pic', 'first_name', 'about') \ No newline at end of file + fields = ('profile_pic', 'first_name', 'about') + + def update(self, instance, validated_data): + """ + Update an existing user's profile. + """ + for attr, value in validated_data.items(): + setattr(instance, attr, value) + instance.save() + return instance \ No newline at end of file diff --git a/backend/users/views.py b/backend/users/views.py index 0cebbae..af201b7 100644 --- a/backend/users/views.py +++ b/backend/users/views.py @@ -8,7 +8,7 @@ from rest_framework.views import APIView from rest_framework.parsers import MultiPartParser from users.serializers import CustomUserSerializer, UpdateProfileSerializer - +from users.models import CustomUser class CustomUserCreate(APIView): """ @@ -49,8 +49,12 @@ class CustomUserProfileUpdate(APIView): return Response(data) def post(self, request): - serializer = UpdateProfileSerializer(data=request.data) + if not CustomUser.objects.filter(email=request.user.email).exists(): + return Response ({ + 'error': 'User does not exist' + }, status=status.HTTP_404_NOT_FOUND) + serializer = UpdateProfileSerializer(request.user, data=request.data) if serializer.is_valid(): serializer.save() return Response(serializer.data) - return Response(serializer.errors, status=400) \ No newline at end of file + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) \ No newline at end of file diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index c18349d..e9bb13b 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -2,11 +2,11 @@ import './App.css'; import { BrowserRouter, Route, Routes, Link } from 'react-router-dom'; import TestAuth from './components/testAuth'; -import IconSideNav from './components/IconSideNav'; import LoginPage from './components/authentication/LoginPage'; import SignUpPage from './components/authentication/SignUpPage'; import NavBar from './components/Nav/Navbar'; import Home from './components/Home'; +import ProfileUpdate from './components/ProfileUpdatePage' const App = () => { return ( @@ -18,11 +18,9 @@ const App = () => { }/> }/> }/> + }/> - {/*
- -
*/}
); } diff --git a/frontend/src/api/UserProfileApi.jsx b/frontend/src/api/UserProfileApi.jsx new file mode 100644 index 0000000..ca80fd1 --- /dev/null +++ b/frontend/src/api/UserProfileApi.jsx @@ -0,0 +1,21 @@ +import axios from 'axios'; + +const ApiUpdateUserProfile = async (formData) => { + try { + const response = await axios.post('http://127.0.1:8000/api/user/update/', formData, { + headers: { + 'Authorization': "Bearer " + localStorage.getItem('access_token'), + 'Content-Type': 'multipart/form-data', + }, + }); + + console.log(response.data); + + return response.data; + } catch (error) { + console.error('Error updating user profile:', error); + throw error; + } +}; + +export { ApiUpdateUserProfile }; diff --git a/frontend/src/components/ProfileUpdatePage.jsx b/frontend/src/components/ProfileUpdatePage.jsx new file mode 100644 index 0000000..06a0213 --- /dev/null +++ b/frontend/src/components/ProfileUpdatePage.jsx @@ -0,0 +1,107 @@ +import React, { useState, useRef } from 'react'; +import { ApiUpdateUserProfile } from '../api/UserProfileApi'; + +function ProfileUpdate() { + const [file, setFile] = useState(null); + const [username, setUsername] = useState(''); + const [fullName, setFullName] = useState(''); + const [about, setAbout] = useState(''); + const defaultImage = 'https://i1.sndcdn.com/artworks-cTz48e4f1lxn5Ozp-L3hopw-t500x500.jpg'; + const fileInputRef = useRef(null); + + const handleImageUpload = () => { + if (fileInputRef.current) { + fileInputRef.current.click(); + } + }; + + const handleFileChange = (e) => { + const selectedFile = e.target.files[0]; + if (selectedFile) { + setFile(selectedFile); + } + }; + + const handleSave = () => { + const formData = new FormData(); + formData.append('profile_pic', file); + formData.append('first_name', username); + formData.append('about', about); + + ApiUpdateUserProfile(formData); + }; + + return ( +
+ {/* Profile Image */} +
+ +
+ {file ? ( + Profile + ) : ( + <> + Default + + + + )} +
+
+ + {/* Username Field */} +
+ + setUsername(e.target.value)} + /> +
+ + {/* Full Name Field */} +
+ + setFullName(e.target.value)} + /> +
+ + {/* About Field */} +
+ +