Compare commits
7 Commits
22eca7d4e0
...
f9d63c6c49
Author | SHA1 | Date | |
---|---|---|---|
f9d63c6c49 | |||
a7929cf144 | |||
77df851e95 | |||
6c9f1c3348 | |||
fca6baa694 | |||
38ed69bf5b | |||
50e245233c |
@ -10,7 +10,7 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fontsource/bangers": "^5.1.1",
|
"@fontsource/press-start-2p": "^5.2.5",
|
||||||
"@fortawesome/fontawesome-svg-core": "^6.7.2",
|
"@fortawesome/fontawesome-svg-core": "^6.7.2",
|
||||||
"@fortawesome/free-solid-svg-icons": "^6.7.2",
|
"@fortawesome/free-solid-svg-icons": "^6.7.2",
|
||||||
"@fortawesome/react-fontawesome": "^0.2.2",
|
"@fortawesome/react-fontawesome": "^0.2.2",
|
||||||
|
12
client/pnpm-lock.yaml
generated
12
client/pnpm-lock.yaml
generated
@ -8,9 +8,9 @@ importers:
|
|||||||
|
|
||||||
.:
|
.:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@fontsource/bangers':
|
'@fontsource/press-start-2p':
|
||||||
specifier: ^5.1.1
|
specifier: ^5.2.5
|
||||||
version: 5.1.1
|
version: 5.2.5
|
||||||
'@fortawesome/fontawesome-svg-core':
|
'@fortawesome/fontawesome-svg-core':
|
||||||
specifier: ^6.7.2
|
specifier: ^6.7.2
|
||||||
version: 6.7.2
|
version: 6.7.2
|
||||||
@ -327,8 +327,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-JubJ5B2pJ4k4yGxaNLdbjrnk9d/iDz6/q8wOilpIowd6PJPgaxCuHBnBszq7Ce2TyMrywm5r4PnKm6V3iiZF+g==}
|
resolution: {integrity: sha512-JubJ5B2pJ4k4yGxaNLdbjrnk9d/iDz6/q8wOilpIowd6PJPgaxCuHBnBszq7Ce2TyMrywm5r4PnKm6V3iiZF+g==}
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
|
|
||||||
'@fontsource/bangers@5.1.1':
|
'@fontsource/press-start-2p@5.2.5':
|
||||||
resolution: {integrity: sha512-+OzMvd6OXRipNWjGtcRTz1kVn+ML2A+4OsejqroZeUju74qncQ6lhlP3cZzu6hijXR8U9rm603lufzYdVAOdsA==}
|
resolution: {integrity: sha512-MmGLqhkv0kuoyeGgGkquEMRxJP6auc6918bKd8uTWP2beXMWLZZwCfXCqmskFLf0XYbtbzxuRXLjTnQBeTwsMQ==}
|
||||||
|
|
||||||
'@fortawesome/fontawesome-common-types@6.7.2':
|
'@fortawesome/fontawesome-common-types@6.7.2':
|
||||||
resolution: {integrity: sha512-Zs+YeHUC5fkt7Mg1l6XTniei3k4bwG/yo3iFUtZWd/pMx9g3fdvkSK9E0FOC+++phXOka78uJcYb8JaFkW52Xg==}
|
resolution: {integrity: sha512-Zs+YeHUC5fkt7Mg1l6XTniei3k4bwG/yo3iFUtZWd/pMx9g3fdvkSK9E0FOC+++phXOka78uJcYb8JaFkW52Xg==}
|
||||||
@ -1786,7 +1786,7 @@ snapshots:
|
|||||||
'@eslint/core': 0.12.0
|
'@eslint/core': 0.12.0
|
||||||
levn: 0.4.1
|
levn: 0.4.1
|
||||||
|
|
||||||
'@fontsource/bangers@5.1.1': {}
|
'@fontsource/press-start-2p@5.2.5': {}
|
||||||
|
|
||||||
'@fortawesome/fontawesome-common-types@6.7.2': {}
|
'@fortawesome/fontawesome-common-types@6.7.2': {}
|
||||||
|
|
||||||
|
@ -1,53 +1,83 @@
|
|||||||
// Button styles for the Song Battle application
|
// Button styles for the Song Battle application - Pixel Art Theme
|
||||||
|
|
||||||
// Base button style
|
// Base button style
|
||||||
.btn
|
.btn
|
||||||
display: inline-block
|
display: inline-block
|
||||||
padding: 0.75rem 1.5rem
|
padding: 0.75rem 1.5rem
|
||||||
border: none
|
border: 4px solid #000
|
||||||
border-radius: 0.5rem
|
border-radius: 0
|
||||||
|
font-family: 'Press Start 2P', monospace
|
||||||
|
font-size: 0.9rem
|
||||||
font-weight: 600
|
font-weight: 600
|
||||||
cursor: pointer
|
cursor: pointer
|
||||||
transition: all 0.3s ease
|
|
||||||
text-transform: uppercase
|
text-transform: uppercase
|
||||||
letter-spacing: 1px
|
letter-spacing: 1px
|
||||||
position: relative
|
position: relative
|
||||||
overflow: hidden
|
|
||||||
text-align: center
|
text-align: center
|
||||||
min-width: 120px
|
min-width: 120px
|
||||||
|
background-color: $primary
|
||||||
|
color: #000
|
||||||
|
// Enhanced pixel art style with jagged edges
|
||||||
|
image-rendering: pixelated
|
||||||
|
box-shadow: 4px 4px 0 #000, inset -4px -4px 0 darken($primary, 30%), inset 4px 4px 0 lighten($primary, 10%)
|
||||||
|
transition: all 0.1s ease
|
||||||
|
|
||||||
// Sheen animation on hover
|
// Add decorative pixel corners
|
||||||
&:before
|
&:before, &:after
|
||||||
content: ''
|
content: ''
|
||||||
position: absolute
|
position: absolute
|
||||||
top: 0
|
width: 4px
|
||||||
left: -100%
|
height: 4px
|
||||||
width: 100%
|
background-color: #000
|
||||||
height: 100%
|
z-index: 2
|
||||||
background: linear-gradient(
|
|
||||||
120deg,
|
|
||||||
transparent,
|
|
||||||
rgba(255, 255, 255, 0.3),
|
|
||||||
transparent
|
|
||||||
)
|
|
||||||
transition: all 0.6s
|
|
||||||
|
|
||||||
&:hover:before
|
&:before
|
||||||
left: 100%
|
top: -4px
|
||||||
|
left: -4px
|
||||||
|
|
||||||
|
&:after
|
||||||
|
bottom: -4px
|
||||||
|
right: -4px
|
||||||
|
|
||||||
&:hover
|
&:hover
|
||||||
transform: translateY(-3px)
|
transform: translate(-2px, -2px)
|
||||||
box-shadow: 0 7px 15px rgba(0, 0, 0, 0.3)
|
box-shadow: 6px 6px 0 #000, inset -4px -4px 0 darken($primary, 30%), inset 4px 4px 0 lighten($primary, 10%)
|
||||||
|
|
||||||
&:active
|
&:active
|
||||||
transform: translateY(0)
|
transform: translate(4px, 4px)
|
||||||
box-shadow: 0 3px 5px rgba(0, 0, 0, 0.2)
|
box-shadow: 0 0 0 #000, inset -4px -4px 0 darken($primary, 30%), inset 4px 4px 0 lighten($primary, 10%)
|
||||||
|
|
||||||
&:disabled
|
&:disabled
|
||||||
opacity: 0.5
|
opacity: 0.5
|
||||||
cursor: not-allowed
|
|
||||||
transform: none
|
// Full width modifier
|
||||||
box-shadow: none
|
&.full-width
|
||||||
|
width: 100%
|
||||||
|
margin-top: 1rem
|
||||||
|
padding: 1rem
|
||||||
|
font-size: 1rem
|
||||||
|
|
||||||
|
// Button types
|
||||||
|
&.primary
|
||||||
|
background-color: $primary
|
||||||
|
color: #000
|
||||||
|
box-shadow: 4px 4px 0 #000, inset -4px -4px 0 darken($primary, 30%), inset 4px 4px 0 lighten($primary, 10%)
|
||||||
|
|
||||||
|
&.secondary
|
||||||
|
background-color: $secondary
|
||||||
|
color: #000
|
||||||
|
box-shadow: 4px 4px 0 #000, inset -4px -4px 0 darken($secondary, 30%), inset 4px 4px 0 lighten($secondary, 10%)
|
||||||
|
|
||||||
|
&.accent
|
||||||
|
background-color: $accent
|
||||||
|
color: #000
|
||||||
|
box-shadow: 4px 4px 0 #000, inset -4px -4px 0 darken($accent, 30%), inset 4px 4px 0 lighten($accent, 10%)
|
||||||
|
|
||||||
|
@keyframes pixel-breathe
|
||||||
|
0%, 100%
|
||||||
|
transform: scale(1)
|
||||||
|
50%
|
||||||
|
transform: scale(1.01)
|
||||||
|
|
||||||
// Primary button
|
// Primary button
|
||||||
.btn.primary
|
.btn.primary
|
||||||
|
@ -1,15 +1,20 @@
|
|||||||
// Colors for Song Battle application
|
// Colors for Song Battle application - Pixel Art Theme
|
||||||
|
|
||||||
// Main colors
|
// Main colors
|
||||||
$background: #0f0f1a // Kept for compatibility
|
$background: #1a1126 // Dark purple background for pixel art feel
|
||||||
$card-bg: rgba(20, 20, 35, 0.85) // Semi-transparent dark card background
|
$card-bg: rgba(32, 24, 48, 0.9) // Semi-transparent dark purple card background
|
||||||
$text: #ffffff
|
$text: #ffffff
|
||||||
$text-muted: rgba(255, 255, 255, 0.7) // Brighter for better contrast with gradient
|
$text-muted: rgba(255, 255, 255, 0.8) // Brighter for better contrast with gradient
|
||||||
|
|
||||||
// Brand colors
|
// Brand colors - More saturated for pixel art aesthetic
|
||||||
$primary: #f0c3ff // Light purple - complementary to gradient
|
$primary: #ff55ff // Bright magenta - pixel art style
|
||||||
$secondary: #00e5ff // Bright cyan - stands out against pink/purple
|
$secondary: #00ddff // Bright cyan - pixel art style
|
||||||
$accent: #ffcc00 // Bright gold - contrasts with pink/purple
|
$accent: #ffcc00 // Bright gold - pixel art style
|
||||||
|
|
||||||
|
// Pixel art accent colors
|
||||||
|
$pixel-purple: #9900ff
|
||||||
|
$pixel-blue: #0088ff
|
||||||
|
$pixel-green: #00ff66
|
||||||
|
|
||||||
// Status colors
|
// Status colors
|
||||||
$success: #00c853
|
$success: #00c853
|
||||||
|
@ -9,73 +9,133 @@
|
|||||||
padding: 2rem
|
padding: 2rem
|
||||||
position: relative
|
position: relative
|
||||||
|
|
||||||
|
// Pixel art floating items in background
|
||||||
|
&:before, &:after
|
||||||
|
content: ''
|
||||||
|
position: absolute
|
||||||
|
width: 12px
|
||||||
|
height: 12px
|
||||||
|
background-color: $pixel-blue
|
||||||
|
animation: pixel-float 3s infinite alternate ease-in-out
|
||||||
|
z-index: -1
|
||||||
|
|
||||||
|
&:before
|
||||||
|
top: 15%
|
||||||
|
left: 20%
|
||||||
|
box-shadow: 0 0 10px $pixel-blue
|
||||||
|
animation-delay: 0s
|
||||||
|
|
||||||
|
&:after
|
||||||
|
bottom: 20%
|
||||||
|
right: 15%
|
||||||
|
background-color: $pixel-purple
|
||||||
|
box-shadow: 0 0 10px $pixel-purple
|
||||||
|
animation-delay: 1.5s
|
||||||
|
|
||||||
.logo
|
.logo
|
||||||
display: flex
|
display: flex
|
||||||
flex-direction: column
|
flex-direction: column
|
||||||
align-items: center
|
align-items: center
|
||||||
margin-bottom: 2rem
|
margin-bottom: 3rem
|
||||||
|
|
||||||
.logo-image
|
.logo-image
|
||||||
image-rendering: pixelated
|
image-rendering: pixelated
|
||||||
width: 120px
|
width: 180px
|
||||||
height: auto
|
height: auto
|
||||||
margin-bottom: 1rem
|
margin-bottom: 1.5rem
|
||||||
animation: pulse 2s infinite ease-in-out
|
animation: pixel-float 3s infinite ease-in-out
|
||||||
filter: drop-shadow(0 0 10px rgba(255, 255, 255, 0.7))
|
filter: drop-shadow(0 0 15px rgba(255, 255, 255, 0.5))
|
||||||
|
|
||||||
h1
|
h1
|
||||||
font-size: 3.5rem
|
font-size: 2.5rem
|
||||||
margin: 0
|
margin: 0
|
||||||
color: white
|
font-family: 'Press Start 2P', monospace
|
||||||
text-shadow: 0 0 10px rgba(0, 0, 0, 0.5), 2px 2px 4px rgba(0, 0, 0, 0.3)
|
color: $primary
|
||||||
|
animation: pixel-flash 3s infinite
|
||||||
|
letter-spacing: 2px
|
||||||
|
text-transform: uppercase
|
||||||
|
|
||||||
.card
|
.card
|
||||||
background-color: $card-bg
|
background-color: $card-bg
|
||||||
border-radius: 1rem
|
|
||||||
padding: 2rem
|
padding: 2rem
|
||||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5), 0 0 15px rgba(255, 255, 255, 0.1)
|
box-shadow: 8px 8px 0 #000
|
||||||
border: 1px solid rgba(255, 255, 255, 0.1)
|
border: 4px solid #000
|
||||||
backdrop-filter: blur(10px)
|
|
||||||
width: 100%
|
width: 100%
|
||||||
max-width: 500px
|
max-width: 500px
|
||||||
|
position: relative
|
||||||
|
overflow: visible
|
||||||
|
|
||||||
.tabs
|
// Pixel art decorations
|
||||||
display: flex
|
&:before
|
||||||
margin-bottom: 1.5rem
|
content: ''
|
||||||
border-bottom: 2px solid rgba(255, 255, 255, 0.1)
|
position: absolute
|
||||||
|
top: -10px
|
||||||
|
left: -10px
|
||||||
|
width: 20px
|
||||||
|
height: 20px
|
||||||
|
background-color: $accent
|
||||||
|
z-index: 1
|
||||||
|
animation: pixel-rotate 8s linear infinite
|
||||||
|
|
||||||
button
|
&:after
|
||||||
flex: 1
|
content: ''
|
||||||
background: none
|
position: absolute
|
||||||
border: none
|
bottom: -10px
|
||||||
color: $text-muted
|
right: -10px
|
||||||
padding: 1rem
|
width: 20px
|
||||||
font-size: 1rem
|
height: 20px
|
||||||
cursor: pointer
|
background-color: $primary
|
||||||
transition: color 0.2s, border-bottom 0.2s
|
z-index: 1
|
||||||
position: relative
|
animation: pixel-rotate 8s linear infinite reverse
|
||||||
|
|
||||||
&:after
|
@keyframes pixel-rotate
|
||||||
content: ''
|
0%
|
||||||
position: absolute
|
transform: rotate(0deg)
|
||||||
bottom: -2px
|
100%
|
||||||
left: 0
|
transform: rotate(360deg)
|
||||||
width: 100%
|
|
||||||
height: 2px
|
|
||||||
background-color: transparent
|
|
||||||
transition: background-color 0.2s
|
|
||||||
|
|
||||||
&.active
|
.tabs
|
||||||
color: $primary
|
display: flex
|
||||||
|
margin-bottom: 1.5rem
|
||||||
|
border-bottom: 4px solid #000
|
||||||
|
|
||||||
&:after
|
button
|
||||||
background-color: $primary
|
flex: 1
|
||||||
|
background: none
|
||||||
.home-footer
|
border: 4px solid #000
|
||||||
margin-top: 2rem
|
border-bottom: none
|
||||||
text-align: center
|
|
||||||
color: $text-muted
|
color: $text-muted
|
||||||
font-style: italic
|
font-family: 'Press Start 2P', monospace
|
||||||
|
font-size: 0.7rem
|
||||||
|
padding: 0.75rem 0.5rem
|
||||||
|
position: relative
|
||||||
|
text-transform: uppercase
|
||||||
|
transition: all 0.2s ease
|
||||||
|
cursor: pointer
|
||||||
|
|
||||||
|
&:hover:not(.active)
|
||||||
|
background-color: rgba($primary, 0.2)
|
||||||
|
color: $text
|
||||||
|
|
||||||
|
&.active
|
||||||
|
background-color: $primary
|
||||||
|
color: #000
|
||||||
|
z-index: 1
|
||||||
|
|
||||||
|
&:after
|
||||||
|
content: ''
|
||||||
|
position: absolute
|
||||||
|
bottom: -4px
|
||||||
|
left: 0
|
||||||
|
width: 100%
|
||||||
|
height: 4px
|
||||||
|
background-color: $primary
|
||||||
|
|
||||||
|
.home-footer
|
||||||
|
margin-top: 2rem
|
||||||
|
text-align: center
|
||||||
|
color: $text-muted
|
||||||
|
font-style: italic
|
||||||
|
|
||||||
@keyframes pulse
|
@keyframes pulse
|
||||||
0%
|
0%
|
||||||
|
@ -0,0 +1,191 @@
|
|||||||
|
// Lobby Screen styles
|
||||||
|
|
||||||
|
.lobby-screen
|
||||||
|
display: flex
|
||||||
|
flex-direction: column
|
||||||
|
min-height: 100%
|
||||||
|
padding: 1.5rem
|
||||||
|
|
||||||
|
.lobby-header
|
||||||
|
display: flex
|
||||||
|
justify-content: space-between
|
||||||
|
align-items: center
|
||||||
|
margin-bottom: 2rem
|
||||||
|
flex-wrap: wrap
|
||||||
|
gap: 1rem
|
||||||
|
|
||||||
|
h1
|
||||||
|
margin: 0
|
||||||
|
color: $primary
|
||||||
|
font-family: 'Press Start 2P', monospace
|
||||||
|
font-size: 1.8rem
|
||||||
|
text-transform: uppercase
|
||||||
|
position: relative
|
||||||
|
letter-spacing: 2px
|
||||||
|
|
||||||
|
&:after
|
||||||
|
content: ''
|
||||||
|
position: absolute
|
||||||
|
bottom: -10px
|
||||||
|
left: 0
|
||||||
|
width: 100%
|
||||||
|
height: 4px
|
||||||
|
background-color: $primary
|
||||||
|
|
||||||
|
.lobby-code
|
||||||
|
display: flex
|
||||||
|
align-items: center
|
||||||
|
background-color: $card-bg
|
||||||
|
padding: 0.75rem 1rem
|
||||||
|
border: 4px solid #000
|
||||||
|
box-shadow: 4px 4px 0 #000
|
||||||
|
|
||||||
|
p
|
||||||
|
margin: 0 1rem 0 0
|
||||||
|
font-family: 'Press Start 2P', monospace
|
||||||
|
font-size: 0.7rem
|
||||||
|
text-transform: uppercase
|
||||||
|
|
||||||
|
.code
|
||||||
|
font-family: 'Press Start 2P', monospace
|
||||||
|
font-weight: bold
|
||||||
|
color: $secondary
|
||||||
|
font-size: 1rem
|
||||||
|
letter-spacing: 2px
|
||||||
|
background-color: rgba(0, 0, 0, 0.3)
|
||||||
|
padding: 0.5rem
|
||||||
|
border: 2px solid $secondary
|
||||||
|
|
||||||
|
.btn
|
||||||
|
padding: 0.25rem 0.5rem
|
||||||
|
font-size: 0.7rem
|
||||||
|
|
||||||
|
.lobby-content
|
||||||
|
display: flex
|
||||||
|
flex-direction: column
|
||||||
|
flex-grow: 1
|
||||||
|
gap: 1.5rem
|
||||||
|
|
||||||
|
@media (min-width: 768px)
|
||||||
|
flex-direction: row
|
||||||
|
|
||||||
|
.settings-section, .players-section
|
||||||
|
background-color: $card-bg
|
||||||
|
border: 4px solid #000
|
||||||
|
box-shadow: 8px 8px 0 #000
|
||||||
|
padding: 1.5rem
|
||||||
|
position: relative
|
||||||
|
|
||||||
|
h2
|
||||||
|
margin-top: 0
|
||||||
|
margin-bottom: 1rem
|
||||||
|
color: $secondary
|
||||||
|
font-family: 'Press Start 2P', monospace
|
||||||
|
font-size: 1.2rem
|
||||||
|
text-transform: uppercase
|
||||||
|
|
||||||
|
&:after
|
||||||
|
content: ''
|
||||||
|
display: block
|
||||||
|
width: 100%
|
||||||
|
height: 4px
|
||||||
|
background-color: $secondary
|
||||||
|
margin-top: 0.5rem
|
||||||
|
|
||||||
|
.players-list
|
||||||
|
flex: 1
|
||||||
|
background-color: $card-bg
|
||||||
|
border-radius: 1rem
|
||||||
|
padding: 1.5rem
|
||||||
|
|
||||||
|
h2
|
||||||
|
margin-top: 0
|
||||||
|
display: flex
|
||||||
|
align-items: center
|
||||||
|
gap: 0.5rem
|
||||||
|
color: $primary
|
||||||
|
|
||||||
|
ul
|
||||||
|
list-style: none
|
||||||
|
padding: 0
|
||||||
|
margin: 0
|
||||||
|
|
||||||
|
li
|
||||||
|
padding: 0.75rem 1rem
|
||||||
|
margin-bottom: 0.5rem
|
||||||
|
border-radius: 0.5rem
|
||||||
|
background-color: rgba(255, 255, 255, 0.05)
|
||||||
|
display: flex
|
||||||
|
justify-content: space-between
|
||||||
|
align-items: center
|
||||||
|
|
||||||
|
&.host
|
||||||
|
border-left: 3px solid $primary
|
||||||
|
|
||||||
|
&.disconnected
|
||||||
|
opacity: 0.6
|
||||||
|
|
||||||
|
.status-disconnected
|
||||||
|
font-size: 0.8rem
|
||||||
|
color: $danger
|
||||||
|
margin-left: auto
|
||||||
|
|
||||||
|
.lobby-info
|
||||||
|
flex: 1
|
||||||
|
display: flex
|
||||||
|
flex-direction: column
|
||||||
|
|
||||||
|
.settings-preview
|
||||||
|
background-color: $card-bg
|
||||||
|
border-radius: 1rem
|
||||||
|
padding: 1.5rem
|
||||||
|
margin-bottom: 1.5rem
|
||||||
|
|
||||||
|
h3
|
||||||
|
margin-top: 0
|
||||||
|
color: $secondary
|
||||||
|
display: flex
|
||||||
|
align-items: center
|
||||||
|
gap: 0.5rem
|
||||||
|
|
||||||
|
.actions
|
||||||
|
display: flex
|
||||||
|
flex-direction: column
|
||||||
|
gap: 1rem
|
||||||
|
margin-top: auto
|
||||||
|
|
||||||
|
.warning
|
||||||
|
color: $warning
|
||||||
|
text-align: center
|
||||||
|
margin-top: 1rem
|
||||||
|
font-size: 0.9rem
|
||||||
|
|
||||||
|
// Modal overlay for settings
|
||||||
|
.modal-overlay
|
||||||
|
position: fixed
|
||||||
|
top: 0
|
||||||
|
left: 0
|
||||||
|
right: 0
|
||||||
|
bottom: 0
|
||||||
|
background-color: rgba(0, 0, 0, 0.7)
|
||||||
|
display: flex
|
||||||
|
justify-content: center
|
||||||
|
align-items: center
|
||||||
|
z-index: 100
|
||||||
|
|
||||||
|
.modal
|
||||||
|
background-color: $card-bg
|
||||||
|
border-radius: 1rem
|
||||||
|
padding: 2rem
|
||||||
|
width: 90%
|
||||||
|
max-width: 500px
|
||||||
|
|
||||||
|
h2
|
||||||
|
margin-top: 0
|
||||||
|
color: $primary
|
||||||
|
|
||||||
|
.modal-actions
|
||||||
|
display: flex
|
||||||
|
justify-content: flex-end
|
||||||
|
gap: 1rem
|
||||||
|
margin-top: 2rem
|
@ -8,48 +8,121 @@
|
|||||||
position: relative
|
position: relative
|
||||||
overflow: hidden
|
overflow: hidden
|
||||||
|
|
||||||
|
// Celebratory pixel confetti
|
||||||
|
&:before
|
||||||
|
content: ''
|
||||||
|
position: absolute
|
||||||
|
top: -20px
|
||||||
|
left: 0
|
||||||
|
width: 100%
|
||||||
|
height: 20px
|
||||||
|
background: repeating-linear-gradient(90deg, $primary 0px, $primary 10px, $accent 10px, $accent 20px, $secondary 20px, $secondary 30px)
|
||||||
|
animation: pixel-rain 8s linear infinite
|
||||||
|
|
||||||
|
@keyframes pixel-rain
|
||||||
|
0%
|
||||||
|
transform: translateY(-20px)
|
||||||
|
100%
|
||||||
|
transform: translateY(100vh)
|
||||||
|
|
||||||
header
|
header
|
||||||
display: flex
|
display: flex
|
||||||
justify-content: center
|
justify-content: center
|
||||||
margin-bottom: 2rem
|
margin-bottom: 3rem
|
||||||
|
position: relative
|
||||||
|
|
||||||
h1
|
h1
|
||||||
margin: 0
|
margin: 0
|
||||||
color: $primary
|
color: $accent
|
||||||
text-shadow: 0 0 8px rgba($primary, 0.5)
|
|
||||||
display: flex
|
display: flex
|
||||||
align-items: center
|
align-items: center
|
||||||
gap: 0.5rem
|
gap: 0.75rem
|
||||||
font-size: 2.5rem
|
font-size: 2.5rem
|
||||||
|
font-family: 'Press Start 2P', monospace
|
||||||
|
text-transform: uppercase
|
||||||
|
letter-spacing: 3px
|
||||||
|
animation: winner-pulse 2s infinite alternate
|
||||||
|
|
||||||
svg
|
svg
|
||||||
color: $accent
|
color: $accent
|
||||||
|
filter: drop-shadow(2px 2px 0 #000)
|
||||||
|
|
||||||
|
@keyframes winner-pulse
|
||||||
|
0%
|
||||||
|
transform: scale(1)
|
||||||
|
text-shadow: 2px 2px 0 #000, -2px -2px 0 #000, 2px -2px 0 #000, -2px 2px 0 #000
|
||||||
|
100%
|
||||||
|
transform: scale(1.05)
|
||||||
|
text-shadow: 3px 3px 0 #000, -3px -3px 0 #000, 3px -3px 0 #000, -3px 3px 0 #000
|
||||||
|
|
||||||
.winner-card
|
.winner-card
|
||||||
background-color: $card-bg
|
background-color: $card-bg
|
||||||
border-radius: 1rem
|
border: 8px solid $accent
|
||||||
padding: 1.5rem
|
padding: 2rem
|
||||||
margin-bottom: 2rem
|
margin-bottom: 2rem
|
||||||
display: flex
|
display: flex
|
||||||
flex-direction: column
|
flex-direction: column
|
||||||
gap: 1.5rem
|
gap: 1.5rem
|
||||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3)
|
|
||||||
position: relative
|
position: relative
|
||||||
overflow: hidden
|
overflow: hidden
|
||||||
|
box-shadow: 10px 10px 0 #000, 0 0 20px rgba($accent, 0.7)
|
||||||
|
|
||||||
&::after
|
// Trophy decoration
|
||||||
|
&:before
|
||||||
|
content: '🏆'
|
||||||
|
position: absolute
|
||||||
|
top: -15px
|
||||||
|
right: 20px
|
||||||
|
font-size: 2.5rem
|
||||||
|
transform: rotate(15deg)
|
||||||
|
filter: drop-shadow(2px 2px 0 #000)
|
||||||
|
z-index: 1
|
||||||
|
|
||||||
|
// Pixel border pattern
|
||||||
|
&:after
|
||||||
content: ''
|
content: ''
|
||||||
position: absolute
|
position: absolute
|
||||||
top: 0
|
top: 0
|
||||||
left: 0
|
left: 0
|
||||||
right: 0
|
right: 0
|
||||||
height: 5px
|
bottom: 0
|
||||||
background: linear-gradient(to right, $primary, $accent)
|
background: repeating-linear-gradient(45deg, transparent 0px, transparent 10px, rgba($accent, 0.2) 10px, rgba($accent, 0.2) 20px)
|
||||||
|
pointer-events: none
|
||||||
|
z-index: 0
|
||||||
|
|
||||||
@media (min-width: 768px)
|
@media (min-width: 768px)
|
||||||
flex-direction: row
|
flex-direction: row
|
||||||
align-items: stretch
|
align-items: stretch
|
||||||
|
|
||||||
|
.winner-content
|
||||||
|
z-index: 1
|
||||||
|
|
||||||
|
.winner-title
|
||||||
|
margin-top: 0
|
||||||
|
color: $accent
|
||||||
|
font-family: 'Press Start 2P', monospace
|
||||||
|
font-size: 1.8rem
|
||||||
|
text-transform: uppercase
|
||||||
|
margin-bottom: 1.5rem
|
||||||
|
|
||||||
|
.winner-info
|
||||||
|
margin-bottom: 1.5rem
|
||||||
|
|
||||||
|
.winner-song
|
||||||
|
font-family: 'Press Start 2P', monospace
|
||||||
|
font-size: 1.2rem
|
||||||
|
color: $text
|
||||||
|
margin-bottom: 0.5rem
|
||||||
|
|
||||||
|
.winner-player
|
||||||
|
font-family: 'Press Start 2P', monospace
|
||||||
|
font-size: 1rem
|
||||||
|
color: $primary
|
||||||
|
padding: 0.5rem
|
||||||
|
background-color: rgba($primary, 0.2)
|
||||||
|
border: 3px solid $primary
|
||||||
|
display: inline-block
|
||||||
|
|
||||||
.winner-info
|
.winner-info
|
||||||
flex: 1
|
flex: 1
|
||||||
|
|
||||||
|
54
client/src/common/styles/components/song-form-overlay.sass
Normal file
54
client/src/common/styles/components/song-form-overlay.sass
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
// Song Form Overlay styles
|
||||||
|
@import '../colors'
|
||||||
|
|
||||||
|
.song-form-overlay
|
||||||
|
position: fixed
|
||||||
|
top: 0
|
||||||
|
left: 0
|
||||||
|
width: 100%
|
||||||
|
height: 100%
|
||||||
|
background-color: rgba(0, 0, 0, 0.75)
|
||||||
|
z-index: 1000
|
||||||
|
display: flex
|
||||||
|
align-items: center
|
||||||
|
justify-content: center
|
||||||
|
padding: 2rem
|
||||||
|
backdrop-filter: blur(5px)
|
||||||
|
|
||||||
|
.song-form
|
||||||
|
background-color: $card-bg
|
||||||
|
border: 4px solid #000
|
||||||
|
box-shadow: 8px 8px 0 rgba(0, 0, 0, 0.5)
|
||||||
|
padding: 2rem
|
||||||
|
width: 100%
|
||||||
|
max-width: 600px
|
||||||
|
max-height: 90vh
|
||||||
|
overflow-y: auto
|
||||||
|
position: relative
|
||||||
|
image-rendering: pixelated
|
||||||
|
|
||||||
|
h3
|
||||||
|
margin-top: 0
|
||||||
|
color: $primary
|
||||||
|
font-family: 'Press Start 2P', monospace
|
||||||
|
text-transform: uppercase
|
||||||
|
letter-spacing: 1px
|
||||||
|
text-shadow: 2px 2px 0 #000
|
||||||
|
margin-bottom: 1.5rem
|
||||||
|
position: relative
|
||||||
|
|
||||||
|
&:after
|
||||||
|
content: ''
|
||||||
|
display: block
|
||||||
|
width: 100%
|
||||||
|
height: 4px
|
||||||
|
background-color: $primary
|
||||||
|
margin-top: 0.75rem
|
||||||
|
box-shadow: 2px 2px 0 #000
|
||||||
|
image-rendering: pixelated
|
||||||
|
|
||||||
|
.form-actions
|
||||||
|
display: flex
|
||||||
|
justify-content: flex-end
|
||||||
|
gap: 1rem
|
||||||
|
margin-top: 2rem
|
@ -17,20 +17,29 @@
|
|||||||
h1
|
h1
|
||||||
margin: 0
|
margin: 0
|
||||||
color: $primary
|
color: $primary
|
||||||
text-shadow: 0 0 8px rgba($primary, 0.5)
|
|
||||||
display: flex
|
display: flex
|
||||||
align-items: center
|
align-items: center
|
||||||
gap: 0.5rem
|
gap: 0.5rem
|
||||||
|
font-family: 'Press Start 2P', monospace
|
||||||
|
font-size: 1.8rem
|
||||||
|
text-transform: uppercase
|
||||||
|
letter-spacing: 2px
|
||||||
|
|
||||||
|
svg
|
||||||
|
filter: drop-shadow(2px 2px 0 #000)
|
||||||
|
|
||||||
.songs-counter
|
.songs-counter
|
||||||
background-color: rgba($card-bg, 0.7)
|
background-color: $card-bg
|
||||||
border-radius: 0.5rem
|
border: 4px solid #000
|
||||||
|
box-shadow: 4px 4px 0 #000
|
||||||
padding: 0.75rem 1rem
|
padding: 0.75rem 1rem
|
||||||
font-size: 1.1rem
|
font-size: 1rem
|
||||||
|
font-family: 'Press Start 2P', monospace
|
||||||
|
|
||||||
.counter
|
.counter
|
||||||
font-weight: bold
|
font-weight: bold
|
||||||
color: $primary
|
color: $secondary
|
||||||
|
text-shadow: 1px 1px 0 #000
|
||||||
|
|
||||||
.submission-content
|
.submission-content
|
||||||
display: flex
|
display: flex
|
||||||
@ -44,11 +53,79 @@
|
|||||||
.songs-list
|
.songs-list
|
||||||
flex: 1.5
|
flex: 1.5
|
||||||
background-color: $card-bg
|
background-color: $card-bg
|
||||||
border-radius: 1rem
|
border: 4px solid #000
|
||||||
|
box-shadow: 8px 8px 0 #000
|
||||||
padding: 1.5rem
|
padding: 1.5rem
|
||||||
|
|
||||||
h2
|
h2
|
||||||
margin-top: 0
|
margin-top: 0
|
||||||
|
color: $secondary
|
||||||
|
font-family: 'Press Start 2P', monospace
|
||||||
|
font-size: 1.2rem
|
||||||
|
text-transform: uppercase
|
||||||
|
margin-bottom: 1.5rem
|
||||||
|
text-shadow: 2px 2px 0 #000
|
||||||
|
|
||||||
|
&:after
|
||||||
|
content: ''
|
||||||
|
display: block
|
||||||
|
width: 100%
|
||||||
|
height: 4px
|
||||||
|
background-color: $secondary
|
||||||
|
margin-top: 0.5rem
|
||||||
|
box-shadow: 2px 2px 0 #000
|
||||||
|
image-rendering: pixelated
|
||||||
|
|
||||||
|
.song-item
|
||||||
|
margin-bottom: 1.5rem
|
||||||
|
padding: 1rem
|
||||||
|
background-color: rgba(0, 0, 0, 0.3)
|
||||||
|
border: 3px solid #000
|
||||||
|
position: relative
|
||||||
|
box-shadow: 4px 4px 0 rgba(0, 0, 0, 0.5)
|
||||||
|
transition: transform 0.2s ease
|
||||||
|
|
||||||
|
&:hover
|
||||||
|
transform: translateX(2px) translateY(-2px)
|
||||||
|
box-shadow: 6px 6px 0 rgba(0, 0, 0, 0.5)
|
||||||
|
|
||||||
|
.song-title
|
||||||
|
font-weight: bold
|
||||||
|
font-family: 'Press Start 2P', monospace
|
||||||
|
font-size: 0.9rem
|
||||||
|
color: $text
|
||||||
|
margin-bottom: 0.75rem
|
||||||
|
text-shadow: 1px 1px 0 #000
|
||||||
|
letter-spacing: 0.5px
|
||||||
|
|
||||||
|
.song-channel
|
||||||
|
font-size: 0.8rem
|
||||||
|
color: $text-muted
|
||||||
|
font-family: 'Press Start 2P', monospace
|
||||||
|
letter-spacing: 0.5px
|
||||||
|
text-shadow: 1px 1px 0 rgba(0,0,0,0.5)
|
||||||
|
|
||||||
|
.remove-song
|
||||||
|
position: absolute
|
||||||
|
top: 0.75rem
|
||||||
|
right: 0.75rem
|
||||||
|
background: $danger
|
||||||
|
color: #fff
|
||||||
|
border: 2px solid #000
|
||||||
|
width: 28px
|
||||||
|
height: 28px
|
||||||
|
font-family: 'Press Start 2P', monospace
|
||||||
|
font-size: 0.7rem
|
||||||
|
display: flex
|
||||||
|
align-items: center
|
||||||
|
justify-content: center
|
||||||
|
cursor: pointer
|
||||||
|
box-shadow: 2px 2px 0 #000
|
||||||
|
image-rendering: pixelated
|
||||||
|
|
||||||
|
&:hover
|
||||||
|
transform: translateY(-2px)
|
||||||
|
box-shadow: 2px 4px 0 #000
|
||||||
display: flex
|
display: flex
|
||||||
align-items: center
|
align-items: center
|
||||||
gap: 0.5rem
|
gap: 0.5rem
|
||||||
@ -62,6 +139,7 @@
|
|||||||
.song-card
|
.song-card
|
||||||
background-color: rgba(255, 255, 255, 0.05)
|
background-color: rgba(255, 255, 255, 0.05)
|
||||||
border-radius: 0.5rem
|
border-radius: 0.5rem
|
||||||
|
box-sizing: border-box
|
||||||
padding: 1rem
|
padding: 1rem
|
||||||
position: relative
|
position: relative
|
||||||
transition: transform 0.2s, box-shadow 0.2s
|
transition: transform 0.2s, box-shadow 0.2s
|
||||||
@ -112,7 +190,9 @@
|
|||||||
cursor: pointer
|
cursor: pointer
|
||||||
gap: 0.5rem
|
gap: 0.5rem
|
||||||
transition: all 0.2s
|
transition: all 0.2s
|
||||||
margin-top: 1rem
|
margin-top: 1.5rem
|
||||||
|
margin-bottom: 1rem
|
||||||
|
clear: both
|
||||||
|
|
||||||
&:hover
|
&:hover
|
||||||
background-color: rgba($primary, 0.1)
|
background-color: rgba($primary, 0.1)
|
||||||
|
@ -17,18 +17,25 @@
|
|||||||
h1
|
h1
|
||||||
margin: 0
|
margin: 0
|
||||||
color: $primary
|
color: $primary
|
||||||
text-shadow: 0 0 8px rgba($primary, 0.5)
|
|
||||||
display: flex
|
display: flex
|
||||||
align-items: center
|
align-items: center
|
||||||
gap: 0.5rem
|
gap: 0.5rem
|
||||||
|
text-transform: uppercase
|
||||||
|
font-size: 1.8rem
|
||||||
|
|
||||||
|
svg
|
||||||
|
filter: drop-shadow(2px 2px 0 #000)
|
||||||
|
|
||||||
.round-info
|
.round-info
|
||||||
background-color: rgba($card-bg, 0.7)
|
background-color: $card-bg
|
||||||
border-radius: 0.5rem
|
border: 4px solid #000
|
||||||
|
box-shadow: 4px 4px 0 #000
|
||||||
padding: 0.75rem 1rem
|
padding: 0.75rem 1rem
|
||||||
display: flex
|
display: flex
|
||||||
align-items: center
|
align-items: center
|
||||||
gap: 0.75rem
|
gap: 0.75rem
|
||||||
|
font-family: 'Press Start 2P', monospace
|
||||||
|
font-size: 0.8rem
|
||||||
|
|
||||||
span
|
span
|
||||||
font-weight: bold
|
font-weight: bold
|
||||||
@ -36,164 +43,325 @@
|
|||||||
.voted-badge
|
.voted-badge
|
||||||
background-color: $success
|
background-color: $success
|
||||||
color: #fff
|
color: #fff
|
||||||
border-radius: 1rem
|
border: 2px solid #000
|
||||||
padding: 0.25rem 0.75rem
|
padding: 0.25rem 0.75rem
|
||||||
font-size: 0.85rem
|
font-size: 0.75rem
|
||||||
animation: pulse 2s infinite
|
animation: pixel-pulse 2s infinite
|
||||||
|
|
||||||
.battle-container
|
@keyframes pixel-pulse
|
||||||
|
0%, 100%
|
||||||
|
transform: scale(1)
|
||||||
|
opacity: 1
|
||||||
|
50%
|
||||||
|
transform: scale(1.1)
|
||||||
|
opacity: 0.8
|
||||||
|
|
||||||
|
.battle-container
|
||||||
|
display: flex
|
||||||
|
flex-direction: column
|
||||||
|
align-items: center
|
||||||
|
gap: 2rem
|
||||||
|
margin-bottom: 2rem
|
||||||
|
perspective: 1000px
|
||||||
|
|
||||||
|
.song-card
|
||||||
|
box-sizing: border-box
|
||||||
|
width: 100%
|
||||||
|
max-width: 600px
|
||||||
|
background-color: $card-bg
|
||||||
|
border: 4px solid #000
|
||||||
|
box-shadow: 8px 8px 0 #000
|
||||||
|
padding: 1rem
|
||||||
|
transition: all 0.2s ease
|
||||||
|
position: relative
|
||||||
|
|
||||||
|
&.selected
|
||||||
|
border-color: $primary
|
||||||
|
box-shadow: 0 0 20px $primary, 8px 8px 0 #000
|
||||||
|
transform: translateY(-4px)
|
||||||
|
|
||||||
|
.song-spotlight
|
||||||
|
opacity: 1
|
||||||
|
|
||||||
|
&.bye-winner
|
||||||
|
border-color: $accent
|
||||||
|
box-shadow: 0 0 20px $accent, 8px 8px 0 #000
|
||||||
|
|
||||||
|
.song-spotlight
|
||||||
|
position: absolute
|
||||||
|
top: 0
|
||||||
|
left: 0
|
||||||
|
right: 0
|
||||||
|
bottom: 0
|
||||||
|
background: linear-gradient(45deg, transparent 0%, rgba($primary, 0.1) 45%, rgba($primary, 0.4) 50%, rgba($primary, 0.1) 55%, transparent 100%)
|
||||||
|
pointer-events: none
|
||||||
|
opacity: 0
|
||||||
|
transition: opacity 0.3s ease
|
||||||
|
|
||||||
|
.song-details
|
||||||
|
h3
|
||||||
|
margin-top: 0
|
||||||
|
margin-bottom: 0.5rem
|
||||||
|
font-family: 'Press Start 2P', monospace
|
||||||
|
font-size: 1.1rem
|
||||||
|
color: $secondary
|
||||||
|
|
||||||
|
p
|
||||||
|
margin-top: 0
|
||||||
|
margin-bottom: 1rem
|
||||||
|
font-family: 'Press Start 2P', monospace
|
||||||
|
font-size: 0.8rem
|
||||||
|
color: $text
|
||||||
|
|
||||||
|
.auto-advance-notice
|
||||||
|
margin-top: 1rem
|
||||||
|
padding: 0.75rem
|
||||||
|
background-color: rgba($accent, 0.2)
|
||||||
|
border: 2px solid $accent
|
||||||
|
|
||||||
|
p
|
||||||
|
margin: 0
|
||||||
|
color: $accent
|
||||||
|
font-weight: bold
|
||||||
|
text-align: center
|
||||||
|
|
||||||
|
.video-container
|
||||||
|
margin-top: 1rem
|
||||||
|
border: 4px solid #000
|
||||||
|
position: relative
|
||||||
|
|
||||||
|
.video-overlay
|
||||||
|
position: absolute
|
||||||
|
top: 0
|
||||||
|
left: 0
|
||||||
|
width: 100%
|
||||||
|
height: 100%
|
||||||
|
background: repeating-linear-gradient( 0deg, rgba(0, 0, 0, 0.1), rgba(0, 0, 0, 0.1) 2px, transparent 2px, transparent 4px)
|
||||||
|
pointer-events: none
|
||||||
|
opacity: 0.4
|
||||||
|
|
||||||
|
.no-video
|
||||||
display: flex
|
display: flex
|
||||||
flex-direction: column
|
flex-direction: column
|
||||||
align-items: center
|
align-items: center
|
||||||
gap: 1.5rem
|
justify-content: center
|
||||||
margin-bottom: 2rem
|
height: 180px
|
||||||
perspective: 1000px
|
background-color: rgba(0, 0, 0, 0.3)
|
||||||
|
border: 2px dashed rgba(255, 255, 255, 0.3)
|
||||||
|
margin-top: 1rem
|
||||||
|
|
||||||
@media (min-width: 768px)
|
.pulse-icon
|
||||||
flex-direction: row
|
|
||||||
align-items: stretch
|
|
||||||
|
|
||||||
.song-card
|
|
||||||
flex: 1
|
|
||||||
background-color: $card-bg
|
|
||||||
border-radius: 1rem
|
|
||||||
padding: 1.5rem
|
|
||||||
display: flex
|
|
||||||
flex-direction: column
|
|
||||||
transition: transform 0.3s ease, box-shadow 0.3s ease, border 0.3s ease
|
|
||||||
border: 2px solid transparent
|
|
||||||
cursor: pointer
|
|
||||||
position: relative
|
|
||||||
overflow: hidden
|
|
||||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.4)
|
|
||||||
|
|
||||||
&:hover:not(.voted)
|
|
||||||
transform: translateY(-5px)
|
|
||||||
box-shadow: 0 15px 40px rgba(0, 0, 0, 0.6)
|
|
||||||
|
|
||||||
&.selected:not(.voted)
|
|
||||||
border-color: $secondary
|
|
||||||
box-shadow: 0 0 25px rgba($secondary, 0.4)
|
|
||||||
transform: translateY(-8px) scale(1.02)
|
|
||||||
|
|
||||||
.song-spotlight
|
|
||||||
opacity: 1
|
|
||||||
|
|
||||||
&.voted
|
|
||||||
cursor: default
|
|
||||||
|
|
||||||
.song-spotlight
|
|
||||||
position: absolute
|
|
||||||
top: -50%
|
|
||||||
left: -50%
|
|
||||||
width: 200%
|
|
||||||
height: 200%
|
|
||||||
background: radial-gradient(ellipse at center, rgba($secondary, 0.3) 0%, rgba($secondary, 0) 70%)
|
|
||||||
pointer-events: none
|
|
||||||
opacity: 0
|
|
||||||
transition: opacity 0.5s
|
|
||||||
z-index: 1
|
|
||||||
|
|
||||||
.song-details
|
|
||||||
margin-bottom: 1rem
|
|
||||||
|
|
||||||
h3
|
|
||||||
margin: 0 0 0.25rem 0
|
|
||||||
font-size: 1.5rem
|
|
||||||
|
|
||||||
p
|
|
||||||
margin: 0
|
|
||||||
color: $text-muted
|
|
||||||
|
|
||||||
.video-container
|
|
||||||
width: 100%
|
|
||||||
flex-grow: 1
|
|
||||||
min-height: 200px
|
|
||||||
position: relative
|
|
||||||
margin-bottom: 0.5rem
|
|
||||||
|
|
||||||
.youtube-embed
|
|
||||||
position: absolute
|
|
||||||
top: 0
|
|
||||||
left: 0
|
|
||||||
width: 100%
|
|
||||||
height: 100%
|
|
||||||
border-radius: 0.5rem
|
|
||||||
overflow: hidden
|
|
||||||
|
|
||||||
.no-video
|
|
||||||
flex-grow: 1
|
|
||||||
min-height: 200px
|
|
||||||
display: flex
|
|
||||||
flex-direction: column
|
|
||||||
align-items: center
|
|
||||||
justify-content: center
|
|
||||||
background-color: rgba(0, 0, 0, 0.3)
|
|
||||||
border-radius: 0.5rem
|
|
||||||
color: $text-muted
|
|
||||||
font-size: 3rem
|
|
||||||
gap: 1rem
|
|
||||||
|
|
||||||
span
|
|
||||||
font-size: 1rem
|
|
||||||
|
|
||||||
.vote-count
|
|
||||||
position: absolute
|
|
||||||
bottom: 1rem
|
|
||||||
right: 1rem
|
|
||||||
background-color: rgba($background, 0.8)
|
|
||||||
padding: 0.5rem 1rem
|
|
||||||
border-radius: 2rem
|
|
||||||
font-size: 0.9rem
|
|
||||||
|
|
||||||
&::after
|
|
||||||
content: ''
|
|
||||||
position: absolute
|
|
||||||
top: 0
|
|
||||||
left: 0
|
|
||||||
right: 0
|
|
||||||
bottom: 0
|
|
||||||
background: linear-gradient(to right, $primary, $secondary)
|
|
||||||
opacity: 0.2
|
|
||||||
border-radius: 2rem
|
|
||||||
z-index: -1
|
|
||||||
|
|
||||||
.versus
|
|
||||||
font-family: 'Bangers', cursive
|
|
||||||
font-size: 2rem
|
font-size: 2rem
|
||||||
color: $accent
|
color: $text-muted
|
||||||
text-shadow: 0 0 10px rgba($accent, 0.5)
|
margin-bottom: 0.5rem
|
||||||
display: flex
|
animation: pixel-float 2s infinite
|
||||||
align-items: center
|
|
||||||
padding: 0 1rem
|
|
||||||
|
|
||||||
@media (min-width: 768px)
|
span
|
||||||
padding: 1rem
|
color: $text-muted
|
||||||
|
font-size: 0.9rem
|
||||||
|
|
||||||
.voting-actions
|
.vote-count
|
||||||
|
position: absolute
|
||||||
|
top: 1rem
|
||||||
|
right: 1rem
|
||||||
|
background-color: rgba(#000, 0.7)
|
||||||
|
border: 2px solid $primary
|
||||||
|
padding: 0.5rem
|
||||||
|
display: flex
|
||||||
|
flex-direction: column
|
||||||
|
align-items: center
|
||||||
|
|
||||||
|
.vote-number
|
||||||
|
font-size: 1.5rem
|
||||||
|
font-weight: bold
|
||||||
|
color: $primary
|
||||||
|
|
||||||
|
.vote-text
|
||||||
|
font-size: 0.7rem
|
||||||
|
color: $text-muted
|
||||||
|
|
||||||
|
.selection-indicator
|
||||||
|
position: absolute
|
||||||
|
top: 1rem
|
||||||
|
right: 1rem
|
||||||
|
background-color: $primary
|
||||||
|
color: #000
|
||||||
|
width: 2rem
|
||||||
|
height: 2rem
|
||||||
|
display: flex
|
||||||
|
align-items: center
|
||||||
|
justify-content: center
|
||||||
|
border: 2px solid #000
|
||||||
|
animation: pixel-pulse 1s infinite
|
||||||
|
|
||||||
|
.versus
|
||||||
|
display: flex
|
||||||
|
flex-direction: column
|
||||||
|
align-items: center
|
||||||
|
margin: 0.5rem 0
|
||||||
|
|
||||||
|
.versus-text
|
||||||
|
font-family: 'Press Start 2P', monospace
|
||||||
|
font-size: 1.5rem
|
||||||
|
font-weight: bold
|
||||||
|
color: $accent
|
||||||
|
letter-spacing: 2px
|
||||||
|
animation: pixel-flash 1.5s infinite
|
||||||
|
|
||||||
|
.versus-decoration
|
||||||
|
width: 150px
|
||||||
|
height: 4px
|
||||||
|
background-color: $accent
|
||||||
|
margin: 0.5rem 0
|
||||||
|
position: relative
|
||||||
|
|
||||||
|
&:before, &:after
|
||||||
|
content: ''
|
||||||
|
position: absolute
|
||||||
|
width: 15px
|
||||||
|
height: 15px
|
||||||
|
background-color: $accent
|
||||||
|
transform: rotate(45deg)
|
||||||
|
|
||||||
|
&:before
|
||||||
|
left: -5px
|
||||||
|
top: -5px
|
||||||
|
|
||||||
|
&:after
|
||||||
|
right: -5px
|
||||||
|
top: -5px
|
||||||
|
|
||||||
|
@keyframes pixel-flash
|
||||||
|
0%, 100%
|
||||||
|
opacity: 1
|
||||||
|
50%
|
||||||
|
opacity: 0.5
|
||||||
|
|
||||||
|
@keyframes pixel-float
|
||||||
|
0%, 100%
|
||||||
|
transform: translateY(0)
|
||||||
|
50%
|
||||||
|
transform: translateY(-5px)
|
||||||
|
|
||||||
|
// Voting action buttons area
|
||||||
|
.voting-actions
|
||||||
|
display: flex
|
||||||
|
justify-content: center
|
||||||
|
margin: 2rem 0
|
||||||
|
|
||||||
|
.btn
|
||||||
|
min-width: 180px
|
||||||
|
font-size: 1rem
|
||||||
|
|
||||||
|
// Voting status and information
|
||||||
|
.voting-status
|
||||||
|
background-color: $card-bg
|
||||||
|
border: 4px solid #000
|
||||||
|
box-shadow: 4px 4px 0 #000
|
||||||
|
padding: 1rem
|
||||||
|
margin-top: auto
|
||||||
|
text-align: center
|
||||||
|
|
||||||
|
p
|
||||||
|
margin: 0 0 1rem 0
|
||||||
|
font-family: 'Press Start 2P', monospace
|
||||||
|
font-size: 0.9rem
|
||||||
|
color: $text
|
||||||
|
|
||||||
|
.votes-count
|
||||||
display: flex
|
display: flex
|
||||||
justify-content: center
|
justify-content: center
|
||||||
margin-bottom: 2rem
|
align-items: center
|
||||||
|
gap: 0.5rem
|
||||||
|
font-family: 'Press Start 2P', monospace
|
||||||
|
font-size: 0.8rem
|
||||||
|
margin-bottom: 1rem
|
||||||
|
|
||||||
.btn
|
span
|
||||||
padding: 0.75rem 2rem
|
color: $primary
|
||||||
font-size: 1.1rem
|
font-weight: bold
|
||||||
|
|
||||||
.voting-status
|
// Player votes list styling
|
||||||
margin-top: auto
|
.player-votes
|
||||||
text-align: center
|
background-color: rgba(0, 0, 0, 0.2)
|
||||||
|
border: 2px dashed rgba(255, 255, 255, 0.2)
|
||||||
|
border-radius: 0.5rem
|
||||||
|
padding: 0.75rem
|
||||||
|
margin-top: 0.5rem
|
||||||
|
|
||||||
p
|
h4
|
||||||
color: $text-muted
|
margin: 0 0 0.75rem 0
|
||||||
font-style: italic
|
font-family: 'Press Start 2P', monospace
|
||||||
margin-bottom: 0.5rem
|
font-size: 0.8rem
|
||||||
|
color: $secondary
|
||||||
.votes-count
|
position: relative
|
||||||
display: inline-block
|
display: inline-block
|
||||||
padding: 0.5rem 1rem
|
|
||||||
background-color: rgba($card-bg, 0.7)
|
|
||||||
border-radius: 0.5rem
|
|
||||||
|
|
||||||
span:first-child
|
&:before, &:after
|
||||||
font-weight: bold
|
content: '>'
|
||||||
color: $secondary
|
position: absolute
|
||||||
|
color: $accent
|
||||||
|
animation: pixel-blink 1s infinite
|
||||||
|
|
||||||
|
&:before
|
||||||
|
left: -1rem
|
||||||
|
|
||||||
|
&:after
|
||||||
|
right: -1rem
|
||||||
|
|
||||||
|
.players-voted-list
|
||||||
|
list-style: none
|
||||||
|
padding: 0
|
||||||
|
margin: 0
|
||||||
|
display: grid
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr))
|
||||||
|
gap: 0.5rem
|
||||||
|
|
||||||
|
li
|
||||||
|
padding: 0.5rem
|
||||||
|
border: 2px solid transparent
|
||||||
|
font-size: 0.75rem
|
||||||
|
display: flex
|
||||||
|
align-items: center
|
||||||
|
justify-content: space-between
|
||||||
|
transition: all 0.2s ease
|
||||||
|
position: relative
|
||||||
|
overflow: hidden
|
||||||
|
|
||||||
|
&.voted
|
||||||
|
background-color: rgba($success, 0.1)
|
||||||
|
border-color: rgba($success, 0.5)
|
||||||
|
|
||||||
|
.vote-icon
|
||||||
|
color: $success
|
||||||
|
margin-left: 0.5rem
|
||||||
|
filter: drop-shadow(0 0 2px rgba($success, 0.8))
|
||||||
|
animation: pixel-pulse 1.5s infinite
|
||||||
|
|
||||||
|
&:before
|
||||||
|
content: ''
|
||||||
|
position: absolute
|
||||||
|
top: 0
|
||||||
|
left: 0
|
||||||
|
width: 100%
|
||||||
|
height: 100%
|
||||||
|
background: repeating-linear-gradient(45deg, transparent, transparent 5px, rgba($success, 0.1) 5px, rgba($success, 0.1) 10px)
|
||||||
|
z-index: -1
|
||||||
|
|
||||||
|
&.not-voted
|
||||||
|
background-color: rgba(255, 255, 255, 0.05)
|
||||||
|
border-color: rgba(255, 255, 255, 0.1)
|
||||||
|
opacity: 0.7
|
||||||
|
|
||||||
|
@keyframes pixel-blink
|
||||||
|
0%, 100%
|
||||||
|
opacity: 1
|
||||||
|
50%
|
||||||
|
opacity: 0.3
|
||||||
|
|
||||||
|
// Bye container for automatic advances
|
||||||
|
.bye-container
|
||||||
|
display: flex
|
||||||
|
justify-content: center
|
||||||
|
margin: 2rem 0
|
||||||
|
animation: pixel-float 3s infinite ease-in-out
|
||||||
|
@ -4,8 +4,38 @@
|
|||||||
width: 100%
|
width: 100%
|
||||||
height: 100%
|
height: 100%
|
||||||
background-color: #000
|
background-color: #000
|
||||||
border-radius: 0.5rem
|
|
||||||
overflow: hidden
|
overflow: hidden
|
||||||
|
border: 4px solid #000
|
||||||
|
position: relative
|
||||||
|
|
||||||
|
// Pixel art CRT screen effect
|
||||||
|
&:before
|
||||||
|
content: ''
|
||||||
|
position: absolute
|
||||||
|
top: 0
|
||||||
|
left: 0
|
||||||
|
right: 0
|
||||||
|
bottom: 0
|
||||||
|
background: linear-gradient(transparent 50%, rgba(0, 0, 0, 0.1) 50%)
|
||||||
|
background-size: 100% 4px
|
||||||
|
z-index: 2
|
||||||
|
pointer-events: none
|
||||||
|
opacity: 0.4
|
||||||
|
|
||||||
|
// Video glow effect
|
||||||
|
&:after
|
||||||
|
content: ''
|
||||||
|
position: absolute
|
||||||
|
top: -3px
|
||||||
|
left: -3px
|
||||||
|
right: -3px
|
||||||
|
bottom: -3px
|
||||||
|
background-color: transparent
|
||||||
|
box-shadow: 0 0 10px rgba($secondary, 0.5)
|
||||||
|
z-index: -1
|
||||||
|
pointer-events: none
|
||||||
|
|
||||||
iframe
|
iframe
|
||||||
border: none
|
border: none
|
||||||
|
z-index: 1
|
||||||
|
position: relative
|
||||||
|
@ -11,16 +11,17 @@
|
|||||||
.search-input
|
.search-input
|
||||||
width: 100%
|
width: 100%
|
||||||
padding: 0.75rem 2.5rem 0.75rem 1rem
|
padding: 0.75rem 2.5rem 0.75rem 1rem
|
||||||
border-radius: 0.5rem
|
background: rgba(0, 0, 0, 0.3)
|
||||||
background: rgba(255, 255, 255, 0.1)
|
border: 3px solid #000
|
||||||
border: 1px solid rgba(255, 255, 255, 0.2)
|
|
||||||
color: $text
|
color: $text
|
||||||
font-size: 1rem
|
font-family: 'Press Start 2P', monospace
|
||||||
|
font-size: 0.8rem
|
||||||
|
box-shadow: 4px 4px 0 #000
|
||||||
|
|
||||||
&:focus
|
&:focus
|
||||||
outline: none
|
outline: none
|
||||||
border-color: $primary
|
border-color: $secondary
|
||||||
box-shadow: 0 0 0 3px rgba($primary, 0.3)
|
box-shadow: 4px 4px 0 #000, 0 0 8px rgba($secondary, 0.5)
|
||||||
|
|
||||||
.spinner-icon, .clear-icon
|
.spinner-icon, .clear-icon
|
||||||
position: absolute
|
position: absolute
|
||||||
@ -28,15 +29,20 @@
|
|||||||
right: 1rem
|
right: 1rem
|
||||||
transform: translateY(-50%)
|
transform: translateY(-50%)
|
||||||
color: $text-muted
|
color: $text-muted
|
||||||
|
filter: drop-shadow(1px 1px 0 #000)
|
||||||
|
|
||||||
.clear-icon
|
.clear-icon
|
||||||
cursor: pointer
|
cursor: pointer
|
||||||
|
|
||||||
&:hover
|
&:hover
|
||||||
color: $text
|
color: $text
|
||||||
|
transform: translateY(-50%) scale(1.2)
|
||||||
|
transition: all 0.2s ease
|
||||||
|
|
||||||
.selected-video
|
.selected-video
|
||||||
background: rgba($card-bg, 0.6)
|
background: $card-bg
|
||||||
border-radius: 0.75rem
|
border: 3px solid #000
|
||||||
|
box-shadow: 4px 4px 0 #000
|
||||||
padding: 1rem
|
padding: 1rem
|
||||||
margin-bottom: 1.5rem
|
margin-bottom: 1.5rem
|
||||||
border: 1px solid rgba($primary, 0.3)
|
border: 1px solid rgba($primary, 0.3)
|
||||||
@ -51,14 +57,12 @@
|
|||||||
margin: 0
|
margin: 0
|
||||||
color: $primary
|
color: $primary
|
||||||
|
|
||||||
.preview-toggle
|
.external-link
|
||||||
background: transparent
|
|
||||||
border: none
|
|
||||||
color: $secondary
|
color: $secondary
|
||||||
cursor: pointer
|
text-decoration: none
|
||||||
|
font-size: 0.85rem
|
||||||
padding: 0.25rem 0.5rem
|
padding: 0.25rem 0.5rem
|
||||||
border-radius: 0.25rem
|
border-radius: 0.25rem
|
||||||
font-size: 0.85rem
|
|
||||||
|
|
||||||
&:hover
|
&:hover
|
||||||
background: rgba($secondary, 0.15)
|
background: rgba($secondary, 0.15)
|
||||||
@ -68,11 +72,31 @@
|
|||||||
gap: 1rem
|
gap: 1rem
|
||||||
margin-bottom: 1rem
|
margin-bottom: 1rem
|
||||||
|
|
||||||
.selected-thumbnail
|
.thumbnail-container
|
||||||
|
position: relative
|
||||||
width: 120px
|
width: 120px
|
||||||
height: 90px
|
flex-shrink: 0
|
||||||
object-fit: cover
|
|
||||||
border-radius: 0.25rem
|
.selected-thumbnail
|
||||||
|
width: 120px
|
||||||
|
height: 90px
|
||||||
|
object-fit: cover
|
||||||
|
border-radius: 0.25rem
|
||||||
|
|
||||||
|
.play-overlay
|
||||||
|
position: absolute
|
||||||
|
top: 50%
|
||||||
|
left: 50%
|
||||||
|
transform: translate(-50%, -50%)
|
||||||
|
color: white
|
||||||
|
background: rgba(0, 0, 0, 0.6)
|
||||||
|
width: 36px
|
||||||
|
height: 36px
|
||||||
|
border-radius: 50%
|
||||||
|
display: flex
|
||||||
|
align-items: center
|
||||||
|
justify-content: center
|
||||||
|
opacity: 0.7
|
||||||
|
|
||||||
.selected-info
|
.selected-info
|
||||||
display: flex
|
display: flex
|
||||||
@ -87,10 +111,10 @@
|
|||||||
margin: 0
|
margin: 0
|
||||||
color: $text-muted
|
color: $text-muted
|
||||||
font-size: 0.9rem
|
font-size: 0.9rem
|
||||||
|
|
||||||
.video-preview
|
.video-preview
|
||||||
position: relative
|
position: relative
|
||||||
padding-bottom: 56.25% // 16:9 aspect ratio
|
padding-bottom: 56.25%
|
||||||
|
// 16:9 aspect ratio
|
||||||
height: 0
|
height: 0
|
||||||
overflow: hidden
|
overflow: hidden
|
||||||
border-radius: 0.5rem
|
border-radius: 0.5rem
|
||||||
@ -105,11 +129,16 @@
|
|||||||
border-radius: 0.5rem
|
border-radius: 0.5rem
|
||||||
|
|
||||||
.search-results
|
.search-results
|
||||||
background: rgba($card-bg, 0.6)
|
background: rgba($card-bg, 0.8)
|
||||||
border-radius: 0.75rem
|
border-radius: 0.75rem
|
||||||
padding: 1rem
|
padding: 1rem
|
||||||
max-height: 400px
|
max-height: 300px
|
||||||
overflow-y: auto
|
overflow-y: auto
|
||||||
|
margin-bottom: 1.5rem
|
||||||
|
border: 2px solid #000
|
||||||
|
box-shadow: 4px 4px 0 #000
|
||||||
|
position: relative
|
||||||
|
z-index: 10
|
||||||
|
|
||||||
h4
|
h4
|
||||||
margin-top: 0
|
margin-top: 0
|
||||||
|
@ -5,12 +5,17 @@
|
|||||||
|
|
||||||
label
|
label
|
||||||
display: block
|
display: block
|
||||||
margin-bottom: 0.5rem
|
margin-bottom: 0.75rem
|
||||||
font-weight: 500
|
font-weight: 500
|
||||||
|
font-family: 'Press Start 2P', monospace
|
||||||
|
font-size: 0.8rem
|
||||||
|
color: $secondary
|
||||||
|
text-transform: uppercase
|
||||||
|
|
||||||
.required
|
.required
|
||||||
color: $danger
|
color: $danger
|
||||||
margin-left: 0.25rem
|
margin-left: 0.25rem
|
||||||
|
animation: pixel-flash 1s infinite
|
||||||
|
|
||||||
input[type="text"],
|
input[type="text"],
|
||||||
input[type="number"],
|
input[type="number"],
|
||||||
@ -21,32 +26,45 @@
|
|||||||
select
|
select
|
||||||
width: 100%
|
width: 100%
|
||||||
padding: 0.75rem
|
padding: 0.75rem
|
||||||
background-color: rgba(0, 0, 0, 0.4)
|
background-color: $card-bg
|
||||||
border: 2px solid rgba(255, 255, 255, 0.3)
|
border: 3px solid #000
|
||||||
border-radius: 0.5rem
|
|
||||||
color: $text
|
color: $text
|
||||||
font-size: 1rem
|
font-size: 1rem
|
||||||
transition: all 0.3s ease
|
font-family: 'Press Start 2P', monospace
|
||||||
|
font-size: 0.9rem
|
||||||
|
transition: all 0.2s ease
|
||||||
box-sizing: border-box
|
box-sizing: border-box
|
||||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.2)
|
box-shadow: 4px 4px 0 #000
|
||||||
|
|
||||||
&:focus
|
|
||||||
border-color: $secondary
|
|
||||||
box-shadow: 0 0 15px rgba($secondary, 0.5)
|
|
||||||
outline: none
|
|
||||||
transform: scale(1.02)
|
|
||||||
|
|
||||||
&::placeholder
|
|
||||||
color: rgba(255, 255, 255, 0.5)
|
|
||||||
|
|
||||||
&:focus
|
&:focus
|
||||||
border-color: $primary
|
border-color: $primary
|
||||||
box-shadow: 0 0 0 2px rgba($primary, 0.25)
|
box-shadow: 4px 4px 0 #000, 0 0 8px $primary
|
||||||
outline: none
|
outline: none
|
||||||
|
|
||||||
&::placeholder
|
&::placeholder
|
||||||
color: rgba(255, 255, 255, 0.4)
|
color: rgba(255, 255, 255, 0.4)
|
||||||
|
|
||||||
|
// Pixel art checkbox
|
||||||
|
input[type="checkbox"]
|
||||||
|
appearance: none
|
||||||
|
width: 24px
|
||||||
|
height: 24px
|
||||||
|
background-color: $card-bg
|
||||||
|
border: 3px solid #000
|
||||||
|
position: relative
|
||||||
|
cursor: pointer
|
||||||
|
|
||||||
|
&:checked:after
|
||||||
|
content: ''
|
||||||
|
position: absolute
|
||||||
|
left: 6px
|
||||||
|
top: 2px
|
||||||
|
width: 6px
|
||||||
|
height: 12px
|
||||||
|
border: solid $primary
|
||||||
|
border-width: 0 3px 3px 0
|
||||||
|
transform: rotate(45deg)
|
||||||
|
|
||||||
&.checkbox
|
&.checkbox
|
||||||
display: flex
|
display: flex
|
||||||
align-items: center
|
align-items: center
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
// Main styles for the Song Battle application
|
// Main styles for the Song Battle application
|
||||||
@import './colors'
|
@import './colors'
|
||||||
@import './forms'
|
@import './forms'
|
||||||
|
@import './buttons'
|
||||||
|
|
||||||
// Component styles
|
// Component styles
|
||||||
@import './components/home-screen'
|
@import './components/home-screen'
|
||||||
@ -10,32 +11,48 @@
|
|||||||
@import './components/results-screen'
|
@import './components/results-screen'
|
||||||
@import './components/youtube-embed'
|
@import './components/youtube-embed'
|
||||||
@import './components/youtube-search'
|
@import './components/youtube-search'
|
||||||
|
@import './components/song-form-overlay'
|
||||||
|
|
||||||
// Global styles
|
// Global styles
|
||||||
html, body
|
html, body
|
||||||
margin: 0
|
margin: 0
|
||||||
padding: 0
|
padding: 0
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif
|
font-family: 'Press Start 2P', 'Courier New', monospace
|
||||||
background: linear-gradient(45deg, #AE00FF 0%, #FF007F 100%) fixed
|
background: linear-gradient(45deg, #6600cc 0%, #ff0066 100%) fixed
|
||||||
color: $text
|
color: $text
|
||||||
height: 100%
|
height: 100%
|
||||||
overflow-x: hidden
|
overflow-x: hidden
|
||||||
|
image-rendering: pixelated
|
||||||
|
|
||||||
|
// Pixel art border mixin
|
||||||
|
@mixin pixel-border($color: $primary, $size: 4px)
|
||||||
|
border: $size solid $color
|
||||||
|
box-shadow: 0 0 0 2px darken($color, 30%)
|
||||||
|
position: relative
|
||||||
|
|
||||||
#root, .app
|
#root, .app
|
||||||
height: 100%
|
height: 100%
|
||||||
width: 100%
|
width: 100%
|
||||||
display: flex
|
display: flex
|
||||||
flex-direction: column
|
flex-direction: column
|
||||||
|
background-image: url('/background.svg')
|
||||||
|
background-size: 32px 32px
|
||||||
|
background-repeat: repeat
|
||||||
|
|
||||||
a
|
a
|
||||||
color: $primary
|
color: $secondary
|
||||||
text-decoration: none
|
text-decoration: none
|
||||||
|
font-weight: bold
|
||||||
|
text-shadow: 2px 2px 0 #000, -2px -2px 0 #000, 2px -2px 0 #000, -2px 2px 0 #000
|
||||||
&:hover
|
&:hover
|
||||||
text-decoration: underline
|
color: lighten($secondary, 20%)
|
||||||
|
transform: scale(1.05)
|
||||||
|
transition: all 0.2s ease
|
||||||
|
|
||||||
h1, h2, h3
|
h1, h2, h3
|
||||||
font-family: 'Bangers', cursive
|
font-family: 'Press Start 2P', 'VT323', monospace
|
||||||
letter-spacing: 1px
|
letter-spacing: 2px
|
||||||
|
text-shadow: 2px 2px 0 #000, -2px -2px 0 #000, 2px -2px 0 #000, -2px 2px 0 #000
|
||||||
|
|
||||||
// Background elements
|
// Background elements
|
||||||
.background-elements
|
.background-elements
|
||||||
@ -346,6 +363,7 @@ h1, h2, h3
|
|||||||
margin-bottom: 2rem
|
margin-bottom: 2rem
|
||||||
|
|
||||||
.song-card
|
.song-card
|
||||||
|
box-sizing: border-box
|
||||||
background-color: rgba($card-bg, 0.8)
|
background-color: rgba($card-bg, 0.8)
|
||||||
backdrop-filter: blur(10px)
|
backdrop-filter: blur(10px)
|
||||||
border-radius: 0.75rem
|
border-radius: 0.75rem
|
||||||
|
@ -30,7 +30,7 @@ const HomeScreen = () => {
|
|||||||
<div className="home-screen">
|
<div className="home-screen">
|
||||||
<div className="logo">
|
<div className="logo">
|
||||||
<img src="/logo.png" alt="Song Battle Logo" className="logo-image" />
|
<img src="/logo.png" alt="Song Battle Logo" className="logo-image" />
|
||||||
<h1>Song Battle</h1>
|
<h1>Liedkampf</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="card">
|
<div className="card">
|
||||||
@ -39,25 +39,25 @@ const HomeScreen = () => {
|
|||||||
className={isCreateMode ? 'active' : ''}
|
className={isCreateMode ? 'active' : ''}
|
||||||
onClick={() => setIsCreateMode(true)}
|
onClick={() => setIsCreateMode(true)}
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={faPlus} /> Create Game
|
<FontAwesomeIcon icon={faPlus} /> Erstellen
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className={!isCreateMode ? 'active' : ''}
|
className={!isCreateMode ? 'active' : ''}
|
||||||
onClick={() => setIsCreateMode(false)}
|
onClick={() => setIsCreateMode(false)}
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={faDoorOpen} /> Join Game
|
<FontAwesomeIcon icon={faDoorOpen} /> Beitreten
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form onSubmit={handleSubmit}>
|
<form onSubmit={handleSubmit}>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="player-name">Your Name</label>
|
<label htmlFor="player-name">Dein Name</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
id="player-name"
|
id="player-name"
|
||||||
value={playerName}
|
value={playerName}
|
||||||
onChange={(e) => setPlayerName(e.target.value)}
|
onChange={(e) => setPlayerName(e.target.value)}
|
||||||
placeholder="Enter your name"
|
placeholder="Gib deinen Namen ein"
|
||||||
maxLength={20}
|
maxLength={20}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
@ -65,27 +65,31 @@ const HomeScreen = () => {
|
|||||||
|
|
||||||
{!isCreateMode && (
|
{!isCreateMode && (
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="lobby-id">Game Code</label>
|
<label htmlFor="lobby-id">Spielcode</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
id="lobby-id"
|
id="lobby-id"
|
||||||
value={lobbyId}
|
value={lobbyId}
|
||||||
onChange={(e) => setLobbyId(e.target.value.toUpperCase())}
|
onChange={(e) => setLobbyId(e.target.value.toUpperCase())}
|
||||||
placeholder="Enter 6-letter code"
|
placeholder="6-stelligen Code eingeben"
|
||||||
maxLength={6}
|
maxLength={6}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<button type="submit" className="btn primary">
|
<button type="submit" className="btn primary pixelated full-width">
|
||||||
{isCreateMode ? 'Create New Game' : 'Join Game'}
|
{isCreateMode ? 'Neues Spiel erstellen' : 'Spiel beitreten'}
|
||||||
|
<span className="pixel-corner tl"></span>
|
||||||
|
<span className="pixel-corner tr"></span>
|
||||||
|
<span className="pixel-corner bl"></span>
|
||||||
|
<span className="pixel-corner br"></span>
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<footer className="home-footer">
|
<footer className="home-footer">
|
||||||
<p>Let your favorite songs battle it out!</p>
|
<p>ich weiß doch auch nicht</p>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -8,8 +8,7 @@ const LobbyScreen = () => {
|
|||||||
const [showSettings, setShowSettings] = useState(false);
|
const [showSettings, setShowSettings] = useState(false);
|
||||||
const [settings, setSettings] = useState({
|
const [settings, setSettings] = useState({
|
||||||
songsPerPlayer: 4,
|
songsPerPlayer: 4,
|
||||||
maxPlayers: 10,
|
maxPlayers: 10
|
||||||
requireYoutubeLinks: true
|
|
||||||
});
|
});
|
||||||
const [copied, setCopied] = useState(false);
|
const [copied, setCopied] = useState(false);
|
||||||
|
|
||||||
@ -51,24 +50,24 @@ const LobbyScreen = () => {
|
|||||||
return (
|
return (
|
||||||
<div className="lobby-screen">
|
<div className="lobby-screen">
|
||||||
<header className="lobby-header">
|
<header className="lobby-header">
|
||||||
<h1>Game Lobby</h1>
|
<h1>Spiellobby</h1>
|
||||||
<div className="lobby-code">
|
<div className="lobby-code">
|
||||||
<p>Game Code: <span className="code">{lobby.id}</span></p>
|
<p>Spielcode: <span className="code">{lobby.id}</span></p>
|
||||||
<button className="btn icon" onClick={handleCopyCode}>
|
<button className="btn icon" onClick={handleCopyCode}>
|
||||||
<FontAwesomeIcon icon={faCopy} />
|
<FontAwesomeIcon icon={faCopy} />
|
||||||
{copied ? 'Copied!' : 'Copy'}
|
{copied ? 'Kopiert!' : 'Kopieren'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div className="lobby-content">
|
<div className="lobby-content">
|
||||||
<div className="players-list">
|
<div className="players-list">
|
||||||
<h2><FontAwesomeIcon icon={faUsers} /> Players ({lobby.players.length})</h2>
|
<h2><FontAwesomeIcon icon={faUsers} /> Spieler ({lobby.players.length})</h2>
|
||||||
<ul>
|
<ul>
|
||||||
{lobby.players.map(player => (
|
{lobby.players.map(player => (
|
||||||
<li key={player.id} className={`player ${!player.isConnected ? 'disconnected' : ''} ${player.id === lobby.hostId ? 'host' : ''}`}>
|
<li key={player.id} className={`player ${!player.isConnected ? 'disconnected' : ''} ${player.id === lobby.hostId ? 'host' : ''}`}>
|
||||||
{player.name} {player.id === lobby.hostId && '(Host)'} {player.id === currentPlayer.id && '(You)'}
|
{player.name} {player.id === lobby.hostId && '(Gastgeber)'} {player.id === currentPlayer.id && '(Du)'}
|
||||||
{!player.isConnected && <span className="status-disconnected">(Disconnected)</span>}
|
{!player.isConnected && <span className="status-disconnected">(Getrennt)</span>}
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
@ -76,14 +75,13 @@ const LobbyScreen = () => {
|
|||||||
|
|
||||||
<div className="lobby-info">
|
<div className="lobby-info">
|
||||||
<div className="settings-preview">
|
<div className="settings-preview">
|
||||||
<h3><FontAwesomeIcon icon={faGear} /> Game Settings</h3>
|
<h3><FontAwesomeIcon icon={faGear} /> Spieleinstellungen</h3>
|
||||||
<p>Songs per player: {settings.songsPerPlayer}</p>
|
<p>Lieder pro Spieler: {settings.songsPerPlayer}</p>
|
||||||
<p>Max players: {settings.maxPlayers}</p>
|
<p>Maximale Spieler: {settings.maxPlayers}</p>
|
||||||
<p>YouTube links required: {settings.requireYoutubeLinks ? 'Yes' : 'No'}</p>
|
|
||||||
|
|
||||||
{isHost && (
|
{isHost && (
|
||||||
<button className="btn secondary" onClick={() => setShowSettings(true)}>
|
<button className="btn secondary" onClick={() => setShowSettings(true)}>
|
||||||
Edit Settings
|
Einstellungen ändern
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -95,16 +93,16 @@ const LobbyScreen = () => {
|
|||||||
onClick={handleStartGame}
|
onClick={handleStartGame}
|
||||||
disabled={lobby.players.length < lobby.settings.minPlayers}
|
disabled={lobby.players.length < lobby.settings.minPlayers}
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={faPlay} /> Start Game
|
<FontAwesomeIcon icon={faPlay} /> Spiel starten
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
<button className="btn danger" onClick={leaveLobby}>
|
<button className="btn danger" onClick={leaveLobby}>
|
||||||
<FontAwesomeIcon icon={faSignOutAlt} /> Leave Lobby
|
<FontAwesomeIcon icon={faSignOutAlt} /> Lobby verlassen
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{lobby.players.length < lobby.settings.minPlayers && isHost && (
|
{lobby.players.length < lobby.settings.minPlayers && isHost && (
|
||||||
<p className="warning">Need at least {lobby.settings.minPlayers} players to start</p>
|
<p className="warning">Mindestens {lobby.settings.minPlayers} Spieler werden benötigt</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -112,10 +110,10 @@ const LobbyScreen = () => {
|
|||||||
{showSettings && (
|
{showSettings && (
|
||||||
<div className="modal-overlay">
|
<div className="modal-overlay">
|
||||||
<div className="modal">
|
<div className="modal">
|
||||||
<h2>Game Settings</h2>
|
<h2>Spieleinstellungen</h2>
|
||||||
|
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="songsPerPlayer">Songs per player</label>
|
<label htmlFor="songsPerPlayer">Lieder pro Spieler</label>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
id="songsPerPlayer"
|
id="songsPerPlayer"
|
||||||
@ -128,7 +126,7 @@ const LobbyScreen = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="maxPlayers">Maximum players</label>
|
<label htmlFor="maxPlayers">Maximale Spieleranzahl</label>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
id="maxPlayers"
|
id="maxPlayers"
|
||||||
@ -140,25 +138,12 @@ const LobbyScreen = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="form-group checkbox">
|
|
||||||
<label htmlFor="requireYoutubeLinks">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
id="requireYoutubeLinks"
|
|
||||||
name="requireYoutubeLinks"
|
|
||||||
checked={settings.requireYoutubeLinks}
|
|
||||||
onChange={handleSettingsChange}
|
|
||||||
/>
|
|
||||||
Require YouTube links
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="modal-actions">
|
<div className="modal-actions">
|
||||||
<button className="btn secondary" onClick={() => setShowSettings(false)}>
|
<button className="btn secondary" onClick={() => setShowSettings(false)}>
|
||||||
Cancel
|
Abbrechen
|
||||||
</button>
|
</button>
|
||||||
<button className="btn primary" onClick={handleSaveSettings}>
|
<button className="btn primary" onClick={handleSaveSettings}>
|
||||||
Save Settings
|
Einstellungen speichern
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -119,13 +119,16 @@ const ResultsScreen = () => {
|
|||||||
<div className="battles-list">
|
<div className="battles-list">
|
||||||
{lobby?.battles?.map((battle, index) => {
|
{lobby?.battles?.map((battle, index) => {
|
||||||
const isWinner = (battle.song1.id === battle.winner);
|
const isWinner = (battle.song1.id === battle.winner);
|
||||||
const song1VideoId = getYouTubeId(battle.song1.youtubeLink);
|
const song1VideoId = getYouTubeId(battle.song1?.youtubeLink);
|
||||||
const song2VideoId = getYouTubeId(battle.song2.youtubeLink);
|
const song2VideoId = battle.song2 ? getYouTubeId(battle.song2.youtubeLink) : null;
|
||||||
|
|
||||||
|
// Handle bye rounds
|
||||||
|
const isByeRound = battle.bye === true || !battle.song2;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={index} className="battle-item">
|
<div key={index} className="battle-item">
|
||||||
<div className="battle-header">
|
<div className="battle-header">
|
||||||
<h4>Round {battle.round + 1}, Battle {index + 1}</h4>
|
<h4>Round {battle.round + 1}, Battle {index + 1} {isByeRound ? "(Automatic Advance)" : ""}</h4>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="battle-songs">
|
<div className="battle-songs">
|
||||||
|
@ -4,7 +4,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
|||||||
import { faPlus, faTrash, faCheck, faMusic, faVideoCamera, faSearch, faSpinner, faExternalLinkAlt, faTimes } from '@fortawesome/free-solid-svg-icons';
|
import { faPlus, faTrash, faCheck, faMusic, faVideoCamera, faSearch, faSpinner, faExternalLinkAlt, faTimes } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
|
||||||
const SongSubmissionScreen = () => {
|
const SongSubmissionScreen = () => {
|
||||||
const { lobby, currentPlayer, addSong, removeSong, setPlayerReady, searchYouTube } = useGame();
|
const { lobby, currentPlayer, addSong, removeSong, setPlayerReady, searchYouTube, getYouTubeMetadata } = useGame();
|
||||||
const [songs, setSongs] = useState([]);
|
const [songs, setSongs] = useState([]);
|
||||||
const [isReady, setIsReady] = useState(false);
|
const [isReady, setIsReady] = useState(false);
|
||||||
const [songForm, setSongForm] = useState({
|
const [songForm, setSongForm] = useState({
|
||||||
@ -51,7 +51,7 @@ const SongSubmissionScreen = () => {
|
|||||||
if (player.songs && Array.isArray(player.songs)) {
|
if (player.songs && Array.isArray(player.songs)) {
|
||||||
console.log('Found player songs for', player.name, ':', player.songs.length);
|
console.log('Found player songs for', player.name, ':', player.songs.length);
|
||||||
console.log('Songs data:', player.songs);
|
console.log('Songs data:', player.songs);
|
||||||
setSongs(player.songs);
|
setSongs(player.songs || []);
|
||||||
} else {
|
} else {
|
||||||
console.log('No songs array for player:', player);
|
console.log('No songs array for player:', player);
|
||||||
setSongs([]);
|
setSongs([]);
|
||||||
@ -65,7 +65,7 @@ const SongSubmissionScreen = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
fetchPlayerSongs();
|
fetchPlayerSongs();
|
||||||
// Include lobby in the dependency array to ensure this runs when the lobby state changes
|
// Important: This effect should run whenever the lobby state changes
|
||||||
}, [lobby, currentPlayer]);
|
}, [lobby, currentPlayer]);
|
||||||
|
|
||||||
// Extract video ID from YouTube URL
|
// Extract video ID from YouTube URL
|
||||||
@ -116,12 +116,34 @@ const SongSubmissionScreen = () => {
|
|||||||
const videoId = extractVideoId(value);
|
const videoId = extractVideoId(value);
|
||||||
if (videoId) {
|
if (videoId) {
|
||||||
setSongForm({ youtubeLink: value });
|
setSongForm({ youtubeLink: value });
|
||||||
setSelectedVideo({
|
|
||||||
|
// Set basic information immediately for a responsive UI
|
||||||
|
const basicVideoInfo = {
|
||||||
id: videoId,
|
id: videoId,
|
||||||
url: value,
|
url: value,
|
||||||
thumbnail: `https://img.youtube.com/vi/${videoId}/mqdefault.jpg`
|
thumbnail: `https://img.youtube.com/vi/${videoId}/mqdefault.jpg`
|
||||||
});
|
};
|
||||||
|
setSelectedVideo(basicVideoInfo);
|
||||||
setSearchResults([]); // Clear any existing search results
|
setSearchResults([]); // Clear any existing search results
|
||||||
|
|
||||||
|
// Fetch additional metadata from the server
|
||||||
|
try {
|
||||||
|
setIsLoadingMetadata(true);
|
||||||
|
const metadata = await getYouTubeMetadata(videoId);
|
||||||
|
if (metadata) {
|
||||||
|
// Update selected video with full metadata
|
||||||
|
setSelectedVideo({
|
||||||
|
...basicVideoInfo,
|
||||||
|
title: metadata.title || 'Unknown Title',
|
||||||
|
artist: metadata.artist || 'Unknown Artist',
|
||||||
|
thumbnail: metadata.thumbnail || basicVideoInfo.thumbnail
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching video metadata:', error);
|
||||||
|
} finally {
|
||||||
|
setIsLoadingMetadata(false);
|
||||||
|
}
|
||||||
} else if (value.trim()) {
|
} else if (value.trim()) {
|
||||||
// Clear any previous timeout
|
// Clear any previous timeout
|
||||||
if (searchTimeout.current) {
|
if (searchTimeout.current) {
|
||||||
@ -175,6 +197,9 @@ const SongSubmissionScreen = () => {
|
|||||||
// Use selected video data if available, otherwise fallback to search query or direct input
|
// Use selected video data if available, otherwise fallback to search query or direct input
|
||||||
let songData;
|
let songData;
|
||||||
|
|
||||||
|
// Generate a consistent ID format that will work for deletion later
|
||||||
|
const generateSongId = () => `song_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||||
|
|
||||||
if (selectedVideo) {
|
if (selectedVideo) {
|
||||||
// We have a selected video with full details - use all available metadata
|
// We have a selected video with full details - use all available metadata
|
||||||
songData = {
|
songData = {
|
||||||
@ -182,7 +207,7 @@ const SongSubmissionScreen = () => {
|
|||||||
title: selectedVideo.title,
|
title: selectedVideo.title,
|
||||||
artist: selectedVideo.artist,
|
artist: selectedVideo.artist,
|
||||||
thumbnail: selectedVideo.thumbnail,
|
thumbnail: selectedVideo.thumbnail,
|
||||||
id: `song_${Date.now()}_${Math.random().toString(36).substr(2, 9)}` // Generate a unique ID
|
id: generateSongId() // Consistent ID format
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log("Adding song with full metadata:", songData);
|
console.log("Adding song with full metadata:", songData);
|
||||||
@ -199,7 +224,7 @@ const SongSubmissionScreen = () => {
|
|||||||
youtubeLink: youtubeLink,
|
youtubeLink: youtubeLink,
|
||||||
// Include the videoId to make server-side processing easier
|
// Include the videoId to make server-side processing easier
|
||||||
videoId: videoId,
|
videoId: videoId,
|
||||||
id: `song_${Date.now()}_${Math.random().toString(36).substr(2, 9)}` // Generate a unique ID
|
id: generateSongId() // Consistent ID format
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log("Adding song with YouTube link:", songData);
|
console.log("Adding song with YouTube link:", songData);
|
||||||
@ -210,19 +235,35 @@ const SongSubmissionScreen = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add the song and manually update the local songs array to ensure UI reflects the change
|
// Add the song and update UI when the server responds
|
||||||
addSong(songData);
|
// We use the Promise-based approach to wait for the server-generated ID for proper deletion
|
||||||
|
|
||||||
// Optimistically add the song to the local state to immediately reflect changes in UI
|
// Add the song with callback to update songs when the server responds
|
||||||
// Note: The server response will ultimately override this if needed
|
addSong(songData)
|
||||||
setSongs(prevSongs => [...prevSongs, songData]);
|
.then((updatedLobby) => {
|
||||||
|
console.log('Song added successfully, received updated lobby:', updatedLobby);
|
||||||
|
if (updatedLobby && currentPlayer) {
|
||||||
|
const player = updatedLobby.players.find(p => p.id === currentPlayer.id);
|
||||||
|
if (player && player.songs) {
|
||||||
|
console.log('Setting songs from addSong response:', player.songs);
|
||||||
|
setSongs([...player.songs]); // Create a new array to ensure state update
|
||||||
|
} else {
|
||||||
|
console.warn('Player or songs not found in updated lobby');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.warn('Missing lobby or currentPlayer in response');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error adding song:', error);
|
||||||
|
});
|
||||||
|
|
||||||
// Reset form
|
// Reset form
|
||||||
setSongForm({ youtubeLink: '' });
|
setSongForm({ youtubeLink: '' });
|
||||||
setSearchQuery('');
|
setSearchQuery('');
|
||||||
setSearchResults([]);
|
setSearchResults([]);
|
||||||
setSelectedVideo(null);
|
setSelectedVideo(null);
|
||||||
setIsFormVisible(false);
|
setIsFormVisible(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRemoveSong = (songId) => {
|
const handleRemoveSong = (songId) => {
|
||||||
@ -267,11 +308,11 @@ const SongSubmissionScreen = () => {
|
|||||||
return (
|
return (
|
||||||
<div className="song-submission-screen">
|
<div className="song-submission-screen">
|
||||||
<header className="screen-header">
|
<header className="screen-header">
|
||||||
<h1>Submit Your Songs</h1>
|
<h1>Füge deine Lieder hinzu</h1>
|
||||||
<p className="status">
|
<p className="status">
|
||||||
{isReady
|
{isReady
|
||||||
? 'Waiting for other players...'
|
? 'Warte auf andere Spieler...'
|
||||||
: `Submit ${lobby?.settings?.songsPerPlayer || 0} songs to battle`}
|
: `Füge ${lobby?.settings?.songsPerPlayer || 0} Lieder für den Kampf hinzu`}
|
||||||
</p>
|
</p>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
@ -315,24 +356,25 @@ const SongSubmissionScreen = () => {
|
|||||||
onClick={() => setIsFormVisible(true)}
|
onClick={() => setIsFormVisible(true)}
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={faPlus} />
|
<FontAwesomeIcon icon={faPlus} />
|
||||||
<span>Add Song</span>
|
<span>Lied hinzufügen</span>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isFormVisible && (
|
{isFormVisible && (
|
||||||
<form className="song-form" onSubmit={handleAddSong}>
|
<div className="song-form-overlay">
|
||||||
<h3>Add a Song</h3>
|
<form className="song-form" onSubmit={handleAddSong}>
|
||||||
|
<h3>Lied hinzufügen</h3>
|
||||||
|
|
||||||
<div className="form-group search-group">
|
<div className="form-group search-group">
|
||||||
<label>Find a song</label>
|
<label>Lied suchen</label>
|
||||||
<div className="search-container">
|
<div className="search-container">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
name="searchQuery"
|
name="searchQuery"
|
||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
placeholder="Search YouTube or paste a link..."
|
placeholder="YouTube durchsuchen oder Link einfügen..."
|
||||||
className="search-input"
|
className="search-input"
|
||||||
/>
|
/>
|
||||||
{isSearching && (
|
{isSearching && (
|
||||||
@ -356,14 +398,14 @@ const SongSubmissionScreen = () => {
|
|||||||
{selectedVideo && (
|
{selectedVideo && (
|
||||||
<div className="selected-video">
|
<div className="selected-video">
|
||||||
<div className="preview-header">
|
<div className="preview-header">
|
||||||
<h4>Selected Song</h4>
|
<h4>Ausgewähltes Lied</h4>
|
||||||
<a
|
<a
|
||||||
href={selectedVideo.url}
|
href={selectedVideo.url}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="external-link"
|
className="external-link"
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={faExternalLinkAlt} /> View on YouTube
|
<FontAwesomeIcon icon={faExternalLinkAlt} /> Auf YouTube ansehen
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -388,7 +430,7 @@ const SongSubmissionScreen = () => {
|
|||||||
|
|
||||||
{searchResults.length > 0 && (
|
{searchResults.length > 0 && (
|
||||||
<div className="search-results">
|
<div className="search-results">
|
||||||
<h4>Search Results</h4>
|
<h4>Suchergebnisse</h4>
|
||||||
{searchResults.map(result => (
|
{searchResults.map(result => (
|
||||||
<div
|
<div
|
||||||
key={result.id}
|
key={result.id}
|
||||||
@ -423,17 +465,18 @@ const SongSubmissionScreen = () => {
|
|||||||
setSearchQuery('');
|
setSearchQuery('');
|
||||||
setSearchResults([]);
|
setSearchResults([]);
|
||||||
}}>
|
}}>
|
||||||
Cancel
|
Abbrechen
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="btn primary"
|
className="btn primary"
|
||||||
disabled={!searchQuery.trim() && !songForm.youtubeLink.trim()}
|
disabled={!searchQuery.trim() && !songForm.youtubeLink.trim()}
|
||||||
>
|
>
|
||||||
Add Song
|
Lied hinzufügen
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="action-buttons">
|
<div className="action-buttons">
|
||||||
@ -442,22 +485,22 @@ const SongSubmissionScreen = () => {
|
|||||||
className="btn primary"
|
className="btn primary"
|
||||||
onClick={handleSetReady}
|
onClick={handleSetReady}
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={faCheck} /> Ready to Battle
|
<FontAwesomeIcon icon={faCheck} /> Kampfbereit
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isReady && (
|
{isReady && (
|
||||||
<div className="waiting-message">
|
<div className="waiting-message">
|
||||||
<h3>Ready to Battle!</h3>
|
<h3>Kampfbereit!</h3>
|
||||||
<p>Waiting for other players to submit their songs...</p>
|
<p>Warte auf andere Spieler, die ihre Lieder einreichen...</p>
|
||||||
|
|
||||||
<div className="player-status">
|
<div className="player-status">
|
||||||
<h4>Players Ready</h4>
|
<h4>Spieler bereit</h4>
|
||||||
<ul className="players-ready-list">
|
<ul className="players-ready-list">
|
||||||
{lobby && lobby.players.map(player => (
|
{lobby && lobby.players.map(player => (
|
||||||
<li key={player.id} className={player.isReady ? 'ready' : 'not-ready'}>
|
<li key={player.id} className={player.isReady ? 'ready' : 'not-ready'}>
|
||||||
{player.name} {player.id === currentPlayer.id && '(You)'}
|
{player.name} {player.id === currentPlayer.id && '(Du)'}
|
||||||
{player.isReady ? (
|
{player.isReady ? (
|
||||||
<FontAwesomeIcon icon={faCheck} className="ready-icon" />
|
<FontAwesomeIcon icon={faCheck} className="ready-icon" />
|
||||||
) : (
|
) : (
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
// VotingScreen.jsx
|
// VotingScreen.jsx
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect, useMemo } from 'react';
|
||||||
import { useGame } from '../context/GameContext';
|
import { useGame } from '../context/GameContext';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import { faVoteYea, faTrophy, faMusic } from '@fortawesome/free-solid-svg-icons';
|
import { faVoteYea, faTrophy, faMusic, faCheck, faMedal, faCrown } from '@fortawesome/free-solid-svg-icons';
|
||||||
import YouTubeEmbed from './YouTubeEmbed';
|
import YouTubeEmbed from './YouTubeEmbed';
|
||||||
|
|
||||||
function VotingScreen() {
|
function VotingScreen() {
|
||||||
@ -14,11 +14,34 @@ function VotingScreen() {
|
|||||||
// Get current battle
|
// Get current battle
|
||||||
const battle = lobby?.currentBattle || null;
|
const battle = lobby?.currentBattle || null;
|
||||||
|
|
||||||
|
// Calculate the tournament phase based on the round number and total songs
|
||||||
|
const tournamentPhase = useMemo(() => {
|
||||||
|
if (!lobby || !battle) return '';
|
||||||
|
|
||||||
|
// Get total number of songs in the tournament
|
||||||
|
const totalSongs = lobby.songs?.length || 0;
|
||||||
|
|
||||||
|
if (totalSongs === 0) return 'Preliminaries';
|
||||||
|
|
||||||
|
// Calculate total rounds needed for the tournament
|
||||||
|
const totalRounds = Math.ceil(Math.log2(totalSongs));
|
||||||
|
const currentRound = battle.round + 1;
|
||||||
|
const roundsRemaining = totalRounds - currentRound;
|
||||||
|
|
||||||
|
if (roundsRemaining === 0) return 'Finals';
|
||||||
|
if (roundsRemaining === 1) return 'Semi-Finals';
|
||||||
|
if (roundsRemaining === 2) return 'Quarter-Finals';
|
||||||
|
return 'Contestant Round';
|
||||||
|
}, [lobby, battle]);
|
||||||
|
|
||||||
// Check if player has already voted
|
// Check if player has already voted
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (battle && battle.votes && currentPlayer) {
|
if (battle && battle.votes && currentPlayer) {
|
||||||
// Check if player's ID exists in votes map
|
// Check if player's ID exists in votes object
|
||||||
setHasVoted(battle.votes.has(currentPlayer.id));
|
// Since votes is sent as an object, not a Map
|
||||||
|
const votesObj = battle.votes || {};
|
||||||
|
setHasVoted(Object.prototype.hasOwnProperty.call(votesObj, currentPlayer.id) ||
|
||||||
|
Object.keys(votesObj).includes(currentPlayer.id));
|
||||||
} else {
|
} else {
|
||||||
setHasVoted(false);
|
setHasVoted(false);
|
||||||
}
|
}
|
||||||
@ -32,11 +55,11 @@ function VotingScreen() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Submit final vote
|
// Submit final vote
|
||||||
const handleSubmitVote = () => {
|
const handleSubmitVote = async () => {
|
||||||
if (!selectedSong || hasVoted) return;
|
if (!selectedSong || hasVoted) return;
|
||||||
|
|
||||||
submitVote(selectedSong);
|
await submitVote(selectedSong);
|
||||||
setHasVoted(true);
|
// Setting hasVoted is now handled by the useEffect that checks votes
|
||||||
};
|
};
|
||||||
|
|
||||||
// Get YouTube video IDs from links
|
// Get YouTube video IDs from links
|
||||||
@ -49,7 +72,7 @@ function VotingScreen() {
|
|||||||
return (match && match[2].length === 11) ? match[2] : null;
|
return (match && match[2].length === 11) ? match[2] : null;
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!battle || !battle.song1 || !battle.song2) {
|
if (!battle || !battle.song1) {
|
||||||
return (
|
return (
|
||||||
<div className="voting-screen">
|
<div className="voting-screen">
|
||||||
<h2>Preparing the next battle...</h2>
|
<h2>Preparing the next battle...</h2>
|
||||||
@ -57,6 +80,58 @@ function VotingScreen() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle "bye" rounds where a song advances automatically
|
||||||
|
if (battle.bye === true && battle.song1 && !battle.song2) {
|
||||||
|
const song1Id = getYouTubeId(battle.song1?.youtubeLink || '');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="voting-screen">
|
||||||
|
<header className="voting-header">
|
||||||
|
<h1>
|
||||||
|
<FontAwesomeIcon icon={faTrophy} /> Automatic Advance
|
||||||
|
</h1>
|
||||||
|
<div className="round-info">
|
||||||
|
<span>Round {battle.round + 1}</span>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div className="bye-container">
|
||||||
|
<div className="song-card bye-winner">
|
||||||
|
<div className="song-spotlight"></div>
|
||||||
|
<div className="song-details">
|
||||||
|
<h3>{battle.song1.title}</h3>
|
||||||
|
<p>{battle.song1.artist}</p>
|
||||||
|
<div className="auto-advance-notice">
|
||||||
|
<p>This song automatically advances to the next round!</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{song1Id ? (
|
||||||
|
<div className="video-container">
|
||||||
|
<YouTubeEmbed videoId={song1Id} />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="no-video">
|
||||||
|
<FontAwesomeIcon icon={faMusic} className="pulse-icon" />
|
||||||
|
<span>No video available</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="voting-status">
|
||||||
|
<button className="btn primary pixelated full-width" onClick={() => submitVote(battle.song1.id)}>
|
||||||
|
Continue to Next Battle
|
||||||
|
<span className="pixel-corner tl"></span>
|
||||||
|
<span className="pixel-corner tr"></span>
|
||||||
|
<span className="pixel-corner bl"></span>
|
||||||
|
<span className="pixel-corner br"></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const song1Id = getYouTubeId(battle.song1?.youtubeLink || '');
|
const song1Id = getYouTubeId(battle.song1?.youtubeLink || '');
|
||||||
const song2Id = getYouTubeId(battle.song2?.youtubeLink || '');
|
const song2Id = getYouTubeId(battle.song2?.youtubeLink || '');
|
||||||
|
|
||||||
@ -64,7 +139,8 @@ function VotingScreen() {
|
|||||||
<div className="voting-screen">
|
<div className="voting-screen">
|
||||||
<header className="voting-header">
|
<header className="voting-header">
|
||||||
<h1>
|
<h1>
|
||||||
<FontAwesomeIcon icon={faVoteYea} /> Song Battle!
|
<FontAwesomeIcon icon={tournamentPhase === 'Finals' ? faCrown : tournamentPhase === 'Semi-Finals' ? faMedal : faVoteYea} />
|
||||||
|
{tournamentPhase}
|
||||||
</h1>
|
</h1>
|
||||||
<div className="round-info">
|
<div className="round-info">
|
||||||
<span>Round {battle.round + 1}</span>
|
<span>Round {battle.round + 1}</span>
|
||||||
@ -81,9 +157,6 @@ function VotingScreen() {
|
|||||||
<div className="song-details">
|
<div className="song-details">
|
||||||
<h3>{battle.song1.title}</h3>
|
<h3>{battle.song1.title}</h3>
|
||||||
<p>{battle.song1.artist}</p>
|
<p>{battle.song1.artist}</p>
|
||||||
<div className="song-submitter">
|
|
||||||
<small>Submitted by: {battle.song1.submittedByName}</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{song1Id ? (
|
{song1Id ? (
|
||||||
@ -125,9 +198,6 @@ function VotingScreen() {
|
|||||||
<div className="song-details">
|
<div className="song-details">
|
||||||
<h3>{battle.song2.title}</h3>
|
<h3>{battle.song2.title}</h3>
|
||||||
<p>{battle.song2.artist}</p>
|
<p>{battle.song2.artist}</p>
|
||||||
<div className="song-submitter">
|
|
||||||
<small>Submitted by: {battle.song2.submittedByName}</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{song2Id ? (
|
{song2Id ? (
|
||||||
@ -165,7 +235,7 @@ function VotingScreen() {
|
|||||||
disabled={!selectedSong}
|
disabled={!selectedSong}
|
||||||
>
|
>
|
||||||
Cast Vote
|
Cast Vote
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@ -174,6 +244,27 @@ function VotingScreen() {
|
|||||||
<div className="votes-count">
|
<div className="votes-count">
|
||||||
<span>{battle.voteCount || 0}</span> of <span>{lobby?.players?.filter(p => p.isConnected).length || 0}</span> votes
|
<span>{battle.voteCount || 0}</span> of <span>{lobby?.players?.filter(p => p.isConnected).length || 0}</span> votes
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Player voting status list */}
|
||||||
|
<div className="player-votes">
|
||||||
|
<h4>Voters</h4>
|
||||||
|
<ul className="players-voted-list">
|
||||||
|
{lobby?.players?.filter(p => p.isConnected).map(player => {
|
||||||
|
// Check if this player has voted
|
||||||
|
const hasPlayerVoted = battle.votes &&
|
||||||
|
Object.keys(battle.votes).includes(player.id);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li key={player.id} className={hasPlayerVoted ? 'voted' : 'not-voted'}>
|
||||||
|
{player.name} {player.id === currentPlayer.id && '(You)'}
|
||||||
|
{hasPlayerVoted &&
|
||||||
|
<FontAwesomeIcon icon={faCheck} className="vote-icon" />
|
||||||
|
}
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -86,7 +86,7 @@ export function GameProvider({ children }) {
|
|||||||
// Save game info when lobby is joined
|
// Save game info when lobby is joined
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (lobby && currentPlayer) {
|
if (lobby && currentPlayer) {
|
||||||
localStorage.setItem('songBattleGame', JSON.stringify({
|
sessionStorage.setItem('songBattleGame', JSON.stringify({
|
||||||
lobbyId: lobby.id,
|
lobbyId: lobby.id,
|
||||||
playerName: currentPlayer.name
|
playerName: currentPlayer.name
|
||||||
}));
|
}));
|
||||||
@ -330,45 +330,52 @@ export function GameProvider({ children }) {
|
|||||||
const addSong = (song) => {
|
const addSong = (song) => {
|
||||||
if (!socket || !isConnected || !lobby) {
|
if (!socket || !isConnected || !lobby) {
|
||||||
setError('Not connected to server or no active lobby');
|
setError('Not connected to server or no active lobby');
|
||||||
return;
|
return Promise.reject('Not connected to server or no active lobby');
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Attempting to add song:', song);
|
console.log('Attempting to add song:', song);
|
||||||
console.log('Current player state:', currentPlayer);
|
console.log('Current player state:', currentPlayer);
|
||||||
console.log('Current lobby state before adding song:', lobby);
|
console.log('Current lobby state before adding song:', lobby);
|
||||||
|
|
||||||
socket.emit('add_song', { song }, (response) => {
|
return new Promise((resolve, reject) => {
|
||||||
console.log('Song addition response:', response);
|
socket.emit('add_song', { song }, (response) => {
|
||||||
if (response.error) {
|
console.log('Song addition response:', response);
|
||||||
console.error('Error adding song:', response.error);
|
if (response.error) {
|
||||||
setError(response.error);
|
console.error('Error adding song:', response.error);
|
||||||
} else if (response.lobby) {
|
setError(response.error);
|
||||||
// Log detailed lobby state for debugging
|
reject(response.error);
|
||||||
console.log('Song added successfully, full lobby response:', response.lobby);
|
} else if (response.lobby) {
|
||||||
console.log('Current player songs:', response.lobby.players.find(p => p.id === currentPlayer.id)?.songs);
|
// Log detailed lobby state for debugging
|
||||||
console.log('All players song data:',
|
console.log('Song added successfully, full lobby response:', response.lobby);
|
||||||
response.lobby.players.map(p => ({
|
console.log('Current player songs:', response.lobby.players.find(p => p.id === currentPlayer.id)?.songs);
|
||||||
name: p.name,
|
console.log('All players song data:',
|
||||||
id: p.id,
|
response.lobby.players.map(p => ({
|
||||||
songCount: p.songCount,
|
name: p.name,
|
||||||
songs: p.songs ? p.songs.map(s => ({id: s.id, title: s.title})) : 'No songs'
|
id: p.id,
|
||||||
}))
|
songCount: p.songCount,
|
||||||
);
|
songs: p.songs ? p.songs.map(s => ({id: s.id, title: s.title})) : 'No songs'
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
// Force a deep clone of the lobby to ensure React detects the change
|
// Force a deep clone of the lobby to ensure React detects the change
|
||||||
const updatedLobby = JSON.parse(JSON.stringify(response.lobby));
|
const updatedLobby = JSON.parse(JSON.stringify(response.lobby));
|
||||||
setLobby(updatedLobby);
|
setLobby(updatedLobby);
|
||||||
|
|
||||||
// Verify the state was updated correctly
|
// Verify the state was updated correctly
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
// This won't show the updated state immediately due to React's state update mechanism
|
// This won't show the updated state immediately due to React's state update mechanism
|
||||||
console.log('Lobby state after update (may not reflect immediate changes):', lobby);
|
console.log('Lobby state after update (may not reflect immediate changes):', lobby);
|
||||||
console.log('Updated lobby that was set:', updatedLobby);
|
console.log('Updated lobby that was set:', updatedLobby);
|
||||||
}, 0);
|
}, 0);
|
||||||
} else {
|
|
||||||
console.error('Song addition succeeded but no lobby data was returned');
|
// Resolve with the updated lobby
|
||||||
setError('Failed to update song list');
|
resolve(updatedLobby);
|
||||||
}
|
} else {
|
||||||
|
console.error('Song addition succeeded but no lobby data was returned');
|
||||||
|
setError('Failed to update song list');
|
||||||
|
reject('Failed to update song list');
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -392,6 +399,26 @@ export function GameProvider({ children }) {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Get metadata for a YouTube video by ID
|
||||||
|
const getYouTubeMetadata = (videoId) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (!socket || !isConnected) {
|
||||||
|
setError('Not connected to server');
|
||||||
|
reject('Not connected to server');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
socket.emit('get_video_metadata', { videoId }, (response) => {
|
||||||
|
if (response.error) {
|
||||||
|
setError(response.error);
|
||||||
|
reject(response.error);
|
||||||
|
} else {
|
||||||
|
resolve(response.metadata);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
// Remove a song
|
// Remove a song
|
||||||
const removeSong = (songId) => {
|
const removeSong = (songId) => {
|
||||||
if (!socket || !isConnected || !lobby) {
|
if (!socket || !isConnected || !lobby) {
|
||||||
@ -456,7 +483,8 @@ export function GameProvider({ children }) {
|
|||||||
setPlayerReady,
|
setPlayerReady,
|
||||||
submitVote,
|
submitVote,
|
||||||
leaveLobby,
|
leaveLobby,
|
||||||
searchYouTube
|
searchYouTube,
|
||||||
|
getYouTubeMetadata
|
||||||
}}>
|
}}>
|
||||||
{children}
|
{children}
|
||||||
</GameContext.Provider>
|
</GameContext.Provider>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import ReactDOM from "react-dom/client";
|
import ReactDOM from "react-dom/client";
|
||||||
import App from "./App.jsx";
|
import App from "./App.jsx";
|
||||||
import "@fontsource/bangers";
|
import "@fontsource/press-start-2p";
|
||||||
import "./common/styles/main.sass";
|
import "./common/styles/main.sass";
|
||||||
import { SocketProvider, GameProvider } from "./context/GameContext.jsx";
|
import { SocketProvider, GameProvider } from "./context/GameContext.jsx";
|
||||||
|
|
||||||
|
@ -28,8 +28,7 @@ class GameManager {
|
|||||||
settings: {
|
settings: {
|
||||||
songsPerPlayer: 3,
|
songsPerPlayer: 3,
|
||||||
maxPlayers: 10,
|
maxPlayers: 10,
|
||||||
minPlayers: 3,
|
minPlayers: 3
|
||||||
requireYoutubeLinks: false
|
|
||||||
},
|
},
|
||||||
players: [{
|
players: [{
|
||||||
id: hostId,
|
id: hostId,
|
||||||
@ -300,15 +299,12 @@ class GameManager {
|
|||||||
return { error: 'Maximum number of songs reached' };
|
return { error: 'Maximum number of songs reached' };
|
||||||
}
|
}
|
||||||
|
|
||||||
// We only require the YouTube link now
|
// If we have a YouTube link, validate it
|
||||||
if (!song.youtubeLink) {
|
if (song.youtubeLink) {
|
||||||
return { error: 'YouTube link is required' };
|
const videoId = await youtubeAPI.extractVideoId(song.youtubeLink);
|
||||||
}
|
if (!videoId) {
|
||||||
|
return { error: 'Invalid YouTube link' };
|
||||||
// If the YouTube link isn't valid, return an error
|
}
|
||||||
const videoId = await youtubeAPI.extractVideoId(song.youtubeLink);
|
|
||||||
if (!videoId) {
|
|
||||||
return { error: 'Invalid YouTube link' };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle async metadata fetching
|
// Handle async metadata fetching
|
||||||
@ -329,6 +325,20 @@ class GameManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get metadata for a single YouTube video
|
||||||
|
* @param {string} videoId - YouTube video ID
|
||||||
|
* @returns {Promise<Object>} Video metadata
|
||||||
|
*/
|
||||||
|
async getYouTubeVideoMetadata(videoId) {
|
||||||
|
try {
|
||||||
|
return await youtubeAPI.getVideoMetadata(videoId);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error getting YouTube video metadata:', error);
|
||||||
|
return { error: 'Failed to get video metadata' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper method to add a song to a player
|
* Helper method to add a song to a player
|
||||||
* @param {number} playerIndex - Index of the player in the lobby
|
* @param {number} playerIndex - Index of the player in the lobby
|
||||||
@ -539,8 +549,15 @@ class GameManager {
|
|||||||
return { error: 'Invalid song ID' };
|
return { error: 'Invalid song ID' };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Record the vote
|
// Find player name for display purposes
|
||||||
lobby.currentBattle.votes.set(playerId, songId);
|
const player = lobby.players.find(p => p.id === playerId);
|
||||||
|
const playerName = player ? player.name : 'Unknown Player';
|
||||||
|
|
||||||
|
// Record the vote with player name for UI display
|
||||||
|
lobby.currentBattle.votes.set(playerId, {
|
||||||
|
songId,
|
||||||
|
playerName
|
||||||
|
});
|
||||||
|
|
||||||
// Update vote counts
|
// Update vote counts
|
||||||
if (songId === lobby.currentBattle.song1.id) {
|
if (songId === lobby.currentBattle.song1.id) {
|
||||||
@ -549,6 +566,9 @@ class GameManager {
|
|||||||
lobby.currentBattle.song2Votes++;
|
lobby.currentBattle.song2Votes++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add a voteCount attribute for easier UI rendering
|
||||||
|
lobby.currentBattle.voteCount = lobby.currentBattle.votes.size;
|
||||||
|
|
||||||
// Check if all connected players have voted
|
// Check if all connected players have voted
|
||||||
const connectedPlayers = lobby.players.filter(p => p.isConnected).length;
|
const connectedPlayers = lobby.players.filter(p => p.isConnected).length;
|
||||||
const voteCount = lobby.currentBattle.votes.size;
|
const voteCount = lobby.currentBattle.votes.size;
|
||||||
|
@ -150,10 +150,11 @@ io.on('connection', (socket) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Add a song
|
// Add a song
|
||||||
socket.on('add_song', ({ song }, callback) => {
|
socket.on('add_song', async ({ song }, callback) => {
|
||||||
try {
|
try {
|
||||||
console.log(`[DEBUG] Attempting to add song: ${JSON.stringify(song)}`);
|
console.log(`[DEBUG] Attempting to add song: ${JSON.stringify(song)}`);
|
||||||
const result = gameManager.addSong(socket.id, song);
|
// Fix: Properly await the result of the async addSong function
|
||||||
|
const result = await gameManager.addSong(socket.id, song);
|
||||||
|
|
||||||
if (result.error) {
|
if (result.error) {
|
||||||
console.log(`[DEBUG] Error adding song: ${result.error}`);
|
console.log(`[DEBUG] Error adding song: ${result.error}`);
|
||||||
@ -225,6 +226,24 @@ io.on('connection', (socket) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Get metadata for a single YouTube video
|
||||||
|
socket.on('get_video_metadata', async ({ videoId }, callback) => {
|
||||||
|
try {
|
||||||
|
if (!videoId || typeof videoId !== 'string') {
|
||||||
|
if (callback) callback({ error: 'Invalid video ID' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const metadata = await gameManager.getYouTubeVideoMetadata(videoId);
|
||||||
|
|
||||||
|
// Send response to client
|
||||||
|
if (callback) callback({ metadata });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error getting YouTube metadata:', error);
|
||||||
|
if (callback) callback({ error: 'Failed to get YouTube metadata' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Mark player as ready
|
// Mark player as ready
|
||||||
socket.on('player_ready', (_, callback) => {
|
socket.on('player_ready', (_, callback) => {
|
||||||
try {
|
try {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user