@@ -4,6 +4,54 @@ import { Input } from '../../components/Input';
44import { useAuth } from '../../context/AuthContext' ;
55import type { AppConfig } from '../../types' ;
66
7+ function computeSpeedBonus ( position : number , totalPlayers : number , max : number , min : number ) {
8+ if ( totalPlayers <= 1 ) return max ;
9+ return Math . max ( Math . round ( max - ( max - min ) * ( position / ( totalPlayers - 1 ) ) ) , min ) ;
10+ }
11+
12+ function SpeedBonusPreview ( { max, min } : { max : number ; min : number } ) {
13+ const examples = [ 5 , 10 , 20 ] ;
14+ return (
15+ < div
16+ style = { {
17+ marginTop : 12 ,
18+ background : 'var(--surface2)' ,
19+ border : '1px solid var(--border)' ,
20+ borderRadius : 'var(--radius-sm)' ,
21+ padding : '12px 14px' ,
22+ } }
23+ >
24+ < p style = { { fontSize : '0.78rem' , color : 'var(--text2)' , marginBottom : 8 , fontWeight : 600 } } >
25+ Preview
26+ </ p >
27+ < div style = { { display : 'flex' , gap : 16 , flexWrap : 'wrap' } } >
28+ { examples . map ( ( n ) => (
29+ < div key = { n } style = { { fontSize : '0.78rem' , lineHeight : 1.6 } } >
30+ < span style = { { color : 'var(--text2)' } } > { n } players:</ span >
31+ < br />
32+ { Array . from ( { length : Math . min ( n , 5 ) } , ( _ , i ) => {
33+ const bonus = computeSpeedBonus ( i , n , max , min ) ;
34+ return (
35+ // biome-ignore lint/suspicious/noArrayIndexKey: stable list based on player count
36+ < span key = { i } style = { { color : 'var(--accent2)' } } >
37+ { i + 1 } st={ bonus }
38+ { i < Math . min ( n , 5 ) - 1 ? ', ' : '' }
39+ </ span >
40+ ) ;
41+ } ) }
42+ { n > 5 && (
43+ < span style = { { color : 'var(--text3)' } } >
44+ { ' ' }
45+ ... { n } th={ computeSpeedBonus ( n - 1 , n , max , min ) }
46+ </ span >
47+ ) }
48+ </ div >
49+ ) ) }
50+ </ div >
51+ </ div >
52+ ) ;
53+ }
54+
755export default function Settings ( ) {
856 const { token } = useAuth ( ) ;
957 const [ cfg , setCfg ] = useState < Partial < AppConfig > | null > ( null ) ;
@@ -135,7 +183,7 @@ export default function Settings() {
135183
136184 { /* Branding */ }
137185 < div className = "card mb-6" style = { { maxWidth : 480 } } >
138- < h2 className = "mb-1" > ✏️ App Name </ h2 >
186+ < h2 className = "mb-1" > ✏️ Branding </ h2 >
139187 < p className = "text-sm text-muted mb-4" >
140188 Shown as{ ' ' }
141189 < strong style = { { color : 'var(--text)' } } >
@@ -148,8 +196,17 @@ export default function Settings() {
148196 placeholder = "e.g. Scaleway (leave blank for default)"
149197 value = { cfg . appName ?? '' }
150198 onChange = { ( e ) => update ( 'appName' , e . target . value ) }
199+ />
200+ < Input
201+ label = "Join page subtitle"
202+ placeholder = "e.g. Quizz of the day — Cloud Edition"
203+ value = { cfg . appSubtitle ?? '' }
204+ onChange = { ( e ) => update ( 'appSubtitle' , e . target . value ) }
151205 noMargin
152206 />
207+ < p className = "text-xs text-muted mt-2" >
208+ Displayed on the player join screen below the logo. Leave blank to hide.
209+ </ p >
153210 </ div >
154211
155212 < div
@@ -181,15 +238,6 @@ export default function Settings() {
181238 onChange = { ( e ) => update ( 'defaultBaseScore' , Number ( e . target . value ) ) }
182239 />
183240
184- < Input
185- label = "Default Speed Bonus (for players beyond top list)"
186- type = "number"
187- min = { 0 }
188- step = { 5 }
189- value = { cfg . defaultSpeedBonus ?? 25 }
190- onChange = { ( e ) => update ( 'defaultSpeedBonus' , Number ( e . target . value ) ) }
191- />
192-
193241 < Input
194242 label = "Max Players Per Session"
195243 type = "number"
@@ -200,52 +248,34 @@ export default function Settings() {
200248 />
201249
202250 < div className = "form-group" >
203- < p className = "form-label" > Speed Bonuses (1st correct → 2nd → 3rd → …)</ p >
204- < div style = { { display : 'flex' , flexDirection : 'column' , gap : 6 , marginBottom : 6 } } >
205- { ( cfg . speedBonuses ?? [ 200 , 150 , 100 , 50 ] ) . map ( ( val , i ) => (
206- < div key = { `speed-${ i + 1 } ` } className = "flex items-center gap-2" >
207- < span style = { { fontSize : '0.78rem' , color : 'var(--text2)' , minWidth : 54 } } >
208- { i + 1 }
209- { i === 0 ? 'st' : i === 1 ? 'nd' : i === 2 ? 'rd' : 'th' } correct
210- </ span >
211- < Input
212- type = "number"
213- min = { 0 }
214- step = { 10 }
215- value = { val }
216- style = { { maxWidth : 110 } }
217- onChange = { ( e ) => {
218- const bonuses = [ ...( cfg . speedBonuses ?? [ ] ) ] ;
219- bonuses [ i ] = Number ( e . target . value ) ;
220- update ( 'speedBonuses' , bonuses ) ;
221- } }
222- />
223- { ( cfg . speedBonuses ?? [ ] ) . length > 1 && (
224- < button
225- type = "button"
226- className = "btn-icon"
227- style = { { fontSize : '0.8rem' } }
228- onClick = { ( ) => {
229- const bonuses = ( cfg . speedBonuses ?? [ ] ) . filter ( ( _ , idx ) => idx !== i ) ;
230- update ( 'speedBonuses' , bonuses ) ;
231- } }
232- >
233- ✕
234- </ button >
235- ) }
236- </ div >
237- ) ) }
238- </ div >
239- < button
240- type = "button"
241- className = "btn btn-ghost btn-sm"
242- onClick = { ( ) => update ( 'speedBonuses' , [ ...( cfg . speedBonuses ?? [ ] ) , 0 ] ) }
243- >
244- + Add Tier
245- </ button >
246- < p className = "text-xs text-muted mt-2" >
247- Players beyond the last tier use the Default Speed Bonus above.
251+ < p className = "form-label" style = { { marginBottom : 4 } } >
252+ Speed Bonus (awarded to correct answerers based on answer speed)
248253 </ p >
254+ < p className = "text-xs text-muted mb-4" >
255+ Scales linearly from max to min based on answer position relative to total players.
256+ The 1st correct answerer gets the max bonus, the last gets the min.
257+ </ p >
258+ < div className = "form-row" >
259+ < Input
260+ label = "Max bonus (1st correct)"
261+ type = "number"
262+ min = { 0 }
263+ step = { 10 }
264+ value = { cfg . speedBonusMax ?? 200 }
265+ onChange = { ( e ) => update ( 'speedBonusMax' , Number ( e . target . value ) ) }
266+ noMargin
267+ />
268+ < Input
269+ label = "Min bonus (last correct)"
270+ type = "number"
271+ min = { 0 }
272+ step = { 5 }
273+ value = { cfg . speedBonusMin ?? 10 }
274+ onChange = { ( e ) => update ( 'speedBonusMin' , Number ( e . target . value ) ) }
275+ noMargin
276+ />
277+ </ div >
278+ < SpeedBonusPreview max = { cfg . speedBonusMax ?? 200 } min = { cfg . speedBonusMin ?? 10 } />
249279 </ div >
250280
251281 < div className = "form-group" >
@@ -264,19 +294,6 @@ export default function Settings() {
264294 noMargin
265295 />
266296 </ div >
267-
268- < div className = "form-group flex items-center gap-3" style = { { marginBottom : 0 } } >
269- < input
270- type = "checkbox"
271- id = "lateJoin"
272- style = { { width : 'auto' } }
273- checked = { cfg . allowLateJoin ?? false }
274- onChange = { ( e ) => update ( 'allowLateJoin' , e . target . checked ) }
275- />
276- < label htmlFor = "lateJoin" style = { { marginBottom : 0 } } >
277- Allow late join (players can join mid-game)
278- </ label >
279- </ div >
280297 </ div >
281298
282299 { /* Streak bonus settings */ }
0 commit comments