@@ -40,6 +40,10 @@ export class ListItemModal extends FloatElement {
40
40
@state ( )
41
41
private listingId : string | undefined ;
42
42
43
+ private readonly MAX_PRICE_CENTS = 100000 * 100 ; // $100,000
44
+
45
+ private readonly SALES_FEE_PERCENTAGE = 0.02 ;
46
+
43
47
private readonly DURATION_OPTIONS = [
44
48
{ value : 1 , label : '1 Day' } ,
45
49
{ value : 3 , label : '3 Days' } ,
@@ -70,7 +74,7 @@ export class ListItemModal extends FloatElement {
70
74
padding: 20px;
71
75
width: 500px;
72
76
max-width: 90%;
73
- font-family: ' Roboto' , sans-serif;
77
+ font-family: Roboto, "Helvetica Neue" , sans-serif;
74
78
border-width: 2px;
75
79
border-style: solid;
76
80
border-color: rgba(193, 206, 255, 0.07);
@@ -117,27 +121,157 @@ export class ListItemModal extends FloatElement {
117
121
118
122
.price-section {
119
123
margin-bottom: 20px;
124
+ color: rgba(255, 255, 255, 0.8);
125
+ font-size: 14px;
126
+ }
127
+
128
+ .price-input-container {
129
+ position: relative;
130
+ margin-top: 8px;
131
+ }
132
+
133
+ .price-input-prefix {
134
+ position: absolute;
135
+ left: 12px;
136
+ top: 50%;
137
+ transform: translateY(-50%);
138
+ color: rgba(255, 255, 255, 0.8);
139
+ font-size: 14px;
140
+ pointer-events: none;
120
141
}
121
142
122
143
.price-input {
123
144
width: 100%;
124
- padding: 8px;
125
- margin-top: 5px;
126
- background: #2a475e;
127
- border: 1px solid #000000;
128
- color: #ffffff;
145
+ box-sizing: border-box;
146
+ padding: 12px;
147
+ padding-left: 28px;
148
+ background: rgba(35, 123, 255, 0.1);
149
+ border: none;
150
+ border-radius: 8px;
151
+ color: white;
152
+ font-size: 14px;
153
+ font-weight: 500;
154
+ font-family: 'Roboto', sans-serif;
155
+ transition: background 0.2s ease;
156
+ }
157
+
158
+ .price-input::placeholder {
159
+ color: rgba(255, 255, 255, 0.4);
160
+ }
161
+
162
+ .price-input:focus {
163
+ outline: none;
164
+ background: rgba(35, 123, 255, 0.15);
165
+ }
166
+
167
+ .price-input::-webkit-outer-spin-button,
168
+ .price-input::-webkit-inner-spin-button {
169
+ -webkit-appearance: none;
170
+ margin: 0;
171
+ }
172
+
173
+ .price-input[type='number'] {
174
+ -moz-appearance: textfield;
129
175
}
130
176
131
177
.percentage-slider {
132
178
width: 100%;
133
- margin-top: 10px;
179
+ margin: 16px 0;
180
+ height: 8px;
181
+ background: rgba(35, 123, 255, 0.15);
182
+ border-radius: 4px;
183
+ -webkit-appearance: none;
184
+ appearance: none;
185
+ cursor: pointer;
186
+ outline: none;
187
+ position: relative;
188
+ }
189
+
190
+ .percentage-slider::before {
191
+ content: '';
192
+ position: absolute;
193
+ height: 100%;
194
+ width: calc(var(--slider-percentage, 100) * 1%);
195
+ background-color: rgb(35, 123, 255);
196
+ border-radius: 4px;
197
+ pointer-events: none;
198
+ }
199
+
200
+ .percentage-slider::-webkit-slider-thumb {
201
+ -webkit-appearance: none;
202
+ appearance: none;
203
+ width: 20px;
204
+ height: 20px;
205
+ border-radius: 50%;
206
+ background: rgb(35, 123, 255);
207
+ cursor: pointer;
208
+ box-shadow: 0 2px 6px rgba(35, 123, 255, 0.3);
209
+ margin-top: -6px;
210
+ position: relative;
211
+ z-index: 1;
212
+ }
213
+
214
+ .percentage-slider::-webkit-slider-runnable-track {
215
+ width: 100%;
216
+ height: 8px;
217
+ border-radius: 4px;
218
+ background: transparent;
219
+ }
220
+
221
+ .percentage-slider::-moz-range-thumb {
222
+ width: 20px;
223
+ height: 20px;
224
+ border: none;
225
+ border-radius: 50%;
226
+ background: rgb(35, 123, 255);
227
+ cursor: pointer;
228
+ box-shadow: 0 2px 6px rgba(35, 123, 255, 0.3);
229
+ position: relative;
230
+ z-index: 1;
231
+ }
232
+
233
+ .percentage-slider::-moz-range-track {
234
+ width: 100%;
235
+ height: 8px;
236
+ border-radius: 4px;
237
+ background: transparent;
238
+ }
239
+
240
+ .percentage-slider::-webkit-slider-thumb:hover,
241
+ .percentage-slider::-moz-range-thumb:hover {
242
+ transform: scale(1.2);
134
243
}
135
244
136
245
.error-message {
137
246
color: #ff4444;
138
247
margin-top: 10px;
139
248
}
140
249
250
+ .price-breakdown {
251
+ margin: 24px 0;
252
+ }
253
+
254
+ .price-breakdown-row {
255
+ display: flex;
256
+ justify-content: space-between;
257
+ align-items: center;
258
+ margin-bottom: 8px;
259
+ color: rgb(158, 167, 177)
260
+ font-size: 16px;
261
+ }
262
+
263
+ .price-breakdown-row:last-child {
264
+ margin-bottom: 0;
265
+ padding-top: 8px;
266
+ border-top: 1px solid rgba(255, 255, 255, 0.1);
267
+ color: #FFFFFF;
268
+ font-size: 20px
269
+ }
270
+
271
+ .price-breakdown-row.fee {
272
+ color: rgba(255, 0, 0, 0.8);
273
+ }
274
+
141
275
.submit-button {
142
276
width: 100%;
143
277
padding: 12px;
@@ -148,7 +282,6 @@ export class ListItemModal extends FloatElement {
148
282
font-size: 14px;
149
283
font-weight: 500;
150
284
cursor: pointer;
151
- margin-top: 24px;
152
285
transition: all 0.2s ease;
153
286
box-shadow: 0 4px 12px rgba(35, 123, 255, 0.3);
154
287
}
@@ -330,6 +463,13 @@ export class ListItemModal extends FloatElement {
330
463
331
464
async connectedCallback ( ) {
332
465
super . connectedCallback ( ) ;
466
+ // Set initial slider progress
467
+ requestAnimationFrame ( ( ) => {
468
+ const slider = this . shadowRoot ?. querySelector ( '.percentage-slider' ) as HTMLInputElement ;
469
+ if ( slider ) {
470
+ slider . style . setProperty ( '--slider-percentage' , '50' ) ;
471
+ }
472
+ } ) ;
333
473
await this . fetchRecommendedPrice ( ) ;
334
474
}
335
475
@@ -360,8 +500,7 @@ export class ListItemModal extends FloatElement {
360
500
return { isValid : false , error : 'Please enter a valid price greater than $0.00' } ;
361
501
}
362
502
363
- if ( price > 10000000 ) {
364
- // $100,000 in cents
503
+ if ( price > this . MAX_PRICE_CENTS ) {
365
504
return { isValid : false , error : 'Price cannot exceed $100,000 USD' } ;
366
505
}
367
506
@@ -378,22 +517,69 @@ export class ListItemModal extends FloatElement {
378
517
379
518
this . error = undefined ;
380
519
this . customPrice = price ;
381
- if ( this . recommendedPrice ) {
382
- this . pricePercentage = Number ( ( ( this . customPrice / this . recommendedPrice ) * 100 ) . toFixed ( 1 ) ) ;
383
- }
520
+ }
521
+
522
+ private getSaleFee ( cents : number ) : number {
523
+ return Math . max ( 1 , cents * this . SALES_FEE_PERCENTAGE ) ;
524
+ }
525
+
526
+ private formatPrice ( cents : number ) : string {
527
+ return ( cents / 100 ) . toFixed ( 2 ) ;
528
+ }
529
+
530
+ private formatInputPrice ( cents : number ) : string {
531
+ // For input, show the exact value without forcing decimals
532
+ const dollars = ( cents / 100 ) . toString ( ) ;
533
+ // Remove trailing .00 if it exists
534
+ return dollars . replace ( / \. ? 0 + $ / , '' ) ;
384
535
}
385
536
386
537
private handlePriceChange ( e : Event ) {
387
- const value = ( e . target as HTMLInputElement ) . value ;
388
- const price = Math . round ( Number ( parseFloat ( value ) ) * 100 ) ;
389
- this . updatePrice ( price ) ;
538
+ const input = e . target as HTMLInputElement ;
539
+ let value = input . value ;
540
+
541
+ // Remove any non-numeric or non-decimal characters
542
+ value = value . replace ( / [ ^ \d . ] / g, '' ) ;
543
+
544
+ // Ensure only one decimal point
545
+ const parts = value . split ( '.' ) ;
546
+ if ( parts . length > 2 ) {
547
+ value = parts [ 0 ] + '.' + parts . slice ( 1 ) . join ( '' ) ;
548
+ }
549
+
550
+ // Limit decimal places to 2
551
+ if ( parts . length === 2 ) {
552
+ value = parts [ 0 ] + '.' + parts [ 1 ] . slice ( 0 , 2 ) ;
553
+ }
554
+
555
+ // Update the input value
556
+ input . value = value ;
557
+
558
+ // Convert to cents for storage
559
+ const dollars = parseFloat ( value || '0' ) ;
560
+ if ( dollars * 100 > this . MAX_PRICE_CENTS ) {
561
+ input . value = ( this . MAX_PRICE_CENTS / 100 ) . toString ( ) ;
562
+ this . updatePrice ( this . MAX_PRICE_CENTS ) ;
563
+ } else {
564
+ const cents = Math . ceil ( dollars * 100 ) ;
565
+ this . updatePrice ( Math . max ( 1 , cents ) ) ;
566
+ }
390
567
}
391
568
392
569
private handlePercentageChange ( e : Event ) {
393
- const value = ( e . target as HTMLInputElement ) . value ;
394
- this . pricePercentage = Number ( parseFloat ( value ) . toFixed ( 1 ) ) ;
570
+ const input = e . target as HTMLInputElement ;
571
+ const value = parseFloat ( input . value ) ;
572
+ this . pricePercentage = value ;
573
+
574
+ // Update the slider progress - normalize to 0-100 based on min-max range
575
+ requestAnimationFrame ( ( ) => {
576
+ const normalizedValue = ( ( value - 80 ) / ( 120 - 80 ) ) * 100 ;
577
+ input . style . setProperty ( '--slider-percentage' , normalizedValue . toString ( ) ) ;
578
+ } ) ;
579
+
395
580
if ( this . recommendedPrice ) {
396
- const newPrice = Math . round ( ( this . pricePercentage / 100 ) * this . recommendedPrice ) ;
581
+ const exactPrice = ( value / 100 ) * this . recommendedPrice ;
582
+ const newPrice = Math . ceil ( exactPrice ) ;
397
583
this . updatePrice ( newPrice ) ;
398
584
}
399
585
}
@@ -508,27 +694,34 @@ export class ListItemModal extends FloatElement {
508
694
? `$${ ( this . recommendedPrice / 100 ) . toFixed ( 2 ) } `
509
695
: 'N/A' }
510
696
</ label >
511
- < input
512
- type ="number "
513
- step ="0.01 "
514
- min ="0 "
515
- max ="100000 "
516
- class ="price-input "
517
- .value ="${ this . customPrice ? ( this . customPrice / 100 ) . toFixed ( 2 ) : '' } "
518
- @input ="${ this . handlePriceChange } "
519
- placeholder ="${ this . listingType === 'buy_now'
520
- ? 'Enter listing price in USD (max $100,000)'
521
- : 'Enter starting price in USD (max $100,000)' } "
522
- />
697
+ < div class ="price-input-container ">
698
+ < span class ="price-input-prefix "> $</ span >
699
+ < input
700
+ type ="text "
701
+ inputmode ="decimal "
702
+ class ="price-input "
703
+ .value ="${ this . customPrice ? this . formatInputPrice ( this . customPrice ) : '' } "
704
+ @input ="${ this . handlePriceChange } "
705
+ placeholder ="${ this . listingType === 'buy_now'
706
+ ? 'Enter listing price in USD (max $100,000)'
707
+ : 'Enter starting price in USD (max $100,000)' } "
708
+ />
709
+ </ div >
523
710
< input
524
711
type ="range "
525
712
min ="80 "
526
713
max ="120 "
714
+ step ="0.1 "
527
715
.value ="${ this . pricePercentage } "
528
716
@input ="${ this . handlePercentageChange } "
529
717
class ="percentage-slider "
530
718
/>
531
- < div > Percentage of recommended price: ${ this . pricePercentage . toFixed ( 0 ) } %</ div >
719
+ < div >
720
+ Percentage of recommended price:
721
+ ${ this . recommendedPrice && this . customPrice
722
+ ? Math . round ( ( this . customPrice / this . recommendedPrice ) * 100 )
723
+ : 100 } %
724
+ </ div >
532
725
533
726
${ this . listingType === 'auction'
534
727
? html `
@@ -561,6 +754,28 @@ export class ListItemModal extends FloatElement {
561
754
</ div >
562
755
563
756
${ this . error ? html `< div class ="error-message "> ${ this . error } </ div > ` : '' }
757
+ ${ this . customPrice
758
+ ? html `
759
+ < div class ="price-breakdown ">
760
+ < div class ="price-breakdown-row ">
761
+ < span > Subtotal</ span >
762
+ < span > $${ this . formatPrice ( this . customPrice ) } </ span >
763
+ </ div >
764
+ < div class ="price-breakdown-row ">
765
+ < span > Sale Fee (2%)</ span >
766
+ < span > -$${ this . formatPrice ( this . getSaleFee ( this . customPrice ) ) } </ span >
767
+ </ div >
768
+ < div class ="price-breakdown-row ">
769
+ < span > Total Earnings</ span >
770
+ < span
771
+ > $${ this . formatPrice (
772
+ this . customPrice - this . getSaleFee ( this . customPrice )
773
+ ) } </ span
774
+ >
775
+ </ div >
776
+ </ div >
777
+ `
778
+ : '' }
564
779
565
780
< button
566
781
class ="submit-button "
0 commit comments