Merge pull request #19 from Reckless-Satoshi/order-book-first-iteration

Add book order API endpoint and basic frontend cards page
This commit is contained in:
Reckless_Satoshi 2022-01-07 11:55:22 +00:00 committed by GitHub
commit 7e89f57779
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 785 additions and 231 deletions

View File

@ -1,3 +1,36 @@
from django.contrib import admin 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

View File

@ -1,11 +1,16 @@
from django.db import models from django.db import models
from django.contrib.auth.models import User 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 # TODO
# Load hparams from .env file # Load hparams from .env file
min_satoshis_trade = 10*1000 min_satoshis_trade = 10*1000
max_satoshis_trade = 500*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) # order info, id = models.CharField(max_length=64, unique=True, null=False)
status = models.PositiveSmallIntegerField(choices=Status.choices, default=Status.WFB) status = models.PositiveSmallIntegerField(choices=Status.choices, default=Status.WFB)
created_at = models.DateTimeField(auto_now_add=True) created_at = models.DateTimeField(auto_now_add=True)
expires_at = models.DateTimeField()
# order details # order details
type = models.PositiveSmallIntegerField(choices=Types.choices, null=False) 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. is_explicit = models.BooleanField(default=False, null=False) # pricing method. A explicit amount of sats, or a relative premium above/below market.
# order participants # 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 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 # order collateral
@ -71,3 +77,40 @@ class Order(models.Model):
# buyer payment LN invoice # buyer payment LN invoice
has_invoice = models.BooleanField(default=False, null=False) # has invoice and is valid 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) 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('<img src="%s" width="50" height="50" />' % self.get_avatar())

View File

@ -4,7 +4,7 @@ from .models import Order
class OrderSerializer(serializers.ModelSerializer): class OrderSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = Order 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 MakeOrderSerializer(serializers.ModelSerializer):
class Meta: class Meta:

View File

@ -1,8 +1,9 @@
from django.urls import path from django.urls import path
from .views import MakeOrder, OrderView, UserGenerator from .views import MakeOrder, OrderView, UserGenerator, BookView
urlpatterns = [ urlpatterns = [
path('make/', MakeOrder.as_view()), path('make/', MakeOrder.as_view()),
path('order/', OrderView.as_view()), path('order/', OrderView.as_view()),
path('usergen/', UserGenerator.as_view()), path('usergen/', UserGenerator.as_view()),
path('book/', BookView.as_view()),
] ]

View File

