diff --git a/api/admin.py b/api/admin.py index 8c38f3f3..dd761dd2 100644 --- a/api/admin.py +++ b/api/admin.py @@ -1,3 +1,36 @@ from django.contrib import admin +from django.db import models +from django.contrib.auth.models import Group, User +from django.contrib.auth.admin import UserAdmin +from .models import Order, Profile -# Register your models here. +admin.site.unregister(Group) +admin.site.unregister(User) + +class ProfileInline(admin.StackedInline): + model = Profile + can_delete = False + fields = ('avatar_tag',) + readonly_fields = ['avatar_tag'] + +# extended users with avatars +@admin.register(User) +class EUserAdmin(UserAdmin): + inlines = [ProfileInline] + list_display = ('avatar_tag',) + UserAdmin.list_display + list_display_links = ['username'] + def avatar_tag(self, obj): + return obj.profile.avatar_tag() + +@admin.register(Order) +class OrderAdmin(admin.ModelAdmin): + list_display = ('id','type','maker','taker','status','amount','currency','created_at','expires_at') + list_display_links = ('maker','taker') + pass + +@admin.register(Profile) +class UserProfileAdmin(admin.ModelAdmin): + list_display = ('avatar_tag','user','id','total_ratings','avg_rating','num_disputes','lost_disputes') + list_display_links =['user'] + readonly_fields = ['avatar_tag'] + pass \ No newline at end of file diff --git a/api/models.py b/api/models.py index 4ca3e704..150c4c35 100644 --- a/api/models.py +++ b/api/models.py @@ -1,11 +1,16 @@ from django.db import models from django.contrib.auth.models import User -from django.core.validators import MaxValueValidator, MinValueValidator +from django.core.validators import MaxValueValidator, MinValueValidator, validate_comma_separated_integer_list +from django.db.models.signals import post_save, pre_delete +from django.dispatch import receiver + +from django.utils.html import mark_safe + +from pathlib import Path ############################# # TODO # Load hparams from .env file - min_satoshis_trade = 10*1000 max_satoshis_trade = 500*1000 @@ -45,6 +50,7 @@ class Order(models.Model): # order info, id = models.CharField(max_length=64, unique=True, null=False) status = models.PositiveSmallIntegerField(choices=Status.choices, default=Status.WFB) created_at = models.DateTimeField(auto_now_add=True) + expires_at = models.DateTimeField() # order details type = models.PositiveSmallIntegerField(choices=Types.choices, null=False) @@ -56,7 +62,7 @@ class Order(models.Model): is_explicit = models.BooleanField(default=False, null=False) # pricing method. A explicit amount of sats, or a relative premium above/below market. # order participants - maker = models.ForeignKey(User, related_name='maker', on_delete=models.SET_NULL, null=True, default=None) # unique = True, a maker can only make one order + maker = models.ForeignKey(User, related_name='maker', on_delete=models.CASCADE, null=True, default=None) # unique = True, a maker can only make one order taker = models.ForeignKey(User, related_name='taker', on_delete=models.SET_NULL, null=True, default=None) # unique = True, a taker can only take one order # order collateral @@ -71,3 +77,40 @@ class Order(models.Model): # buyer payment LN invoice has_invoice = models.BooleanField(default=False, null=False) # has invoice and is valid invoice = models.CharField(max_length=300, unique=False, null=True, default=None) + +class Profile(models.Model): + user = models.OneToOneField(User,on_delete=models.CASCADE) + + # Ratings stored as a comma separated integer list + total_ratings = models.PositiveIntegerField(null=False, default=0) + latest_ratings = models.CharField(max_length=999, null=True, default=None, validators=[validate_comma_separated_integer_list]) # Will only store latest ratings + avg_rating = models.DecimalField(max_digits=4, decimal_places=1, default=None, null=True, validators=[MinValueValidator(0), MaxValueValidator(100)]) + + # Disputes + num_disputes = models.PositiveIntegerField(null=False, default=0) + lost_disputes = models.PositiveIntegerField(null=False, default=0) + + # RoboHash + avatar = models.ImageField(default="static/assets/avatars/unknown.png", verbose_name='Avatar') + + @receiver(post_save, sender=User) + def create_user_profile(sender, instance, created, **kwargs): + if created: + Profile.objects.create(user=instance) + + @receiver(post_save, sender=User) + def save_user_profile(sender, instance, **kwargs): + instance.profile.save() + + def __str__(self): + return self.user.username + + # to display avatars in admin panel + def get_avatar(self): + if not self.avatar: + return 'static/assets/avatars/unknown.png' + return self.avatar.url + + # method to create a fake table field in read only mode + def avatar_tag(self): + return mark_safe('' % self.get_avatar()) diff --git a/api/serializers.py b/api/serializers.py index 7fa68069..b73cb157 100644 --- a/api/serializers.py +++ b/api/serializers.py @@ -4,7 +4,7 @@ from .models import Order class OrderSerializer(serializers.ModelSerializer): class Meta: model = Order - fields = ('id','status','created_at','type','currency','amount','payment_method','is_explicit','premium','satoshis','maker','taker') + fields = ('id','status','created_at','expires_at','type','currency','amount','payment_method','is_explicit','premium','satoshis','maker','taker') class MakeOrderSerializer(serializers.ModelSerializer): class Meta: diff --git a/api/urls.py b/api/urls.py index 27f39862..1af71120 100644 --- a/api/urls.py +++ b/api/urls.py @@ -1,8 +1,9 @@ from django.urls import path -from .views import MakeOrder, OrderView, UserGenerator +from .views import MakeOrder, OrderView, UserGenerator, BookView urlpatterns = [ path('make/', MakeOrder.as_view()), path('order/', OrderView.as_view()), path('usergen/', UserGenerator.as_view()), + path('book/', BookView.as_view()), ] \ No newline at end of file diff --git a/api/views.py b/api/views.py index 784cd096..e7accd36 100644 --- a/api/views.py +++ b/api/views.py @@ -3,6 +3,7 @@ from rest_framework.views import APIView from rest_framework.response import Response from django.contrib.auth import authenticate, login, logout from django.contrib.auth.models import User +from django.conf.urls.static import static from .serializers import OrderSerializer, MakeOrderSerializer from .models import Order @@ -17,6 +18,12 @@ from pathlib import Path from datetime import timedelta from django.utils import timezone +# .env +expiration_time = 8 + +avatar_path = Path('frontend/static/assets/avatars') +avatar_path.mkdir(parents=True, exist_ok=True) + # Create your views here. class MakeOrder(APIView): @@ -51,6 +58,7 @@ class MakeOrder(APIView): premium=premium, satoshis=satoshis, is_explicit=is_explicit, + expires_at= timezone.now()+timedelta(hours=expiration_time), maker=request.user) order.save() @@ -81,7 +89,7 @@ class OrderView(APIView): #To do fix: data['status_message'] = Order.Status.get(order.status).label data['status_message'] = Order.Status.WFB.label # Hardcoded WFB, should use order.status value. - + data['maker_nick'] = str(order.maker) data['taker_nick'] = str(order.taker) @@ -96,7 +104,6 @@ class OrderView(APIView): return Response({'Bad Request':'Order ID parameter not found in request'}, status=status.HTTP_400_BAD_REQUEST) - class UserGenerator(APIView): lookup_url_kwarg = 'token' NickGen = NickGenerator( @@ -106,7 +113,7 @@ class UserGenerator(APIView): use_noun=True, max_num=999) - def get(self,request): + def get(self,request, format=None): ''' Get a new user derived from a high entropy token @@ -139,18 +146,20 @@ class UserGenerator(APIView): # generate avatar rh = Robohash(hash) - rh.assemble(roboset='set1') # bgset='any' for backgrounds ON + rh.assemble(roboset='set1', bgset='any')# for backgrounds ON - avatars_path = Path('frontend/static/assets/avatars') - avatars_path.mkdir(parents=True, exist_ok=True) - - with open(avatars_path.joinpath(nickname+".png"), "wb") as f: - rh.img.save(f, format="png") + # Does not replace image if existing (avoid re-avatar in case of nick collusion) - # Create new credentials if nickname is new + image_path = avatar_path.joinpath(nickname+".png") + if not image_path.exists(): + with open(image_path, "wb") as f: + rh.img.save(f, format="png") + + # Create new credentials and logsin if nickname is new if len(User.objects.filter(username=nickname)) == 0: User.objects.create_user(username=nickname, password=token, is_staff=False) user = authenticate(request, username=nickname, password=token) + user.profile.avatar = str(image_path)[9:] # removes frontend/ from url (ugly, to be fixed) login(request, user) return Response(context, status=status.HTTP_201_CREATED) @@ -159,17 +168,15 @@ class UserGenerator(APIView): if user is not None: login(request, user) # Sends the welcome back message, only if created +30 mins ago - if request.user.date_joined < (timezone.now()-timedelta(minutes=1)): + if request.user.date_joined < (timezone.now()-timedelta(minutes=30)): context['found'] = 'We found your Robosat. Welcome back!' return Response(context, status=status.HTTP_202_ACCEPTED) else: - # It is unlikely (1/20 Billions) but maybe the nickname is taken + # It is unlikely, but maybe the nickname is taken (1 in 20 Billion change) context['found'] = 'Bad luck, this nickname is taken' context['bad_request'] = 'Enter a different token' return Response(context, status=status.HTTP_403_FORBIDDEN) - - def delete(self,request): user = User.objects.get(id = request.user.id) @@ -177,10 +184,40 @@ class UserGenerator(APIView): # However it might be a long time recovered user # Only delete if user live is < 5 minutes + # TODO check if user exists AND it is not a maker or taker! if user is not None: + avatar_file = avatar_path.joinpath(str(request.user)+".png") + avatar_file.unlink() # Unsafe if avatar does not exist. logout(request) user.delete() - return Response(status=status.HTTP_301_MOVED_PERMANENTLY) + + return Response({'user_deleted':'User deleted permanently'},status=status.HTTP_301_MOVED_PERMANENTLY) return Response(status=status.HTTP_403_FORBIDDEN) +class BookView(APIView): + serializer_class = OrderSerializer + + def get(self,request, format=None): + currency = request.GET.get('currency') + type = request.GET.get('type') + queryset = Order.objects.filter(currency=currency, type=type, status=0) # TODO status = 1 for orders that are Public + if len(queryset)== 0: + return Response({'not_found':'No orders found, be the first to make one'}, status=status.HTTP_404_NOT_FOUND) + + queryset = queryset.order_by('created_at') + book_data = [] + for order in queryset: + data = OrderSerializer(order).data + user = User.objects.filter(id=data['maker']) + if len(user) == 1: + data['maker_nick'] = user[0].username + # TODO avoid sending status and takers for book views + #data.pop('status','taker') + book_data.append(data) + + return Response(book_data, status=status.HTTP_200_OK) + + + + diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 4e559c3d..b5eb7116 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -191,7 +191,6 @@ "version": "7.16.7", "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.16.7.tgz", "integrity": "sha512-LVtS6TqjJHFc+nYeITRo6VLXve70xmq7wPhWTqDJusJEgGmkAACWwMiTNrvfoQo6hEhFwAIixNkvB0jPXDL8Wg==", - "dev": true, "requires": { "@babel/types": "^7.16.7" } @@ -558,7 +557,6 @@ "version": "7.16.7", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.16.7.tgz", "integrity": "sha512-Esxmk7YjA8QysKeT3VhTXvF6y77f/a91SIs4pWb4H2eWGQkCKFgQaG6hdoEVZtGsrAcb2K5BW66XsOErD4WU3Q==", - "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.16.7" } @@ -1149,11 +1147,132 @@ "integrity": "sha512-ws57AidsDvREKrZKYffXddNkyaF14iHNHm8VQnZH6t99E8gczjNN0GpvcGny0imC80yQ0tHz1xVUKk/KFQSUyA==", "dev": true }, + "@emotion/babel-plugin": { + "version": "11.7.2", + "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.7.2.tgz", + "integrity": "sha512-6mGSCWi9UzXut/ZAN6lGFu33wGR3SJisNl3c0tvlmb8XChH1b2SUvxvnOh7hvLpqyRdHHU9AiazV3Cwbk5SXKQ==", + "requires": { + "@babel/helper-module-imports": "^7.12.13", + "@babel/plugin-syntax-jsx": "^7.12.13", + "@babel/runtime": "^7.13.10", + "@emotion/hash": "^0.8.0", + "@emotion/memoize": "^0.7.5", + "@emotion/serialize": "^1.0.2", + "babel-plugin-macros": "^2.6.1", + "convert-source-map": "^1.5.0", + "escape-string-regexp": "^4.0.0", + "find-root": "^1.1.0", + "source-map": "^0.5.7", + "stylis": "4.0.13" + }, + "dependencies": { + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==" + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + } + } + }, + "@emotion/cache": { + "version": "11.7.1", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.7.1.tgz", + "integrity": "sha512-r65Zy4Iljb8oyjtLeCuBH8Qjiy107dOYC6SJq7g7GV5UCQWMObY4SJDPGFjiiVpPrOJ2hmJOoBiYTC7hwx9E2A==", + "requires": { + "@emotion/memoize": "^0.7.4", + "@emotion/sheet": "^1.1.0", + "@emotion/utils": "^1.0.0", + "@emotion/weak-memoize": "^0.2.5", + "stylis": "4.0.13" + } + }, "@emotion/hash": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz", "integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==" }, + "@emotion/is-prop-valid": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.1.1.tgz", + "integrity": "sha512-bW1Tos67CZkOURLc0OalnfxtSXQJMrAMV0jZTVGJUPSOd4qgjF3+tTD5CwJM13PHA8cltGW1WGbbvV9NpvUZPw==", + "requires": { + "@emotion/memoize": "^0.7.4" + } + }, + "@emotion/memoize": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.5.tgz", + "integrity": "sha512-igX9a37DR2ZPGYtV6suZ6whr8pTFtyHL3K/oLUotxpSVO2ASaprmAe2Dkq7tBo7CRY7MMDrAa9nuQP9/YG8FxQ==" + }, + "@emotion/react": { + "version": "11.7.1", + "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.7.1.tgz", + "integrity": "sha512-DV2Xe3yhkF1yT4uAUoJcYL1AmrnO5SVsdfvu+fBuS7IbByDeTVx9+wFmvx9Idzv7/78+9Mgx2Hcmr7Fex3tIyw==", + "requires": { + "@babel/runtime": "^7.13.10", + "@emotion/cache": "^11.7.1", + "@emotion/serialize": "^1.0.2", + "@emotion/sheet": "^1.1.0", + "@emotion/utils": "^1.0.0", + "@emotion/weak-memoize": "^0.2.5", + "hoist-non-react-statics": "^3.3.1" + } + }, + "@emotion/serialize": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.0.2.tgz", + "integrity": "sha512-95MgNJ9+/ajxU7QIAruiOAdYNjxZX7G2mhgrtDWswA21VviYIRP1R5QilZ/bDY42xiKsaktP4egJb3QdYQZi1A==", + "requires": { + "@emotion/hash": "^0.8.0", + "@emotion/memoize": "^0.7.4", + "@emotion/unitless": "^0.7.5", + "@emotion/utils": "^1.0.0", + "csstype": "^3.0.2" + }, + "dependencies": { + "csstype": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.10.tgz", + "integrity": "sha512-2u44ZG2OcNUO9HDp/Jl8C07x6pU/eTR3ncV91SiK3dhG9TWvRVsCoJw14Ckx5DgWkzGA3waZWO3d7pgqpUI/XA==" + } + } + }, + "@emotion/sheet": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.1.0.tgz", + "integrity": "sha512-u0AX4aSo25sMAygCuQTzS+HsImZFuS8llY8O7b9MDRzbJM0kVJlAz6KNDqcG7pOuQZJmj/8X/rAW+66kMnMW+g==" + }, + "@emotion/styled": { + "version": "11.6.0", + "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.6.0.tgz", + "integrity": "sha512-mxVtVyIOTmCAkFbwIp+nCjTXJNgcz4VWkOYQro87jE2QBTydnkiYusMrRGFtzuruiGK4dDaNORk4gH049iiQuw==", + "requires": { + "@babel/runtime": "^7.13.10", + "@emotion/babel-plugin": "^11.3.0", + "@emotion/is-prop-valid": "^1.1.1", + "@emotion/serialize": "^1.0.2", + "@emotion/utils": "^1.0.0" + } + }, + "@emotion/unitless": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz", + "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==" + }, + "@emotion/utils": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.0.0.tgz", + "integrity": "sha512-mQC2b3XLDs6QCW+pDQDiyO/EdGZYOygE8s5N5rrzjSI4M3IejPE/JPndCBwRT9z982aqQNi6beWs1UeayrQxxA==" + }, + "@emotion/weak-memoize": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.2.5.tgz", + "integrity": "sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA==" + }, "@material-ui/core": { "version": "4.12.3", "resolved": "https://registry.npmjs.org/@material-ui/core/-/core-4.12.3.tgz", @@ -1230,6 +1349,65 @@ "react-is": "^16.8.0 || ^17.0.0" } }, + "@mui/private-theming": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.2.3.tgz", + "integrity": "sha512-Lc1Cmu8lSsYZiXADi9PBb17Ho82ZbseHQujUFAcp6bCJ5x/d+87JYCIpCBMagPu/isRlFCwbziuXPmz7WOzJPQ==", + "requires": { + "@babel/runtime": "^7.16.3", + "@mui/utils": "^5.2.3", + "prop-types": "^15.7.2" + } + }, + "@mui/styled-engine": { + "version": "5.2.6", + "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.2.6.tgz", + "integrity": "sha512-bqAhli8eGS6v2qxivy2/4K0Ag8o//jsu1G2G6QcieFiT6y7oIF/nd/6Tvw6OSm3roOTifVQWNKwkt1yFWhGS+w==", + "requires": { + "@babel/runtime": "^7.16.3", + "@emotion/cache": "^11.7.1", + "prop-types": "^15.7.2" + } + }, + "@mui/system": { + "version": "5.2.6", + "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.2.6.tgz", + "integrity": "sha512-PZ7bmpWOLikWgqn2zWv9/Xa7lxnRBOmfjoMH7c/IVYJs78W3971brXJ3xV9MEWWQcoqiYQeXzUJaNf4rFbKCBA==", + "requires": { + "@babel/runtime": "^7.16.3", + "@mui/private-theming": "^5.2.3", + "@mui/styled-engine": "^5.2.6", + "@mui/types": "^7.1.0", + "@mui/utils": "^5.2.3", + "clsx": "^1.1.1", + "csstype": "^3.0.10", + "prop-types": "^15.7.2" + }, + "dependencies": { + "csstype": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.10.tgz", + "integrity": "sha512-2u44ZG2OcNUO9HDp/Jl8C07x6pU/eTR3ncV91SiK3dhG9TWvRVsCoJw14Ckx5DgWkzGA3waZWO3d7pgqpUI/XA==" + } + } + }, + "@mui/types": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.1.0.tgz", + "integrity": "sha512-Hh7ALdq/GjfIwLvqH3XftuY3bcKhupktTm+S6qRIDGOtPtRuq2L21VWzOK4p7kblirK0XgGVH5BLwa6u8z/6QQ==" + }, + "@mui/utils": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.2.3.tgz", + "integrity": "sha512-sQujlajIS0zQKcGIS6tZR0L1R+ib26B6UtuEn+cZqwKHsPo3feuS+SkdscYBdcCdMbrZs4gj8WIJHl2z6tbSzQ==", + "requires": { + "@babel/runtime": "^7.16.3", + "@types/prop-types": "^15.7.4", + "@types/react-is": "^16.7.1 || ^17.0.0", + "prop-types": "^15.7.2", + "react-is": "^17.0.2" + } + }, "@types/eslint": { "version": "8.2.1", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.2.1.tgz", @@ -1268,6 +1446,11 @@ "integrity": "sha512-+XBAjfZmmivILUzO0HwBJoYkAyyySSLg5KCGBDFLomJo0sV6szvVLAf4ANZZ0pfWzgEds5KmGLG9D5hfEqOhaA==", "dev": true }, + "@types/parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==" + }, "@types/prop-types": { "version": "15.7.4", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.4.tgz", @@ -1290,6 +1473,14 @@ } } }, + "@types/react-is": { + "version": "17.0.3", + "resolved": "https://registry.npmjs.org/@types/react-is/-/react-is-17.0.3.tgz", + "integrity": "sha512-aBTIWg1emtu95bLTLx0cpkxwGW3ueZv71nE2YFBpL8k/z5czEW8yYpOo8Dp+UUAFAtKwNaOsh/ioSeQnWlZcfw==", + "requires": { + "@types/react": "*" + } + }, "@types/react-transition-group": { "version": "4.4.4", "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.4.tgz", @@ -1554,6 +1745,16 @@ "object.assign": "^4.1.0" } }, + "babel-plugin-macros": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-2.8.0.tgz", + "integrity": "sha512-SEP5kJpfGYqYKpBrj5XU3ahw5p5GOHJ0U5ssOSQ/WBVdwkD2Dzlce95exQTs3jOVWPPKLBN2rlEWkCK7dSmLvg==", + "requires": { + "@babel/runtime": "^7.7.2", + "cosmiconfig": "^6.0.0", + "resolve": "^1.12.0" + } + }, "babel-plugin-polyfill-corejs2": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.3.0.tgz", @@ -1619,6 +1820,11 @@ "get-intrinsic": "^1.0.2" } }, + "callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==" + }, "caniuse-lite": { "version": "1.0.30001294", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001294.tgz", @@ -1707,7 +1913,6 @@ "version": "1.8.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.8.0.tgz", "integrity": "sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA==", - "dev": true, "requires": { "safe-buffer": "~5.1.1" }, @@ -1715,8 +1920,7 @@ "safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" } } }, @@ -1738,6 +1942,18 @@ } } }, + "cosmiconfig": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-6.0.0.tgz", + "integrity": "sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg==", + "requires": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.1.0", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.7.2" + } + }, "cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -1824,6 +2040,14 @@ "integrity": "sha512-/o+BXHmB7ocbHEAs6F2EnG0ogybVVUdkRunTT2glZU9XAaGmhqskrvKwqXuDfNjEO0LZKWdejEEpnq8aM0tOaw==", "dev": true }, + "error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "requires": { + "is-arrayish": "^0.2.1" + } + }, "es-module-lexer": { "version": "0.9.3", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-0.9.3.tgz", @@ -1932,6 +2156,11 @@ "pkg-dir": "^4.1.0" } }, + "find-root": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==" + }, "find-up": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", @@ -1945,8 +2174,7 @@ "function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" }, "gensync": { "version": "1.0.0-beta.2", @@ -1992,7 +2220,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dev": true, "requires": { "function-bind": "^1.1.1" } @@ -2048,6 +2275,22 @@ "resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.0.4.tgz", "integrity": "sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ==" }, + "import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "requires": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "dependencies": { + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==" + } + } + }, "import-local": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.0.3.tgz", @@ -2064,11 +2307,15 @@ "integrity": "sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw==", "dev": true }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=" + }, "is-core-module": { "version": "2.8.0", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.0.tgz", "integrity": "sha512-vd15qHsaqrRL7dtH6QNuy0ndJmRDrS9HAM1CAiSifNUFv4x1a0CCVsj18hJ1mShxIG6T2i1sO78MkP56r0nYRw==", - "dev": true, "requires": { "has": "^1.0.3" } @@ -2137,6 +2384,11 @@ "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", "dev": true }, + "json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" + }, "json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -2243,6 +2495,11 @@ "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", "dev": true }, + "lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" + }, "loader-runner": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.2.0.tgz", @@ -2435,6 +2692,25 @@ "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", "dev": true }, + "parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "requires": { + "callsites": "^3.0.0" + } + }, + "parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "requires": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + } + }, "path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -2450,8 +2726,7 @@ "path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" }, "path-to-regexp": { "version": "1.8.0", @@ -2461,6 +2736,11 @@ "isarray": "0.0.1" } }, + "path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==" + }, "picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", @@ -2667,7 +2947,6 @@ "version": "1.20.0", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz", "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==", - "dev": true, "requires": { "is-core-module": "^2.2.0", "path-parse": "^1.0.6" @@ -2787,6 +3066,11 @@ "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", "dev": true }, + "stylis": { + "version": "4.0.13", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.0.13.tgz", + "integrity": "sha512-xGPXiFVl4YED9Jh7Euv2V220mriG9u4B2TA6Ybjc1catrstKD2PpIdU3U0RKpkVBC2EhmL/F0sPCr9vrFTNRag==" + }, "supports-color": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", @@ -2991,6 +3275,11 @@ "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.0.tgz", "integrity": "sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw==", "dev": true + }, + "yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==" } } } diff --git a/frontend/package.json b/frontend/package.json index 280eda3e..f687e275 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -22,8 +22,11 @@ }, "dependencies": { "@babel/plugin-proposal-class-properties": "^7.16.7", + "@emotion/react": "^11.7.1", + "@emotion/styled": "^11.6.0", "@material-ui/core": "^4.12.3", "@material-ui/icons": "^4.11.2", + "@mui/system": "^5.2.6", "material-ui-image": "^3.3.2", "react-router-dom": "^5.2.0" } diff --git a/frontend/src/components/BookPage.js b/frontend/src/components/BookPage.js index 09a2970b..13f6ef3b 100644 --- a/frontend/src/components/BookPage.js +++ b/frontend/src/components/BookPage.js @@ -1,11 +1,173 @@ import React, { Component } from "react"; +import { Paper, Button , Divider, Card, CardActionArea, CardContent, Typography, Grid, Select, MenuItem, FormControl, FormHelperText, List, ListItem, ListItemText, Avatar, Link, RouterLink, ListItemAvatar} from "@material-ui/core" export default class BookPage extends Component { - constructor(props) { - super(props); - } + constructor(props) { + super(props); + this.state = { + orders: new Array(), + currency: 1, + type: 1, + }; + this.getOrderDetails() + this.state.currencyCode = this.getCurrencyCode(this.state.currency) + } - render() { - return

