From ad93dfd4175220c790c7dbbf320ef3f1f2976a21 Mon Sep 17 00:00:00 2001 From: Manish Sinha Date: Wed, 16 Jun 2021 17:38:49 -0700 Subject: [PATCH] wip --- client/src/App.js | 13 +- client/src/components/Main.jsx | 45 +- client/src/views/Creator.jsx | 810 ++++++++++++++++++ .../accounts/migrations/0011_user_creator.py | 18 + server/accounts/models.py | 4 + server/core/admin.py | 20 + .../migrations/0015_auto_20210609_0448.py | 30 + .../migrations/0016_auto_20210609_0549.py | 32 + .../migrations/0017_submovement_minimum.py | 18 + server/core/models.py | 16 + server/underline/graphql/schema.py | 20 + 11 files changed, 989 insertions(+), 37 deletions(-) create mode 100644 client/src/views/Creator.jsx create mode 100644 server/accounts/migrations/0011_user_creator.py create mode 100644 server/core/migrations/0015_auto_20210609_0448.py create mode 100644 server/core/migrations/0016_auto_20210609_0549.py create mode 100644 server/core/migrations/0017_submovement_minimum.py diff --git a/client/src/App.js b/client/src/App.js index 27bf383..410dbbc 100644 --- a/client/src/App.js +++ b/client/src/App.js @@ -20,6 +20,7 @@ import Lobby from 'views/Lobby.jsx' import Active from 'views/Active.jsx' import Completed from 'views/Completed.jsx' import Settings from 'views/Settings.jsx' +import Creator from 'views/Creator.jsx' import Main from 'components/Main' // If there isn't a valid token, don't send it. This is because we can have an expired @@ -108,6 +109,10 @@ function App() { path="/active" component={Active} > + - ( -
- -
- )} - /> ( diff --git a/client/src/components/Main.jsx b/client/src/components/Main.jsx index a2c1444..393af3c 100644 --- a/client/src/components/Main.jsx +++ b/client/src/components/Main.jsx @@ -29,6 +29,7 @@ const GET_ME_QUERY = gql` lastName username walletBalance + creator } } ` @@ -98,21 +99,16 @@ const Main = (props) => { > Complete - {/* - - @ - {data && data.me.username - ? data.me.username - : 'me'} - - */} + {isActiveJWT() && + data && + data.me.creator && ( + + Creator + + )} { Completed )} - {/* isActiveJWT() && data && ( + {isActiveJWT() && data && data.me.creator && ( - @ - {data && data.me.username - ? data.me.username - : 'me'} + Creator - )*/} + )} {isActiveJWT() && ( diff --git a/client/src/views/Creator.jsx b/client/src/views/Creator.jsx new file mode 100644 index 0000000..dbc9275 --- /dev/null +++ b/client/src/views/Creator.jsx @@ -0,0 +1,810 @@ +import React, { useRef, useState } from 'react' +import { + Divider, + Form, + Progress, + Message, + Segment, + Grid, + Label, + Icon, + List, + Popup, + Modal, + Card, + Image, + Header, + Input, + Container, + Button, + Dropdown, + Menu, + Tab, +} from 'semantic-ui-react' +import { gql, useApolloClient, useQuery } from '@apollo/client' +import moment from 'moment-timezone' +import { Helmet } from 'react-helmet' +import './Lobby.scss' +import { useParams, Link, useHistory } from 'react-router-dom' +import { useMediaQuery } from 'react-responsive' +import { getJWT, clearJWT } from 'utils' + +import { DateInput } from 'semantic-ui-calendar-react' + +const GET_TODAYS_SUBLINES_AND_MOVEMENTS = gql` + query { + todaysSublines { + id + projectedValue + line { + id + category { + id + category + } + player { + id + name + headshotUrl + team { + id + } + } + game { + datetime + homeTeam { + abbreviation + } + awayTeam { + abbreviation + } + } + } + } + todaysMovements { + id + cap + submovementSet { + id + category { + id + category + } + swing + } + } + lineCategories(league: "NBA") { + id + category + } + } +` + +const CREATE_SLIP_MUTATION = gql` + mutation CreateSlip( + $picks: [PickType]! + $entryAmount: Int! + $creatorCode: String + ) { + createSlip( + picks: $picks + entryAmount: $entryAmount + creatorCode: $creatorCode + ) { + success + freeToPlay + } + } +` + +const GET_CURRENT_DATE_QUERY = gql` + query currentDate { + currentDate + } +` + +const PlayerList = ({ picks, addOrRemovePick, setTabActiveIndex }) => { + const { data } = useQuery(GET_TODAYS_SUBLINES_AND_MOVEMENTS) + const isTabletOrMobile = useMediaQuery({ query: '(max-width: 767px)' }) + + const panes = + data && + data.lineCategories.map((lineCategory) => { + return { + menuItem: lineCategory.category, + pane: { + key: lineCategory.category, + content: ( + + {data.todaysSublines + .filter( + (subline) => + subline.line.category.category === + lineCategory.category + ) + .map((subline) => { + const pick = picks.filter((e) => { + return e.id === subline.id + })[0] + + return ( + + + + + {subline.line.player.name} + + + + { + subline.line + .category + .category + } + :{' '} + {parseFloat( + subline.projectedValue + ).toFixed(1)} + + + + { + subline.line.game + .awayTeam + .abbreviation + }{' '} + @{' '} + { + subline.line.game + .homeTeam + .abbreviation + }{' '} + -{' '} + {moment + .tz( + subline.line.game + .datetime, + moment.tz.guess() + ) + .format('h:mma z')} + + + + + + + + ))} + + ) +} + +const LobbyHeader = () => { + const { data } = useQuery(GET_CURRENT_DATE_QUERY) + const isTabletOrMobile = useMediaQuery({ query: '(max-width: 767px)' }) + + return ( +
+ Featured Players:{' '} + {data && moment(data.currentDate).format('MMMM Do YYYY')} +
+ ) +} + +const Creator = ({ updateMainComponent }) => { + const [tab, setTab] = useState('lobby') + const [picks, setPicks] = useState([]) + const [percent, setPercent] = useState(0) + const [multiplier, setMultiplier] = useState('1x') + const [payout, setPayout] = useState('') + const [entryAmount, setEntryAmount] = useState('') + const [checking, setChecking] = useState(false) + const [processing, setProcessing] = useState(false) + const [ + scrollToBottomButtonVisible, + setScrollToBottomButtonVisible, + ] = useState(true) + const [errorModalVisible, setErrorModalVisible] = useState({ + open: false, + header: '', + message: '', + }) + const [confirmationModalVisible, setConfirmationModalVisible] = useState( + false + ) + const [ + creatorSlipCreatedModalVisible, + setCreatorSlipCreatedModalVisible, + ] = useState(false) + const { code, username } = useParams() + const [creatorCode, setCreatorCode] = useState(code ? code : '') + const [tabActiveIndex, setTabActiveIndex] = useState() + const [ + insufficientFundsModalVisible, + setInsufficentFundsModalVisible, + ] = useState(false) + const [payoutErrorVisible, setPayoutErrorVisible] = useState(false) + const client = useApolloClient() + const history = useHistory() + const isTabletOrMobile = useMediaQuery({ query: '(max-width: 767px)' }) + + //const { data } = useQuery(GET_ME_QUERY) + //const isSelf = data && data.me && data.me.username === username + + const addOrRemovePick = (subline, under) => { + const pickIndex = picks.findIndex((e) => e.id === subline.id) + let newPicks = [] + + // If the pick already exists, remove it or update it + if (pickIndex != -1) { + var array = [...picks] // deep copy + + // Check if user is changing the over/under. If so, just update that. + if (array[pickIndex].under != under) { + array[pickIndex].under = under + } + + // Else, remove it + else { + array.splice(pickIndex, 1) + } + + newPicks = array + setPicks(newPicks) + } + + // New pick. Set the attribute + else { + // If we're at 5 picks, tell the user and don't proceed + if (picks.length === 5) { + setErrorModalVisible({ + open: true, + header: 'Too many picks', + message: 'You can only choose five picks.', + }) + newPicks = picks + } else { + newPicks = [...picks, Object.assign({}, subline, { under })] + setPicks(newPicks) + } + } + + // Update multiplier + if (newPicks.length == 0) { + setPercent(0) + setMultiplier(`${getMultiplier(newPicks.length)}x`) + setPayout(entryAmount ? entryAmount : '') + } else if (newPicks.length === 1) { + setPercent(10) + setMultiplier(`${getMultiplier(newPicks.length)}x`) + setPayout(entryAmount ? entryAmount : '') + } else if (newPicks.length === 2) { + setPercent(25) + setMultiplier(`${getMultiplier(newPicks.length)}x`) + setPayout( + entryAmount ? entryAmount * getMultiplier(newPicks.length) : '' + ) + } else if (newPicks.length === 3) { + setPercent(50) + setMultiplier(`${getMultiplier(newPicks.length)}x`) + setPayout( + entryAmount ? entryAmount * getMultiplier(newPicks.length) : '' + ) + } else if (newPicks.length === 4) { + setPercent(75) + setMultiplier(`${getMultiplier(newPicks.length)}x`) + setPayout( + entryAmount ? entryAmount * getMultiplier(newPicks.length) : '' + ) + } else if (newPicks.length === 5) { + setPercent(100) + setMultiplier(`${getMultiplier(newPicks.length)}x`) + setPayout( + entryAmount ? entryAmount * getMultiplier(newPicks.length) : '' + ) + } + } + + const getMultiplier = (numPicks) => { + if (numPicks == 0) { + return 1 + } else if (numPicks === 1) { + return 1 + } else if (numPicks === 2) { + return 3 + } else if (numPicks === 3) { + return 6 + } else if (numPicks === 4) { + return 10 + } else if (numPicks === 5) { + return 20 + } + } + + // (1) Check if they entered a payout amount + // (2) Check if entry amount is <= $50 + // (3) Check that there are atleast two teams involved + // (4) Check the location of the user + // (5) Check if user has linked a payment method + // (6) Check if user has sufficients funds in their wallet + const checkPicks = async () => { + if (!getJWT()) { + history.push(`/signup${code ? '?code=' + code : ''}`) + return + } + + setChecking(true) + let lat, + lng = null + + // (1) + if (!payout) { + setPayoutErrorVisible(true) + setChecking(false) + return + } + + if (entryAmount > 50) { + setPayoutErrorVisible(true) + setErrorModalVisible({ + open: true, + header: 'Max $50 entry', + message: 'We only allow a maximum of $50 for entry', + }) + setChecking(false) + return + } + + // (2) + let teamIds = [] + for (let i = 0; i < picks.length; i++) { + const teamId = picks[i].line.player.team.id + + if (!teamIds.includes(teamId)) { + teamIds.push(teamId) + } + } + + if (teamIds.length < 2) { + // Error + setErrorModalVisible({ + open: true, + header: 'Two teams must be involved', + message: 'You must select picks that span at least two teams.', + }) + setChecking(false) + return + } + + // (3) + if (!'geolocation' in navigator) { + setErrorModalVisible({ + open: true, + header: 'Please enable location access', + message: + 'We need to verify your location. Please enable location access.', + }) + setChecking(false) + return + } + + /* + const { data } = await client.query({ + query: GET_ME_QUERY, + }) + + if (parseFloat(data.me.walletBalance) < entryAmount) { + setInsufficentFundsModalVisible(true) + setChecking(false) + return + } + */ + + // We made it! User is all good to go + // Show confirmation modal + setChecking(false) + setConfirmationModalVisible(true) + } + + const submitPicks = async () => { + setProcessing(true) + + if (!getJWT()) { + history.push(`/signup${username ? '?username=' + username : ''}`) + return + } + + setChecking(true) + + // (1) + let teamIds = [] + for (let i = 0; i < picks.length; i++) { + const teamId = picks[i].line.player.team.id + + if (!teamIds.includes(teamId)) { + teamIds.push(teamId) + } + } + + if (teamIds.length < 2) { + // Error + setErrorModalVisible({ + open: true, + header: 'Two teams must be involved', + message: 'You must select picks that span at least two teams.', + }) + setChecking(false) + return + } + + const response = await client.mutate({ + mutation: CREATE_SLIP_MUTATION, + variables: { + picks: picks.map((e) => { + return { + id: e.id, + under: e.under, + } + }), + creatorCode: creatorCode, + entryAmount: entryAmount, + }, + }) + + // Redirect + if (response.data.createSlip.success) { + updateMainComponent() + history.push( + `/active?success${ + response.data.createSlip.freeToPlay ? '&freetoplay' : '' + }` + ) + } + } + + const handleScroll = () => { + const bottom = + Math.ceil(window.innerHeight + window.scrollY) + 50 >= + document.documentElement.scrollHeight + + setScrollToBottomButtonVisible(!bottom) + } + + React.useEffect(() => { + window.addEventListener('scroll', handleScroll, { + passive: true, + }) + + return () => { + window.removeEventListener('scroll', handleScroll) + } + }, []) + + const slipButtonRef = useRef(null) + const scrollToBottom = () => { + slipButtonRef.current?.scrollIntoView({ behavior: 'smooth' }) + } + + return ( +
+ {isTabletOrMobile && scrollToBottomButtonVisible && ( + + )} + + Lobby + + +
+ + You created your own slip! +
+ +

+ Congratulations, now share it with the world and earn a + share of their profits. +

+

+ + {`https://www.underlinefantasy.com/${username}`} + +

+
+ + + +
+ setErrorModalVisible({ open: false })} + open={errorModalVisible.open} + size="small" + > +
+ + {errorModalVisible.header} +
+ +

{errorModalVisible.message}

+
+ + + +
+ setInsufficentFundsModalVisible(false)} + open={insufficientFundsModalVisible} + size="small" + > +
+ + Insufficient funds +
+ +

+ You do not have enough funds in your wallet to make this + slip. Please click 'Deposit' below to fix this. +

+
+ + + + +
+ setConfirmationModalVisible(false)} + open={confirmationModalVisible} + size="small" + > +
+ + Confirmation +
+ +

Once confirmed, your selections are locked.

+
+ + + + +
+
+ TODO + TODO +
+ + + + + + {isTabletOrMobile && ( + <> + + + )} + + + {!isTabletOrMobile && ( + <> + + + )} + + + +
+ ) +} + +export default Creator diff --git a/server/accounts/migrations/0011_user_creator.py b/server/accounts/migrations/0011_user_creator.py new file mode 100644 index 0000000..c5e1058 --- /dev/null +++ b/server/accounts/migrations/0011_user_creator.py @@ -0,0 +1,18 @@ +# Generated by Django 3.1.6 on 2021-06-09 11:58 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounts', '0010_user_current_creator_slip'), + ] + + operations = [ + migrations.AddField( + model_name='user', + name='creator', + field=models.BooleanField(default=False), + ), + ] diff --git a/server/accounts/models.py b/server/accounts/models.py index 8851520..7cb70ec 100755 --- a/server/accounts/models.py +++ b/server/accounts/models.py @@ -89,10 +89,14 @@ class User(AbstractBaseUser, PermissionsMixin): wallet_balance = models.DecimalField(max_digits=6, decimal_places=2, default=0) free_to_play = models.BooleanField(default=False) username = models.CharField(max_length=32, blank=True, null=True, unique=True) + + # used? current_creator_slip = models.ForeignKey( "core.Slip", blank=True, null=True, on_delete=models.CASCADE ) + creator = models.BooleanField(default=False) + # Add additional fields here if needed objects = UserManager() USERNAME_FIELD = "email" diff --git a/server/core/admin.py b/server/core/admin.py index 93b734d..3870ba3 100755 --- a/server/core/admin.py +++ b/server/core/admin.py @@ -38,6 +38,8 @@ Pick, Deposit, LineCategory, + Movement, + SubMovement, ) @@ -331,10 +333,28 @@ def has_change_permission(self, request, obj=None): return False +class SubMovementTabularInline(admin.TabularInline): + list_display = [ + field.name for field in SubMovement._meta.fields if field.name != "id" + ] + model = SubMovement + extra = 0 + + +class MovementAdmin(admin.ModelAdmin): + inlines = [ + SubMovementTabularInline, + ] + + class Meta: + model = Movement + + admin.site.register(Team, TeamAdmin) # Register your models here. admin.site.register(League) +admin.site.register(Movement, MovementAdmin) admin.site.register(Slip, SlipAdmin) admin.site.register(FreeToPlaySlip, FreeToPlaySlipsAdmin) admin.site.register(PaidSlip, PaidSlipAdmin) diff --git a/server/core/migrations/0015_auto_20210609_0448.py b/server/core/migrations/0015_auto_20210609_0448.py new file mode 100644 index 0000000..bb6d0fa --- /dev/null +++ b/server/core/migrations/0015_auto_20210609_0448.py @@ -0,0 +1,30 @@ +# Generated by Django 3.1.6 on 2021-06-09 11:48 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0014_auto_20210602_1450'), + ] + + operations = [ + migrations.CreateModel( + name='Movement', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('date', models.DateField()), + ('swing', models.DecimalField(decimal_places=2, default=0, max_digits=6)), + ('cap', models.IntegerField()), + ('datetime_created', models.DateTimeField(auto_now_add=True)), + ('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.linecategory')), + ], + ), + migrations.AddField( + model_name='subline', + name='movement', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='core.movement'), + ), + ] diff --git a/server/core/migrations/0016_auto_20210609_0549.py b/server/core/migrations/0016_auto_20210609_0549.py new file mode 100644 index 0000000..81c8d0f --- /dev/null +++ b/server/core/migrations/0016_auto_20210609_0549.py @@ -0,0 +1,32 @@ +# Generated by Django 3.1.6 on 2021-06-09 12:49 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0015_auto_20210609_0448'), + ] + + operations = [ + migrations.RemoveField( + model_name='movement', + name='category', + ), + migrations.RemoveField( + model_name='movement', + name='swing', + ), + migrations.CreateModel( + name='SubMovement', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('swing', models.DecimalField(decimal_places=2, default=0, max_digits=6)), + ('datetime_created', models.DateTimeField(auto_now_add=True)), + ('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.linecategory')), + ('movement', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.movement')), + ], + ), + ] diff --git a/server/core/migrations/0017_submovement_minimum.py b/server/core/migrations/0017_submovement_minimum.py new file mode 100644 index 0000000..6cfab97 --- /dev/null +++ b/server/core/migrations/0017_submovement_minimum.py @@ -0,0 +1,18 @@ +# Generated by Django 3.1.6 on 2021-06-09 13:26 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0016_auto_20210609_0549'), + ] + + operations = [ + migrations.AddField( + model_name='submovement', + name='minimum', + field=models.DecimalField(decimal_places=2, default=0, max_digits=6), + ), + ] diff --git a/server/core/models.py b/server/core/models.py index 6ac6887..814e00f 100755 --- a/server/core/models.py +++ b/server/core/models.py @@ -112,6 +112,20 @@ def game_date(self): game_date.short_description = "Game date PST" +class Movement(models.Model): + date = models.DateField() + cap = models.IntegerField() + datetime_created = models.DateTimeField(auto_now_add=True) + + +class SubMovement(models.Model): + category = models.ForeignKey(LineCategory, on_delete=models.CASCADE) + swing = models.DecimalField(max_digits=6, decimal_places=2, default=0) + minimum = models.DecimalField(max_digits=6, decimal_places=2, default=0) + datetime_created = models.DateTimeField(auto_now_add=True) + movement = models.ForeignKey(Movement, on_delete=models.CASCADE) + + class Subline(models.Model): line = models.ForeignKey(Line, on_delete=models.CASCADE) projected_value = models.DecimalField( @@ -121,6 +135,8 @@ class Subline(models.Model): # Visible in lobby visible = models.BooleanField(default=True) + movement = models.ForeignKey(Movement, on_delete=models.CASCADE, null=True) + def __str__(self): return f"{self.line}" diff --git a/server/underline/graphql/schema.py b/server/underline/graphql/schema.py index be79c9d..2d15418 100644 --- a/server/underline/graphql/schema.py +++ b/server/underline/graphql/schema.py @@ -18,6 +18,8 @@ Deposit, LineCategory, League, + Movement, + SubMovement, ) from accounts.models import User from graphql_jwt.decorators import login_required @@ -80,8 +82,20 @@ class Meta: model = Line +class SubMovementType(DjangoObjectType): + class Meta: + model = SubMovement + + +class MovementType(DjangoObjectType): + + class Meta: + model = Movement + + class SublineType(DjangoObjectType): line = graphene.Field(LineType) + movement = graphene.Field(MovementType) class Meta: model = Subline @@ -126,6 +140,7 @@ class Meta: class Query(graphene.ObjectType): todays_sublines = graphene.List(SublineType) + todays_movements = graphene.List(MovementType) line_categories = graphene.Field( graphene.List(LineCategoryType), league=graphene.String(required=True) ) @@ -168,6 +183,11 @@ def resolve_todays_sublines(self, info, **kwargs): return q1 + # Get today's date. Find all the movements for the date + def resolve_todays_movements(self, info, **kwargs): + cd = CurrentDate.objects.first() + return Movement.objects.filter(date=cd.date) + @login_required def resolve_me(self, info, **kawargs): return info.context.user