@ -3,6 +3,7 @@ from rest_framework.views import APIView
from rest_framework.response import Response from rest_framework.response import Response
from django.contrib.auth import authenticate, login, logout from django.contrib.auth import authenticate, login, logout
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.conf.urls.static import static
from .serializers import OrderSerializer, MakeOrderSerializer from .serializers import OrderSerializer, MakeOrderSerializer
from .models import Order from .models import Order
@ -17,6 +18,12 @@ from pathlib import Path
from datetime import timedelta from datetime import timedelta
from django.utils import timezone 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. # Create your views here.
class MakeOrder(APIView): class MakeOrder(APIView):
@ -51,6 +58,7 @@ class MakeOrder(APIView):
premium=premium, premium=premium,
satoshis=satoshis, satoshis=satoshis,
is_explicit=is_explicit, is_explicit=is_explicit,
expires_at= timezone.now()+timedelta(hours=expiration_time),
maker=request.user) maker=request.user)
order.save() order.save()
@ -96,7 +104,6 @@ class OrderView(APIView):
return Response({'Bad Request':'Order ID parameter not found in request'}, status=status.HTTP_400_BAD_REQUEST) return Response({'Bad Request':'Order ID parameter not found in request'}, status=status.HTTP_400_BAD_REQUEST)
class UserGenerator(APIView): class UserGenerator(APIView):
lookup_url_kwarg = 'token' lookup_url_kwarg = 'token'
NickGen = NickGenerator( NickGen = NickGenerator(
@ -106,7 +113,7 @@ class UserGenerator(APIView):
use_noun=True, use_noun=True,
max_num=999) max_num=999)
def get(self,request): def get(self,request, format=None):
''' '''
Get a new user derived from a high entropy token Get a new user derived from a high entropy token
@ -139,18 +146,20 @@ class UserGenerator(APIView):
# generate avatar # generate avatar
rh = Robohash(hash) 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') # Does not replace image if existing (avoid re-avatar in case of nick collusion)
avatars_path.mkdir(parents=True, exist_ok=True)
with open(avatars_path.joinpath(nickname+".png"), "wb") as f: image_path = avatar_path.joinpath(nickname+".png")
rh.img.save(f, format="png") if not image_path.exists():
with open(image_path, "wb") as f:
rh.img.save(f, format="png")
# Create new credentials if nickname is new # Create new credentials and logsin if nickname is new
if len(User.objects.filter(username=nickname)) == 0: if len(User.objects.filter(username=nickname)) == 0:
User.objects.create_user(username=nickname, password=token, is_staff=False) User.objects.create_user(username=nickname, password=token, is_staff=False)
user = authenticate(request, username=nickname, password=token) 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) login(request, user)
return Response(context, status=status.HTTP_201_CREATED) return Response(context, status=status.HTTP_201_CREATED)
@ -159,17 +168,15 @@ class UserGenerator(APIView):
if user is not None: if user is not None:
login(request, user) login(request, user)
# Sends the welcome back message, only if created +30 mins ago # 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!' context['found'] = 'We found your Robosat. Welcome back!'
return Response(context, status=status.HTTP_202_ACCEPTED) return Response(context, status=status.HTTP_202_ACCEPTED)
else: 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['found'] = 'Bad luck, this nickname is taken'
context['bad_request'] = 'Enter a different token' context['bad_request'] = 'Enter a different token'
return Response(context, status=status.HTTP_403_FORBIDDEN) return Response(context, status=status.HTTP_403_FORBIDDEN)
def delete(self,request): def delete(self,request):
user = User.objects.get(id = request.user.id) user = User.objects.get(id = request.user.id)
@ -177,10 +184,40 @@ class UserGenerator(APIView):
# However it might be a long time recovered user # However it might be a long time recovered user
# Only delete if user live is < 5 minutes # 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: 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) logout(request)
user.delete() 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) 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)

View File