This is the order book page

; - } + // Fix needed to handle HTTP 404 error when no order is found + // Show message to be the first one to make an order + getOrderDetails() { + fetch('/api/book' + '?currency=' + this.state.currency + "&type=" + this.state.type) + .then((response) => response.json()) + .then((data) => //console.log(data)); + this.setState({orders: data})); + } + + handleCardClick=(e)=>{ + console.log(e.target) + this.props.history.push('/order/' + e.target); + } + + // Make these two functions sequential. getOrderDetails needs setState to be finish beforehand. + handleTypeChange=(e)=>{ + this.setState({ + type: e.target.value, + }); + this.getOrderDetails(); + } + handleCurrencyChange=(e)=>{ + this.setState({ + currency: e.target.value, + currencyCode: this.getCurrencyCode(e.target.value), + }) + this.getOrderDetails(); + } + + // Gets currency code (3 letters) from numeric (e.g., 1 -> USD) + // Improve this function so currencies are read from json + getCurrencyCode(val){ + return (val == 1 ) ? "USD": ((val == 2 ) ? "EUR":"ETH") + } + + // pretty numbers + pn(x) { + return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ","); + } + + render() { + return ( + + + + Order Book + + + + + + + I want to + + + + + + + + + And pay with + + + + + {this.state.orders.map((order) => + + + + {/* To fix! does not pass order.id to handleCardCLick. Instead passes the clicked */} + + + + + + + + + + + {order.maker_nick} + + + + + {/* CARD PARAGRAPH CONTENT */} + + + ◑{order.type == 0 ? Buys : Sells } + {parseFloat(parseFloat(order.amount).toFixed(4))} + {" " +this.getCurrencyCode(order.currency)} worth of bitcoin + + + + ◑ Payment via {order.payment_method} + + + + ◑ Priced {order.is_explicit ? + " explicitly at " + this.pn(order.satoshis) + " Sats" : ( + " at " + + parseFloat(parseFloat(order.premium).toFixed(4)) + "% over the market" + )} + + + + ◑ {" 42,354 "}{this.getCurrencyCode(order.currency)}/BTC (Binance API) + + + + + + + + + + )} + + + You are {this.state.type == 0 ? " selling " : " buying "} BTC for {this.state.currencyCode} + + + + + + + ); + }; } \ No newline at end of file diff --git a/frontend/src/components/MakerPage.js b/frontend/src/components/MakerPage.js index 2999ec83..61d38a46 100644 --- a/frontend/src/components/MakerPage.js +++ b/frontend/src/components/MakerPage.js @@ -1,5 +1,5 @@ import React, { Component } from 'react'; -import { Button , Grid, Typography, TextField, Select, FormHelperText, MenuItem, FormControl, Radio, FormControlLabel, RadioGroup, Menu} from "@material-ui/core" +import { Paper, Button , Grid, Typography, TextField, Select, FormHelperText, MenuItem, FormControl, Radio, FormControlLabel, RadioGroup, Menu} from "@material-ui/core" import { Link } from 'react-router-dom' function getCookie(name) { @@ -79,7 +79,7 @@ export default class MakerPage extends Component { premium: 0, }); } - handleClickisExplicit=(e)=>{ + handleClickExplicit=(e)=>{ this.setState({ isExplicit: true, satoshis: 10000, @@ -104,130 +104,116 @@ export default class MakerPage extends Component { }; fetch("/api/make/",requestOptions) .then((response) => response.json()) - .then((data) => this.props.history.push('/order/' + data.id)); + .then((data) => (console.log(data) & this.props.history.push('/order/' + data.id))); } render() { return ( - - Make an Order - - - - - - + + Make an Order + + + + + + +
+ Choose Buy or Sell Bitcoin +
+
+ + } + label="Buy" + labelPlacement="Top" + /> + } + label="Sell" + labelPlacement="Top" + /> + +
+
+ + + + + + + + + + + + + + + + } - label="Buy" + label="Relative" labelPlacement="Top" - /> - + } - label="Sell" + label="Explicit" labelPlacement="Top" - /> - - -
- Choose Buy or Sell Bitcoin -
-
-
-
- - - - -
- Select Payment Currency -
-
-
-
- - - - - -
- Amount of Fiat to Trade -
-
-
- - - - -
- Enter the Payment Method(s) -
-
-
-
- - - - } - label="Relative" - labelPlacement="Top" - onClick={this.handleClickRelative} - /> - } - label="Explicit" - labelPlacement="Top" - onClick={this.handleClickisExplicit} - onShow="false" - /> - - -
- Choose a Pricing Method -
-
-
-
-{/* conditional shows either Premium % field or Satoshis field based on pricing method */} - { this.state.isExplicit - ? - - + + +
+ Choose a Pricing Method +
+
+
+
+ {/* conditional shows either Premium % field or Satoshis field based on pricing method */} + { this.state.isExplicit + ? + - -
- Explicit Amount in Satoshis -
-
- -
- : - + + : - -
- Premium Relative to Market Price (%) -
-
- -
- } +
+ } + + - @@ -277,14 +252,13 @@ export default class MakerPage extends Component { } - - - + + + - ); } } \ No newline at end of file diff --git a/frontend/src/components/OrderPage.js b/frontend/src/components/OrderPage.js index 351d273e..50c526fa 100644 --- a/frontend/src/components/OrderPage.js +++ b/frontend/src/components/OrderPage.js @@ -1,7 +1,12 @@ import React, { Component } from "react"; -import { Button , Grid, Typography, List, ListItem, ListItemText, ListItemAvatar, Avatar, Divider} from "@material-ui/core" +import { Paper, Button , Grid, Typography, List, ListItem, ListItemText, ListItemAvatar, Avatar, Divider} from "@material-ui/core" import { Link } from 'react-router-dom' +// pretty numbers +function pn(x) { + return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ","); +} + export default class OrderPage extends Component { constructor(props) { super(props); @@ -36,27 +41,31 @@ export default class OrderPage extends Component { }); } + // Fix to use proper react props + handleClickBackButton=()=>{ + window.history.back(); + } render (){ return ( - Robosat BTC {this.state.type ? " Sell " : " Buy "} Order + BTC {this.state.type ? " Sell " : " Buy "} Order + - + @@ -65,9 +74,9 @@ export default class OrderPage extends Component { {this.state.isExplicit ? - + : - + } @@ -93,6 +102,10 @@ export default class OrderPage extends Component { {this.state.isParticipant ? "" : } + + {this.state.isParticipant ? "" : } + + ); diff --git a/frontend/src/components/UserGenPage.js b/frontend/src/components/UserGenPage.js index 02e03074..bdab24d0 100644 --- a/frontend/src/components/UserGenPage.js +++ b/frontend/src/components/UserGenPage.js @@ -1,5 +1,5 @@ import React, { Component } from "react"; -import { Button , Grid, Typography, TextField, Select, FormHelperText, MenuItem, FormControl, Radio, FormControlLabel, RadioGroup, Menu} from "@material-ui/core" +import { Button , Grid, Typography, TextField, ButtonGroup} from "@material-ui/core" import { Link } from 'react-router-dom' import Image from 'material-ui-image' @@ -85,9 +85,39 @@ export default class UserGenPage extends Component { this.getGeneratedUser(); } + // TO FIX CSRF TOKEN IS NOT UPDATED UNTIL WINDOW IS RELOADED + reload_for_csrf_to_work=()=>{ + window.location.reload() + } + render() { return ( + + + {this.state.nickname ? "⚡"+this.state.nickname+"⚡" : ""} + + + +
+ +

+
+ { + this.state.found ? + + + {this.state.found}
+
+
+ : + "" + } -
- -
+ +
+ + + + + + - {this.state.nickname ? "⚡"+this.state.nickname+"⚡" : ""} - - - { - this.state.found ? - - - {this.state.found}
-
- -
- : - - - - } - - - + Easy and Private Lightning peer-to-peer Exchange +
); diff --git a/frontend/src/components/getCookieToken.js b/frontend/src/components/getCookieToken.js deleted file mode 100644 index 87568ccb..00000000 --- a/frontend/src/components/getCookieToken.js +++ /dev/null @@ -1,15 +0,0 @@ -function getCookie(name) { - let cookieValue = null; - if (document.cookie && document.cookie !== '') { - const cookies = document.cookie.split(';'); - for (let i = 0; i < cookies.length; i++) { - const cookie = cookies[i].trim(); - // Does this cookie string begin with the name we want? - if (cookie.substring(0, name.length + 1) === (name + '=')) { - cookieValue = decodeURIComponent(cookie.substring(name.length + 1)); - break; - } - } - } - return cookieValue; -} \ No newline at end of file diff --git a/setup.md b/setup.md index 8a1f1fdd..f3b0af5d 100644 --- a/setup.md +++ b/setup.md @@ -47,7 +47,7 @@ pip install robohash ### Install npm `sudo apt install npm` -No need to repeat, this is the list of npm packages we use +npm packages we use ``` cd frontend npm init -y @@ -59,6 +59,7 @@ npm install @babel/plugin-proposal-class-properties npm install react-router-dom@5.2.0 npm install @material-ui/icons npm install material-ui-image +npm install @mui/system @emotion/react @emotion/styled ``` ### Launch the React render