@ -191,7 +191,6 @@
"version": "7.16.7", "version": "7.16.7",
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.16.7.tgz", "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.16.7.tgz",
"integrity": "sha512-LVtS6TqjJHFc+nYeITRo6VLXve70xmq7wPhWTqDJusJEgGmkAACWwMiTNrvfoQo6hEhFwAIixNkvB0jPXDL8Wg==", "integrity": "sha512-LVtS6TqjJHFc+nYeITRo6VLXve70xmq7wPhWTqDJusJEgGmkAACWwMiTNrvfoQo6hEhFwAIixNkvB0jPXDL8Wg==",
"dev": true,
"requires": { "requires": {
"@babel/types": "^7.16.7" "@babel/types": "^7.16.7"
} }
@ -558,7 +557,6 @@
"version": "7.16.7", "version": "7.16.7",
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.16.7.tgz", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.16.7.tgz",
"integrity": "sha512-Esxmk7YjA8QysKeT3VhTXvF6y77f/a91SIs4pWb4H2eWGQkCKFgQaG6hdoEVZtGsrAcb2K5BW66XsOErD4WU3Q==", "integrity": "sha512-Esxmk7YjA8QysKeT3VhTXvF6y77f/a91SIs4pWb4H2eWGQkCKFgQaG6hdoEVZtGsrAcb2K5BW66XsOErD4WU3Q==",
"dev": true,
"requires": { "requires": {
"@babel/helper-plugin-utils": "^7.16.7" "@babel/helper-plugin-utils": "^7.16.7"
} }
@ -1149,11 +1147,132 @@
"integrity": "sha512-ws57AidsDvREKrZKYffXddNkyaF14iHNHm8VQnZH6t99E8gczjNN0GpvcGny0imC80yQ0tHz1xVUKk/KFQSUyA==", "integrity": "sha512-ws57AidsDvREKrZKYffXddNkyaF14iHNHm8VQnZH6t99E8gczjNN0GpvcGny0imC80yQ0tHz1xVUKk/KFQSUyA==",
"dev": true "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": { "@emotion/hash": {
"version": "0.8.0", "version": "0.8.0",
"resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz", "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz",
"integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==" "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": { "@material-ui/core": {
"version": "4.12.3", "version": "4.12.3",
"resolved": "https://registry.npmjs.org/@material-ui/core/-/core-4.12.3.tgz", "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" "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": { "@types/eslint": {
"version": "8.2.1", "version": "8.2.1",
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.2.1.tgz", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.2.1.tgz",
@ -1268,6 +1446,11 @@
"integrity": "sha512-+XBAjfZmmivILUzO0HwBJoYkAyyySSLg5KCGBDFLomJo0sV6szvVLAf4ANZZ0pfWzgEds5KmGLG9D5hfEqOhaA==", "integrity": "sha512-+XBAjfZmmivILUzO0HwBJoYkAyyySSLg5KCGBDFLomJo0sV6szvVLAf4ANZZ0pfWzgEds5KmGLG9D5hfEqOhaA==",
"dev": true "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": { "@types/prop-types": {
"version": "15.7.4", "version": "15.7.4",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.4.tgz", "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": { "@types/react-transition-group": {
"version": "4.4.4", "version": "4.4.4",
"resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.4.tgz", "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" "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": { "babel-plugin-polyfill-corejs2": {
"version": "0.3.0", "version": "0.3.0",
"resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.3.0.tgz", "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" "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": { "caniuse-lite": {
"version": "1.0.30001294", "version": "1.0.30001294",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001294.tgz", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001294.tgz",
@ -1707,7 +1913,6 @@
"version": "1.8.0", "version": "1.8.0",
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.8.0.tgz", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.8.0.tgz",
"integrity": "sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA==", "integrity": "sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA==",
"dev": true,
"requires": { "requires": {
"safe-buffer": "~5.1.1" "safe-buffer": "~5.1.1"
}, },
@ -1715,8 +1920,7 @@
"safe-buffer": { "safe-buffer": {
"version": "5.1.2", "version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
"dev": true
} }
} }
}, },
@ -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": { "cross-spawn": {
"version": "7.0.3", "version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
@ -1824,6 +2040,14 @@
"integrity": "sha512-/o+BXHmB7ocbHEAs6F2EnG0ogybVVUdkRunTT2glZU9XAaGmhqskrvKwqXuDfNjEO0LZKWdejEEpnq8aM0tOaw==", "integrity": "sha512-/o+BXHmB7ocbHEAs6F2EnG0ogybVVUdkRunTT2glZU9XAaGmhqskrvKwqXuDfNjEO0LZKWdejEEpnq8aM0tOaw==",
"dev": true "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": { "es-module-lexer": {
"version": "0.9.3", "version": "0.9.3",
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-0.9.3.tgz", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-0.9.3.tgz",
@ -1932,6 +2156,11 @@
"pkg-dir": "^4.1.0" "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": { "find-up": {
"version": "4.1.0", "version": "4.1.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
@ -1945,8 +2174,7 @@
"function-bind": { "function-bind": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
"dev": true
}, },
"gensync": { "gensync": {
"version": "1.0.0-beta.2", "version": "1.0.0-beta.2",
@ -1992,7 +2220,6 @@
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
"integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
"dev": true,
"requires": { "requires": {
"function-bind": "^1.1.1" "function-bind": "^1.1.1"
} }
@ -2048,6 +2275,22 @@
"resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.0.4.tgz", "resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.0.4.tgz",
"integrity": "sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ==" "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": { "import-local": {
"version": "3.0.3", "version": "3.0.3",
"resolved": "https://registry.npmjs.org/import-local/-/import-local-3.0.3.tgz", "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.0.3.tgz",
@ -2064,11 +2307,15 @@
"integrity": "sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw==", "integrity": "sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw==",
"dev": true "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": { "is-core-module": {
"version": "2.8.0", "version": "2.8.0",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.0.tgz", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.0.tgz",
"integrity": "sha512-vd15qHsaqrRL7dtH6QNuy0ndJmRDrS9HAM1CAiSifNUFv4x1a0CCVsj18hJ1mShxIG6T2i1sO78MkP56r0nYRw==", "integrity": "sha512-vd15qHsaqrRL7dtH6QNuy0ndJmRDrS9HAM1CAiSifNUFv4x1a0CCVsj18hJ1mShxIG6T2i1sO78MkP56r0nYRw==",
"dev": true,
"requires": { "requires": {
"has": "^1.0.3" "has": "^1.0.3"
} }
@ -2137,6 +2384,11 @@
"integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==",
"dev": true "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": { "json-schema-traverse": {
"version": "0.4.1", "version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "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==", "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==",
"dev": true "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": { "loader-runner": {
"version": "4.2.0", "version": "4.2.0",
"resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.2.0.tgz", "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.2.0.tgz",
@ -2435,6 +2692,25 @@
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
"dev": true "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": { "path-exists": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
@ -2450,8 +2726,7 @@
"path-parse": { "path-parse": {
"version": "1.0.7", "version": "1.0.7",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="
"dev": true
}, },
"path-to-regexp": { "path-to-regexp": {
"version": "1.8.0", "version": "1.8.0",
@ -2461,6 +2736,11 @@
"isarray": "0.0.1" "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": { "picocolors": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
@ -2667,7 +2947,6 @@
"version": "1.20.0", "version": "1.20.0",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz",
"integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==", "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==",
"dev": true,
"requires": { "requires": {
"is-core-module": "^2.2.0", "is-core-module": "^2.2.0",
"path-parse": "^1.0.6" "path-parse": "^1.0.6"
@ -2787,6 +3066,11 @@
"integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==",
"dev": true "dev": true
}, },
"stylis": {
"version": "4.0.13",
"resolved": "https://registry.npmjs.org/stylis/-/stylis-4.0.13.tgz",
"integrity": "sha512-xGPXiFVl4YED9Jh7Euv2V220mriG9u4B2TA6Ybjc1catrstKD2PpIdU3U0RKpkVBC2EhmL/F0sPCr9vrFTNRag=="
},
"supports-color": { "supports-color": {
"version": "8.1.1", "version": "8.1.1",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", "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", "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.0.tgz",
"integrity": "sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw==", "integrity": "sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw==",
"dev": true "dev": true
},
"yaml": {
"version": "1.10.2",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
"integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg=="
} }
} }
} }

View File

@ -22,8 +22,11 @@
}, },
"dependencies": { "dependencies": {
"@babel/plugin-proposal-class-properties": "^7.16.7", "@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/core": "^4.12.3",
"@material-ui/icons": "^4.11.2", "@material-ui/icons": "^4.11.2",
"@mui/system": "^5.2.6",
"material-ui-image": "^3.3.2", "material-ui-image": "^3.3.2",
"react-router-dom": "^5.2.0" "react-router-dom": "^5.2.0"
} }

View File

@ -1,11 +1,173 @@
import React, { Component } from "react"; 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 { export default class BookPage extends Component {
constructor(props) { constructor(props) {
super(props); super(props);
} this.state = {
orders: new Array(),
currency: 1,
type: 1,
};
this.getOrderDetails()
this.state.currencyCode = this.getCurrencyCode(this.state.currency)
}
render() { // Fix needed to handle HTTP 404 error when no order is found
return <p>This is the order book page</p>; // 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 (
<Grid className='orderBook' container spacing={1}>
<Grid item xs={12} align="center">
<Typography component="h4" variant="h4">
Order Book
</Typography>
</Grid>
<Grid item xs={6} align="right">
<FormControl >
<FormHelperText>
I want to
</FormHelperText>
<Select
label="Select Order Type"
required="true"
value={this.state.type}
inputProps={{
style: {textAlign:"center"}
}}
onChange={this.handleTypeChange}
>
<MenuItem value={1}>BUY</MenuItem>
<MenuItem value={0}>SELL</MenuItem>
</Select>
</FormControl>
</Grid>
<Grid item xs={6} align="left">
<FormControl >
<FormHelperText>
And pay with
</FormHelperText>
<Select
label="Select Payment Currency"
required="true"
value={this.state.currency}
inputProps={{
style: {textAlign:"center"}
}}
onChange={this.handleCurrencyChange}
>
<MenuItem value={1}>USD</MenuItem>
<MenuItem value={2}>EUR</MenuItem>
<MenuItem value={3}>ETH</MenuItem>
</Select>
</FormControl>
</Grid>
{this.state.orders.map((order) =>
<Grid container item sm={4}>
<Card elevation={6} sx={{ width: 945 }}>
{/* To fix! does not pass order.id to handleCardCLick. Instead passes the clicked </>*/}
<CardActionArea value={order.id} onClick={this.handleCardClick}>
<CardContent>
<List dense="true">
<ListItem >
<ListItemAvatar >
<Avatar
alt={order.maker_nick}
src={window.location.origin +'/static/assets/avatars/' + order.maker_nick + '.png'}
/>
</ListItemAvatar>
<ListItemText>
<Typography gutterBottom variant="h6">
{order.maker_nick}
</Typography>
</ListItemText>
</ListItem>
{/* CARD PARAGRAPH CONTENT */}
<ListItemText>
<Typography variant="subtitle1" color="text.secondary">
{order.type == 0 ? <b> Buys </b>: <b> Sells </b>}
<b>{parseFloat(parseFloat(order.amount).toFixed(4))}
{" " +this.getCurrencyCode(order.currency)}</b> <a> worth of bitcoin</a>
</Typography>
<Typography variant="subtitle1" color="text.secondary">
Payment via <b>{order.payment_method}</b>
</Typography>
<Typography variant="subtitle1" color="text.secondary">
Priced {order.is_explicit ?
" explicitly at " + this.pn(order.satoshis) + " Sats" : (
" at " +
parseFloat(parseFloat(order.premium).toFixed(4)) + "% over the market"
)}
</Typography>
<Typography variant="subtitle1" color="text.secondary">
<b>{" 42,354 "}{this.getCurrencyCode(order.currency)}/BTC</b> (Binance API)
</Typography>
</ListItemText>
</List>
</CardContent>
</CardActionArea>
</Card>
</Grid>
)}
<Grid item xs={12} align="center">
<Typography component="h5" variant="h5">
You are {this.state.type == 0 ? " selling " : " buying "} BTC for {this.state.currencyCode}
</Typography>
</Grid>
<Grid item xs={12} align="center">
<Button color="secondary" variant="contained" to="/" component={Link}>
Back
</Button>
</Grid>
</Grid>
);
};
} }

View File

@ -1,5 +1,5 @@
import React, { Component } from 'react'; 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' import { Link } from 'react-router-dom'
function getCookie(name) { function getCookie(name) {
@ -79,7 +79,7 @@ export default class MakerPage extends Component {
premium: 0, premium: 0,
}); });
} }
handleClickisExplicit=(e)=>{ handleClickExplicit=(e)=>{
this.setState({ this.setState({
isExplicit: true, isExplicit: true,
satoshis: 10000, satoshis: 10000,
@ -104,130 +104,116 @@ export default class MakerPage extends Component {
}; };
fetch("/api/make/",requestOptions) fetch("/api/make/",requestOptions)
.then((response) => response.json()) .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() { render() {
return ( return (
<Grid container spacing={1}> <Grid container spacing={1}>
<Grid item xs={12} align="center"> <Grid item xs={12} align="center">
<Typography component="h4" variant="h4"> <Grid item xs={12} align="center">
Make an Order <Typography component="h4" variant="h4">
</Typography> Make an Order
</Grid> </Typography>
<Grid item xs={12} align="center"> </Grid>
<FormControl component="fieldset"> <Paper elevation={12} style={{ padding: 8,}}>
<RadioGroup row defaultValue="0" onChange={this.handleTypeChange}> <Grid item xs={12} align="center">
<FormControlLabel <FormControl component="fieldset">
value="0" <FormHelperText>
<div align='center'>
Choose Buy or Sell Bitcoin
</div>
</FormHelperText>
<RadioGroup row defaultValue="0" onChange={this.handleTypeChange}>
<FormControlLabel
value="0"
control={<Radio color="primary"/>}
label="Buy"
labelPlacement="Top"
/>
<FormControlLabel
value="1"
control={<Radio color="secondary"/>}
label="Sell"
labelPlacement="Top"
/>
</RadioGroup>
</FormControl>
</Grid>
<Grid item xs={12} align="center">
<FormControl >
<TextField
label="Amount of Fiat to Trade"
type="number"
required="true"
defaultValue={this.defaultAmount}
inputProps={{
min:0 ,
style: {textAlign:"center"}
}}
onChange={this.handleAmountChange}
/>
<Select
label="Select Payment Currency"
required="true"
defaultValue={this.defaultCurrency}
inputProps={{
style: {textAlign:"center"}
}}
onChange={this.handleCurrencyChange}
>
<MenuItem value={1}>USD</MenuItem>
<MenuItem value={2}>EUR</MenuItem>
<MenuItem value={3}>ETH</MenuItem>
</Select>
</FormControl>
</Grid>
<Grid item xs={12} align="center">
<FormControl >
<TextField
label="Payment Method(s)"
type="text"
require={true}
inputProps={{
style: {textAlign:"center"}
}}
onChange={this.handlePaymentMethodChange}
/>
</FormControl>
</Grid>
<Grid item xs={12} align="center">
<FormControl component="fieldset">
<RadioGroup row defaultValue="relative">
<FormControlLabel
value="relative"
control={<Radio color="primary"/>} control={<Radio color="primary"/>}
label="Buy" label="Relative"
labelPlacement="Top" labelPlacement="Top"
/> onClick={this.handleClickRelative}
<FormControlLabel />
value="1" <FormControlLabel
value="explicit"
control={<Radio color="secondary"/>} control={<Radio color="secondary"/>}
label="Sell" label="Explicit"
labelPlacement="Top" labelPlacement="Top"
/> onClick={this.handleClickExplicit}
</RadioGroup> />
<FormHelperText> </RadioGroup>
<div align='center'> <FormHelperText >
Choose Buy or Sell Bitcoin <div align='center'>
</div> Choose a Pricing Method
</FormHelperText> </div>
</FormControl> </FormHelperText>
</Grid> </FormControl>
<Grid item xs={12} align="center"> </Grid>
<FormControl > {/* conditional shows either Premium % field or Satoshis field based on pricing method */}
<Select { this.state.isExplicit
require={true} ? <Grid item xs={12} align="center">
defaultValue={this.defaultCurrency} <TextField
inputProps={{ label="Explicit Amount in Satoshis"
style: {textAlign:"center"}
}}
onChange={this.handleCurrencyChange}
>
<MenuItem value={1}>USD</MenuItem>
<MenuItem value={2}>EUR</MenuItem>
<MenuItem value={3}>ETH</MenuItem>
</Select>
<FormHelperText>
<div align='center'>
Select Payment Currency
</div>
</FormHelperText>
</FormControl>
</Grid>
<Grid item xs={12} align="center">
<FormControl >
<TextField
type="number"
require={true}
defaultValue={this.defaultAmount}
inputProps={{
min:0 ,
style: {textAlign:"center"}
}}
onChange={this.handleAmountChange}
/>
</FormControl>
<FormHelperText>
<div align='center'>
Amount of Fiat to Trade
</div>
</FormHelperText>
</Grid>
<Grid item xs={12} align="center">
<FormControl >
<TextField
type="text"
require={true}
inputProps={{
style: {textAlign:"center"}
}}
onChange={this.handlePaymentMethodChange}
/>
<FormHelperText>
<div align='center'>
Enter the Payment Method(s)
</div>
</FormHelperText>
</FormControl>
</Grid>
<Grid item xs={12} align="center">
<FormControl component="fieldset">
<RadioGroup row defaultValue="relative">
<FormControlLabel
value="relative"
control={<Radio color="primary"/>}
label="Relative"
labelPlacement="Top"
onClick={this.handleClickRelative}
/>
<FormControlLabel
value="explicit"
control={<Radio color="secondary"/>}
label="Explicit"
labelPlacement="Top"
onClick={this.handleClickisExplicit}
onShow="false"
/>
</RadioGroup>
<FormHelperText >
<div align='center'>
Choose a Pricing Method
</div>
</FormHelperText>
</FormControl>
</Grid>
{/* conditional shows either Premium % field or Satoshis field based on pricing method */}
{ this.state.isExplicit
? <Grid item xs={12} align="center">
<FormControl >
<TextField
type="number" type="number"
require={true} required="true"
inputProps={{ inputProps={{
// TODO read these from .env file // TODO read these from .env file
min:10000 , min:10000 ,
@ -235,36 +221,25 @@ export default class MakerPage extends Component {
style: {textAlign:"center"} style: {textAlign:"center"}
}} }}
onChange={this.handleSatoshisChange} onChange={this.handleSatoshisChange}
defaultValue={this.defaultSatoshis} // defaultValue={this.defaultSatoshis}
/> />
<FormHelperText> </Grid>
<div align='center'> : <Grid item xs={12} align="center">
Explicit Amount in Satoshis
</div>
</FormHelperText>
</FormControl>
</Grid>
: <Grid item xs={12} align="center">
<FormControl >
<TextField <TextField
label="Premium over Market (%)"
type="number" type="number"
require={true} // defaultValue={this.defaultPremium}
defaultValue={this.defaultPremium}
inputProps={{ inputProps={{
style: {textAlign:"center"} style: {textAlign:"center"}
}} }}
onChange={this.handlePremiumChange} onChange={this.handlePremiumChange}
/> />
<FormHelperText> </Grid>
<div align='center'> }
Premium Relative to Market Price (%) </Paper>
</div> </Grid>
</FormHelperText>
</FormControl>
</Grid>
}
<Grid item xs={12} align="center"> <Grid item xs={12} align="center">
<Button color="primary" variant="contained" onClick={this.handleCreateOfferButtonPressed}> <Button color="primary" variant="contained" onClick={this.handleCreateOfferButtonPressed} >
Create Order Create Order
</Button> </Button>
<Typography component="subtitle2" variant="subtitle2"> <Typography component="subtitle2" variant="subtitle2">
@ -277,14 +252,13 @@ export default class MakerPage extends Component {
} }
</div> </div>
</Typography> </Typography>
</Grid> <Grid item xs={12} align="center">
<Grid item xs={12} align="center"> <Button color="secondary" variant="contained" to="/" component={Link}>
<Button color="secondary" variant="contained" to="/" component={Link}> Back
Back </Button>
</Button> </Grid>
</Grid> </Grid>
</Grid> </Grid>
); );
} }
} }

View File

@ -1,7 +1,12 @@
import React, { Component } from "react"; 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' 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 { export default class OrderPage extends Component {
constructor(props) { constructor(props) {
super(props); super(props);
@ -36,27 +41,31 @@ export default class OrderPage extends Component {
}); });
} }
// Fix to use proper react props
handleClickBackButton=()=>{
window.history.back();
}
render (){ render (){
return ( return (
<Grid container spacing={1}> <Grid container spacing={1}>
<Grid item xs={12} align="center"> <Grid item xs={12} align="center">
<Typography component="h5" variant="h5"> <Typography component="h5" variant="h5">
Robosat BTC {this.state.type ? " Sell " : " Buy "} Order BTC {this.state.type ? " Sell " : " Buy "} Order
</Typography> </Typography>
<Paper elevation={12} style={{ padding: 8,}}>
<List component="nav" aria-label="mailbox folders"> <List component="nav" aria-label="mailbox folders">
<ListItem> <ListItem>
<ListItemAvatar sx={{ width: 56, height: 56 }}> <ListItemAvatar sx={{ width: 56, height: 56 }}>
<Avatar <Avatar
alt={this.state.makerNick} alt={this.state.makerNick}
src={window.location.origin +'/static/assets/avatars/' + this.state.makerNick + '.png'} src={window.location.origin +'/static/assets/avatars/' + this.state.makerNick + '.png'}
sx={{ width: 56, height: 56 }}
/> />
</ListItemAvatar> </ListItemAvatar>
<ListItemText primary={this.state.makerNick} secondary="Order maker" /> <ListItemText primary={this.state.makerNick} secondary="Order maker" />
</ListItem> </ListItem>
<Divider /> <Divider />
<ListItem> <ListItem>
<ListItemText primary={this.state.amount+" "+this.state.currencyCode} secondary="Amount and currency requested"/> <ListItemText primary={parseFloat(parseFloat(this.state.amount).toFixed(4))+" "+this.state.currencyCode} secondary="Amount and currency requested"/>
</ListItem> </ListItem>
<Divider /> <Divider />
<ListItem> <ListItem>
@ -65,9 +74,9 @@ export default class OrderPage extends Component {
<Divider /> <Divider />
<ListItem> <ListItem>
{this.state.isExplicit ? {this.state.isExplicit ?
<ListItemText primary={this.state.satoshis} secondary="Amount of Satoshis"/> <ListItemText primary={pn(this.state.satoshis)} secondary="Amount of Satoshis"/>
: :
<ListItemText primary={this.state.premium} secondary="Premium over market price"/> <ListItemText primary={parseFloat(parseFloat(this.state.premium).toFixed(2))+"%"} secondary="Premium over market price"/>
} }
</ListItem> </ListItem>
<Divider /> <Divider />
@ -93,6 +102,10 @@ export default class OrderPage extends Component {
<Grid item xs={12} align="center"> <Grid item xs={12} align="center">
{this.state.isParticipant ? "" : <Button variant='contained' color='primary' to='/home' component={Link}>Take Order</Button>} {this.state.isParticipant ? "" : <Button variant='contained' color='primary' to='/home' component={Link}>Take Order</Button>}
</Grid> </Grid>
<Grid item xs={12} align="center">
{this.state.isParticipant ? "" : <Button variant='contained' color='secondary' onClick={this.handleClickBackButton}>Back</Button>}
</Grid>
</Paper>
</Grid> </Grid>
</Grid> </Grid>
); );

View File

@ -1,5 +1,5 @@
import React, { Component } from "react"; 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 { Link } from 'react-router-dom'
import Image from 'material-ui-image' import Image from 'material-ui-image'
@ -85,9 +85,39 @@ export default class UserGenPage extends Component {
this.getGeneratedUser(); this.getGeneratedUser();
} }
// TO FIX CSRF TOKEN IS NOT UPDATED UNTIL WINDOW IS RELOADED
reload_for_csrf_to_work=()=>{
window.location.reload()
}
render() { render() {
return ( return (
<Grid container spacing={1}> <Grid container spacing={1}>
<Grid item xs={12} align="center">
<Typography component="h5" variant="h5">
<b>{this.state.nickname ? "⚡"+this.state.nickname+"⚡" : ""}</b>
</Typography>
</Grid>
<Grid item xs={12} align="center">
<div style={{ maxWidth: 200, maxHeight: 200 }}>
<Image className='newAvatar'
disableError='true'
cover='true'
color='null'
src={this.state.avatar_url}
/>
</div><br/>
</Grid>
{
this.state.found ?
<Grid item xs={12} align="center">
<Typography component="subtitle2" variant="subtitle2" color='primary'>
{this.state.found}<br/>
</Typography>
</Grid>
:
""
}
<Grid item xs={12} align="center"> <Grid item xs={12} align="center">
<TextField <TextField
error={this.state.bad_request} error={this.state.bad_request}
@ -102,36 +132,19 @@ export default class UserGenPage extends Component {
/> />
</Grid> </Grid>
<Grid item xs={12} align="center"> <Grid item xs={12} align="center">
<div style={{ maxWidth: 200, maxHeight: 200 }}> <Button onClick={this.handleAnotherButtonPressed}>Generate Another Robosat</Button>
<Image className='newAvatar' </Grid>
disableError='true' <Grid item xs={12} align="center">
cover='true' <ButtonGroup variant="contained" aria-label="outlined primary button group">
color='null' <Button color='primary' to='/make/' component={Link}>Make Order</Button>
src={this.state.avatar_url} <Button to='/home' component={Link}>INFO</Button>
/> <Button color='secondary' to='/book/' component={Link}>View Book</Button>
</div> </ButtonGroup>
</Grid> </Grid>
<Grid item xs={12} align="center"> <Grid item xs={12} align="center">
<Typography component="h5" variant="h5"> <Typography component="h5" variant="h5">
<b>{this.state.nickname ? "⚡"+this.state.nickname+"⚡" : ""}</b> Easy and Private Lightning peer-to-peer Exchange
</Typography> </Typography>
</Grid>
{
this.state.found ?
<Grid item xs={12} align="center">
<Typography component="subtitle2" variant="subtitle2" color='primary'>
{this.state.found}<br/>
</Typography>
<Button variant='contained' color='primary' to='/home' component={Link}>Cool!</Button>
</Grid>
:
<Grid item xs={12} align="center">
<Button variant='contained' color='primary' to='/home' component={Link}>Take This Robosat!</Button>
</Grid>
}
<Grid item xs={12} align="center">
<Button variant='contained' to='/' component={Link} onClick={this.handleAnotherButtonPressed}>Give Me Another</Button>
</Grid> </Grid>
</Grid> </Grid>
); );

View File

@ -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;
}

View File

@ -47,7 +47,7 @@ pip install robohash
### Install npm ### Install npm
`sudo apt 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 cd frontend
npm init -y npm init -y
@ -59,6 +59,7 @@ npm install @babel/plugin-proposal-class-properties
npm install react-router-dom@5.2.0 npm install react-router-dom@5.2.0
npm install @material-ui/icons npm install @material-ui/icons
npm install material-ui-image npm install material-ui-image
npm install @mui/system @emotion/react @emotion/styled
``` ```
### Launch the React render ### Launch the React render