Private
Public Access
Compare commits
170
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3e90bbe5cf | ||
|
|
1d8d0ac110 | ||
|
|
007c91f7e9 | ||
|
|
e70cb57176 | ||
|
|
db54899ed4 | ||
|
|
558c8a3bad | ||
|
|
2be49ebdf1 | ||
|
|
8b428260d7 | ||
|
|
306add033a | ||
|
|
f451df283c | ||
|
|
8d542e683e | ||
|
|
dff46cdc6f | ||
|
|
82fff0cdf0 | ||
|
|
4bf7ac358d | ||
|
|
1a97c2c4b4 | ||
|
|
9ea83d7e6e | ||
|
|
f3e4ff211d | ||
|
|
7a86767db1 | ||
|
|
2b1689b321 | ||
|
|
cf56b10aba | ||
|
|
7063704539 | ||
|
|
199bb7e525 | ||
|
|
5daaf71ae7 | ||
|
|
0aeec47996 | ||
|
|
3d67b8f2d9 | ||
|
|
dd9a190fd9 | ||
|
|
f5e5019ea8 | ||
|
|
3f51eb5db6 | ||
|
|
55ef7c9301 | ||
|
|
ddfa395c6b | ||
|
|
8694627817 | ||
|
|
f796819af4 | ||
|
|
b209ad4220 | ||
|
|
df1eefdfe0 | ||
|
|
7aaaf120b2 | ||
|
|
e48d651eb5 | ||
|
|
d704be5bff | ||
|
|
6bf908b43e | ||
|
|
085e010907 | ||
|
|
8935216525 | ||
|
|
1d8efa4e61 | ||
|
|
69fce52bed | ||
|
|
13adf908bf | ||
|
|
3bbfb8740f | ||
|
|
0d04ec91e7 | ||
|
|
20a969705d | ||
|
|
4944d2aa21 | ||
|
|
2ec37a802b | ||
|
|
6a5ba84b5e | ||
|
|
6be0d52fb7 | ||
|
|
f493f94368 | ||
|
|
cd93a26c2c | ||
|
|
175581cdd5 | ||
|
|
5f856e4d70 | ||
|
|
e0495d182e | ||
|
|
0b7c1406cf | ||
|
|
30edc5782b | ||
|
|
d92a7f3aa0 | ||
|
|
f72cd45afd | ||
|
|
51bd909879 | ||
|
|
db37ab45b2 | ||
|
|
9256db7f8c | ||
|
|
d9059acb78 | ||
|
|
5da8a04c18 | ||
|
|
ba8a0befb0 | ||
|
|
5ac291de81 | ||
|
|
991b114a3c | ||
|
|
c79584c7d2 | ||
|
|
e77c8a8f7c | ||
|
|
c2308ba408 | ||
|
|
e5a22cdfe3 | ||
|
|
09b0d21621 | ||
|
|
9aef27a0eb | ||
|
|
c00ed57240 | ||
|
|
ef4cf6ef69 | ||
|
|
dc9c5f6545 | ||
|
|
25f2aaab8c | ||
|
|
0cc9cdaf07 | ||
|
|
247f437445 | ||
|
|
0e94367223 | ||
|
|
a9ee28b395 | ||
|
|
bd074c5c9d | ||
|
|
42c552c528 | ||
|
|
3b376e5386 | ||
|
|
45a8e6b4a1 | ||
|
|
1f8e9c3c56 | ||
|
|
a511b86db8 | ||
|
|
1c0ad054bb | ||
|
|
5a8799bb7f | ||
|
|
6c443d8e86 | ||
|
|
8795fedda9 | ||
|
|
588fb57299 | ||
|
|
eb345e17ca | ||
|
|
c2693c4648 | ||
|
|
43efc16562 | ||
|
|
80d6440ece | ||
|
|
5ee972f003 | ||
|
|
6f3edb41ea | ||
|
|
c52939a7a3 | ||
|
|
573d409606 | ||
|
|
9a58bc9a5e | ||
|
|
8780800dff | ||
|
|
f442942faf | ||
|
|
a61d881a4e | ||
|
|
926b614136 | ||
|
|
c0c84f4651 | ||
|
|
176e255037 | ||
|
|
b134358e9e | ||
|
|
3525aaeeb7 | ||
|
|
af67ec3931 | ||
|
|
d515f42cfd | ||
|
|
5d6aff8d90 | ||
|
|
15ba26ccf2 | ||
|
|
d3fa0cbbf9 | ||
|
|
ed776ca75b | ||
|
|
67133a4efa | ||
|
|
6044ee5c50 | ||
|
|
184925ab13 | ||
|
|
5a5467fda9 | ||
|
|
f135746826 | ||
|
|
62915fe5e4 | ||
|
|
68a25aafa4 | ||
|
|
9fff5bd5d1 | ||
|
|
304663adb7 | ||
|
|
cd593e99fc | ||
|
|
9d51654aec | ||
|
|
82465322f2 | ||
|
|
055e59d896 | ||
|
|
3db8a30115 | ||
|
|
2e8d878337 | ||
|
|
28221e092a | ||
|
|
0c0b8ae920 | ||
|
|
98f6e8cb6e | ||
|
|
4239177563 | ||
|
|
e9c6795eb7 | ||
|
|
fb8a54f687 | ||
|
|
0144a3953c | ||
|
|
acbe9c7f63 | ||
|
|
e2b227ed7a | ||
|
|
c0dcc2896a | ||
|
|
92bfa5b301 | ||
|
|
826690769f | ||
|
|
6b3e19b063 | ||
|
|
eff849549b | ||
|
|
63a0f442ed | ||
|
|
1cf21622b8 | ||
|
|
85a440017c | ||
|
|
d388e25192 | ||
|
|
5b55a6ce73 | ||
|
|
c660c13ea2 | ||
|
|
d186a96f0d | ||
|
|
b57442bec1 | ||
|
|
086d6c601e | ||
|
|
15806a6e04 | ||
|
|
fe2de91e91 | ||
|
|
76143fca3e | ||
|
|
7219471a86 | ||
|
|
b55c223d8a | ||
|
|
23547f4237 | ||
|
|
4cdca43ecc | ||
|
|
fa0fc0743d | ||
|
|
dd4b410624 | ||
|
|
1b5e87c033 | ||
|
|
2daab7140e | ||
|
|
8355cc90ed | ||
|
|
c909c036a3 | ||
|
|
3bbc96c76c | ||
|
|
6caf340302 | ||
|
|
98b71d75e9 | ||
|
|
aae8b9ebec |
@@ -0,0 +1,13 @@
|
|||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
node_modules
|
||||||
|
var/cache
|
||||||
|
var/log
|
||||||
|
var/sessions
|
||||||
|
public/build
|
||||||
|
*.md
|
||||||
|
docker-compose*.yml
|
||||||
|
compose.override.yaml
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
+884
@@ -0,0 +1,884 @@
|
|||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
end_of_line = lf
|
||||||
|
indent_size = 2
|
||||||
|
indent_style = space
|
||||||
|
insert_final_newline = true
|
||||||
|
max_line_length = 120
|
||||||
|
tab_width = 2
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
ij_continuation_indent_size = 8
|
||||||
|
ij_formatter_off_tag = @formatter:off
|
||||||
|
ij_formatter_on_tag = @formatter:on
|
||||||
|
ij_formatter_tags_enabled = true
|
||||||
|
ij_smart_tabs = false
|
||||||
|
ij_visual_guides = 80,120
|
||||||
|
ij_wrap_on_typing = false
|
||||||
|
|
||||||
|
[*.blade.php]
|
||||||
|
ij_visual_guides =
|
||||||
|
ij_blade_keep_indents_on_empty_lines = false
|
||||||
|
|
||||||
|
[*.css]
|
||||||
|
indent_size = 2
|
||||||
|
tab_width = 2
|
||||||
|
ij_continuation_indent_size = 4
|
||||||
|
ij_visual_guides =
|
||||||
|
ij_css_align_closing_brace_with_properties = false
|
||||||
|
ij_css_blank_lines_around_nested_selector = 1
|
||||||
|
ij_css_blank_lines_between_blocks = 1
|
||||||
|
ij_css_block_comment_add_space = false
|
||||||
|
ij_css_brace_placement = end_of_line
|
||||||
|
ij_css_enforce_quotes_on_format = false
|
||||||
|
ij_css_hex_color_long_format = false
|
||||||
|
ij_css_hex_color_lower_case = false
|
||||||
|
ij_css_hex_color_short_format = false
|
||||||
|
ij_css_hex_color_upper_case = false
|
||||||
|
ij_css_keep_blank_lines_in_code = 2
|
||||||
|
ij_css_keep_indents_on_empty_lines = false
|
||||||
|
ij_css_keep_single_line_blocks = false
|
||||||
|
ij_css_properties_order = font,font-family,font-size,font-weight,font-style,font-variant,font-size-adjust,font-stretch,line-height,position,z-index,top,right,bottom,left,display,visibility,float,clear,overflow,overflow-x,overflow-y,clip,zoom,align-content,align-items,align-self,flex,flex-flow,flex-basis,flex-direction,flex-grow,flex-shrink,flex-wrap,justify-content,order,box-sizing,width,min-width,max-width,height,min-height,max-height,margin,margin-top,margin-right,margin-bottom,margin-left,padding,padding-top,padding-right,padding-bottom,padding-left,table-layout,empty-cells,caption-side,border-spacing,border-collapse,list-style,list-style-position,list-style-type,list-style-image,content,quotes,counter-reset,counter-increment,resize,cursor,user-select,nav-index,nav-up,nav-right,nav-down,nav-left,transition,transition-delay,transition-timing-function,transition-duration,transition-property,transform,transform-origin,animation,animation-name,animation-duration,animation-play-state,animation-timing-function,animation-delay,animation-iteration-count,animation-direction,text-align,text-align-last,vertical-align,white-space,text-decoration,text-emphasis,text-emphasis-color,text-emphasis-style,text-emphasis-position,text-indent,text-justify,letter-spacing,word-spacing,text-outline,text-transform,text-wrap,text-overflow,text-overflow-ellipsis,text-overflow-mode,word-wrap,word-break,tab-size,hyphens,pointer-events,opacity,color,border,border-width,border-style,border-color,border-top,border-top-width,border-top-style,border-top-color,border-right,border-right-width,border-right-style,border-right-color,border-bottom,border-bottom-width,border-bottom-style,border-bottom-color,border-left,border-left-width,border-left-style,border-left-color,border-radius,border-top-left-radius,border-top-right-radius,border-bottom-right-radius,border-bottom-left-radius,border-image,border-image-source,border-image-slice,border-image-width,border-image-outset,border-image-repeat,outline,outline-width,outline-style,outline-color,outline-offset,background,background-color,background-image,background-repeat,background-attachment,background-position,background-position-x,background-position-y,background-clip,background-origin,background-size,box-decoration-break,box-shadow,text-shadow
|
||||||
|
ij_css_space_after_colon = true
|
||||||
|
ij_css_space_before_opening_brace = true
|
||||||
|
ij_css_use_double_quotes = true
|
||||||
|
ij_css_value_alignment = do_not_align
|
||||||
|
|
||||||
|
[*.feature]
|
||||||
|
indent_size = 2
|
||||||
|
ij_visual_guides =
|
||||||
|
ij_gherkin_keep_indents_on_empty_lines = false
|
||||||
|
|
||||||
|
[*.less]
|
||||||
|
indent_size = 2
|
||||||
|
ij_visual_guides =
|
||||||
|
ij_less_align_closing_brace_with_properties = false
|
||||||
|
ij_less_blank_lines_around_nested_selector = 1
|
||||||
|
ij_less_blank_lines_between_blocks = 1
|
||||||
|
ij_less_block_comment_add_space = false
|
||||||
|
ij_less_brace_placement = 0
|
||||||
|
ij_less_enforce_quotes_on_format = false
|
||||||
|
ij_less_hex_color_long_format = false
|
||||||
|
ij_less_hex_color_lower_case = false
|
||||||
|
ij_less_hex_color_short_format = false
|
||||||
|
ij_less_hex_color_upper_case = false
|
||||||
|
ij_less_keep_blank_lines_in_code = 2
|
||||||
|
ij_less_keep_indents_on_empty_lines = false
|
||||||
|
ij_less_keep_single_line_blocks = false
|
||||||
|
ij_less_line_comment_add_space = false
|
||||||
|
ij_less_line_comment_at_first_column = false
|
||||||
|
ij_less_properties_order = font,font-family,font-size,font-weight,font-style,font-variant,font-size-adjust,font-stretch,line-height,position,z-index,top,right,bottom,left,display,visibility,float,clear,overflow,overflow-x,overflow-y,clip,zoom,align-content,align-items,align-self,flex,flex-flow,flex-basis,flex-direction,flex-grow,flex-shrink,flex-wrap,justify-content,order,box-sizing,width,min-width,max-width,height,min-height,max-height,margin,margin-top,margin-right,margin-bottom,margin-left,padding,padding-top,padding-right,padding-bottom,padding-left,table-layout,empty-cells,caption-side,border-spacing,border-collapse,list-style,list-style-position,list-style-type,list-style-image,content,quotes,counter-reset,counter-increment,resize,cursor,user-select,nav-index,nav-up,nav-right,nav-down,nav-left,transition,transition-delay,transition-timing-function,transition-duration,transition-property,transform,transform-origin,animation,animation-name,animation-duration,animation-play-state,animation-timing-function,animation-delay,animation-iteration-count,animation-direction,text-align,text-align-last,vertical-align,white-space,text-decoration,text-emphasis,text-emphasis-color,text-emphasis-style,text-emphasis-position,text-indent,text-justify,letter-spacing,word-spacing,text-outline,text-transform,text-wrap,text-overflow,text-overflow-ellipsis,text-overflow-mode,word-wrap,word-break,tab-size,hyphens,pointer-events,opacity,color,border,border-width,border-style,border-color,border-top,border-top-width,border-top-style,border-top-color,border-right,border-right-width,border-right-style,border-right-color,border-bottom,border-bottom-width,border-bottom-style,border-bottom-color,border-left,border-left-width,border-left-style,border-left-color,border-radius,border-top-left-radius,border-top-right-radius,border-bottom-right-radius,border-bottom-left-radius,border-image,border-image-source,border-image-slice,border-image-width,border-image-outset,border-image-repeat,outline,outline-width,outline-style,outline-color,outline-offset,background,background-color,background-image,background-repeat,background-attachment,background-position,background-position-x,background-position-y,background-clip,background-origin,background-size,box-decoration-break,box-shadow,text-shadow
|
||||||
|
ij_less_space_after_colon = true
|
||||||
|
ij_less_space_before_opening_brace = true
|
||||||
|
ij_less_use_double_quotes = true
|
||||||
|
ij_less_value_alignment = 0
|
||||||
|
|
||||||
|
[*.neon]
|
||||||
|
ij_visual_guides =
|
||||||
|
|
||||||
|
[*.sass]
|
||||||
|
indent_size = 2
|
||||||
|
ij_visual_guides =
|
||||||
|
ij_sass_align_closing_brace_with_properties = false
|
||||||
|
ij_sass_blank_lines_around_nested_selector = 1
|
||||||
|
ij_sass_blank_lines_between_blocks = 1
|
||||||
|
ij_sass_brace_placement = 0
|
||||||
|
ij_sass_enforce_quotes_on_format = false
|
||||||
|
ij_sass_hex_color_long_format = false
|
||||||
|
ij_sass_hex_color_lower_case = false
|
||||||
|
ij_sass_hex_color_short_format = false
|
||||||
|
ij_sass_hex_color_upper_case = false
|
||||||
|
ij_sass_keep_blank_lines_in_code = 2
|
||||||
|
ij_sass_keep_indents_on_empty_lines = false
|
||||||
|
ij_sass_keep_single_line_blocks = false
|
||||||
|
ij_sass_line_comment_add_space = false
|
||||||
|
ij_sass_line_comment_at_first_column = false
|
||||||
|
ij_sass_properties_order = font,font-family,font-size,font-weight,font-style,font-variant,font-size-adjust,font-stretch,line-height,position,z-index,top,right,bottom,left,display,visibility,float,clear,overflow,overflow-x,overflow-y,clip,zoom,align-content,align-items,align-self,flex,flex-flow,flex-basis,flex-direction,flex-grow,flex-shrink,flex-wrap,justify-content,order,box-sizing,width,min-width,max-width,height,min-height,max-height,margin,margin-top,margin-right,margin-bottom,margin-left,padding,padding-top,padding-right,padding-bottom,padding-left,table-layout,empty-cells,caption-side,border-spacing,border-collapse,list-style,list-style-position,list-style-type,list-style-image,content,quotes,counter-reset,counter-increment,resize,cursor,user-select,nav-index,nav-up,nav-right,nav-down,nav-left,transition,transition-delay,transition-timing-function,transition-duration,transition-property,transform,transform-origin,animation,animation-name,animation-duration,animation-play-state,animation-timing-function,animation-delay,animation-iteration-count,animation-direction,text-align,text-align-last,vertical-align,white-space,text-decoration,text-emphasis,text-emphasis-color,text-emphasis-style,text-emphasis-position,text-indent,text-justify,letter-spacing,word-spacing,text-outline,text-transform,text-wrap,text-overflow,text-overflow-ellipsis,text-overflow-mode,word-wrap,word-break,tab-size,hyphens,pointer-events,opacity,color,border,border-width,border-style,border-color,border-top,border-top-width,border-top-style,border-top-color,border-right,border-right-width,border-right-style,border-right-color,border-bottom,border-bottom-width,border-bottom-style,border-bottom-color,border-left,border-left-width,border-left-style,border-left-color,border-radius,border-top-left-radius,border-top-right-radius,border-bottom-right-radius,border-bottom-left-radius,border-image,border-image-source,border-image-slice,border-image-width,border-image-outset,border-image-repeat,outline,outline-width,outline-style,outline-color,outline-offset,background,background-color,background-image,background-repeat,background-attachment,background-position,background-position-x,background-position-y,background-clip,background-origin,background-size,box-decoration-break,box-shadow,text-shadow
|
||||||
|
ij_sass_space_after_colon = true
|
||||||
|
ij_sass_space_before_opening_brace = true
|
||||||
|
ij_sass_use_double_quotes = true
|
||||||
|
ij_sass_value_alignment = 0
|
||||||
|
|
||||||
|
[*.scss]
|
||||||
|
indent_size = 2
|
||||||
|
tab_width = 2
|
||||||
|
ij_continuation_indent_size = 2
|
||||||
|
ij_visual_guides =
|
||||||
|
ij_scss_align_closing_brace_with_properties = false
|
||||||
|
ij_scss_blank_lines_around_nested_selector = 1
|
||||||
|
ij_scss_blank_lines_between_blocks = 1
|
||||||
|
ij_scss_block_comment_add_space = false
|
||||||
|
ij_scss_brace_placement = 0
|
||||||
|
ij_scss_enforce_quotes_on_format = false
|
||||||
|
ij_scss_hex_color_long_format = false
|
||||||
|
ij_scss_hex_color_lower_case = false
|
||||||
|
ij_scss_hex_color_short_format = false
|
||||||
|
ij_scss_hex_color_upper_case = false
|
||||||
|
ij_scss_keep_blank_lines_in_code = 2
|
||||||
|
ij_scss_keep_indents_on_empty_lines = false
|
||||||
|
ij_scss_keep_single_line_blocks = false
|
||||||
|
ij_scss_line_comment_add_space = false
|
||||||
|
ij_scss_line_comment_at_first_column = false
|
||||||
|
ij_scss_properties_order = font,font-family,font-size,font-weight,font-style,font-variant,font-size-adjust,font-stretch,line-height,position,z-index,top,right,bottom,left,display,visibility,float,clear,overflow,overflow-x,overflow-y,clip,zoom,align-content,align-items,align-self,flex,flex-flow,flex-basis,flex-direction,flex-grow,flex-shrink,flex-wrap,justify-content,order,box-sizing,width,min-width,max-width,height,min-height,max-height,margin,margin-top,margin-right,margin-bottom,margin-left,padding,padding-top,padding-right,padding-bottom,padding-left,table-layout,empty-cells,caption-side,border-spacing,border-collapse,list-style,list-style-position,list-style-type,list-style-image,content,quotes,counter-reset,counter-increment,resize,cursor,user-select,nav-index,nav-up,nav-right,nav-down,nav-left,transition,transition-delay,transition-timing-function,transition-duration,transition-property,transform,transform-origin,animation,animation-name,animation-duration,animation-play-state,animation-timing-function,animation-delay,animation-iteration-count,animation-direction,text-align,text-align-last,vertical-align,white-space,text-decoration,text-emphasis,text-emphasis-color,text-emphasis-style,text-emphasis-position,text-indent,text-justify,letter-spacing,word-spacing,text-outline,text-transform,text-wrap,text-overflow,text-overflow-ellipsis,text-overflow-mode,word-wrap,word-break,tab-size,hyphens,pointer-events,opacity,color,border,border-width,border-style,border-color,border-top,border-top-width,border-top-style,border-top-color,border-right,border-right-width,border-right-style,border-right-color,border-bottom,border-bottom-width,border-bottom-style,border-bottom-color,border-left,border-left-width,border-left-style,border-left-color,border-radius,border-top-left-radius,border-top-right-radius,border-bottom-right-radius,border-bottom-left-radius,border-image,border-image-source,border-image-slice,border-image-width,border-image-outset,border-image-repeat,outline,outline-width,outline-style,outline-color,outline-offset,background,background-color,background-image,background-repeat,background-attachment,background-position,background-position-x,background-position-y,background-clip,background-origin,background-size,box-decoration-break,box-shadow,text-shadow
|
||||||
|
ij_scss_space_after_colon = true
|
||||||
|
ij_scss_space_before_opening_brace = true
|
||||||
|
ij_scss_use_double_quotes = true
|
||||||
|
ij_scss_value_alignment = 0
|
||||||
|
|
||||||
|
[*.twig]
|
||||||
|
indent_size = 2
|
||||||
|
tab_width = 2
|
||||||
|
ij_continuation_indent_size = 2
|
||||||
|
ij_visual_guides =
|
||||||
|
ij_twig_keep_indents_on_empty_lines = false
|
||||||
|
ij_twig_spaces_inside_comments_delimiters = true
|
||||||
|
ij_twig_spaces_inside_delimiters = true
|
||||||
|
ij_twig_spaces_inside_variable_delimiters = true
|
||||||
|
|
||||||
|
[*.vue]
|
||||||
|
indent_size = 2
|
||||||
|
tab_width = 2
|
||||||
|
ij_continuation_indent_size = 4
|
||||||
|
ij_visual_guides =
|
||||||
|
ij_vue_indent_children_of_top_level = template
|
||||||
|
ij_vue_interpolation_new_line_after_start_delimiter = true
|
||||||
|
ij_vue_interpolation_new_line_before_end_delimiter = true
|
||||||
|
ij_vue_interpolation_wrap = off
|
||||||
|
ij_vue_keep_indents_on_empty_lines = false
|
||||||
|
ij_vue_spaces_within_interpolation_expressions = true
|
||||||
|
|
||||||
|
[.editorconfig]
|
||||||
|
ij_visual_guides =
|
||||||
|
ij_editorconfig_align_group_field_declarations = false
|
||||||
|
ij_editorconfig_space_after_colon = true
|
||||||
|
ij_editorconfig_space_after_comma = true
|
||||||
|
ij_editorconfig_space_before_colon = true
|
||||||
|
ij_editorconfig_space_before_comma = false
|
||||||
|
ij_editorconfig_spaces_around_assignment_operators = true
|
||||||
|
|
||||||
|
[{*.ant,*.fxml,*.jhm,*.jnlp,*.jrxml,*.rng,*.tld,*.wsdl,*.xlf,*.xml,*.xsd,*.xsl,*.xslt,*.xul,phpunit.xml.dist}]
|
||||||
|
indent_size = 2
|
||||||
|
tab_width = 2
|
||||||
|
ij_visual_guides =
|
||||||
|
ij_xml_align_attributes = true
|
||||||
|
ij_xml_align_text = false
|
||||||
|
ij_xml_attribute_wrap = normal
|
||||||
|
ij_xml_block_comment_add_space = false
|
||||||
|
ij_xml_block_comment_at_first_column = true
|
||||||
|
ij_xml_keep_blank_lines = 2
|
||||||
|
ij_xml_keep_indents_on_empty_lines = false
|
||||||
|
ij_xml_keep_line_breaks = true
|
||||||
|
ij_xml_keep_line_breaks_in_text = true
|
||||||
|
ij_xml_keep_whitespaces = false
|
||||||
|
ij_xml_keep_whitespaces_around_cdata = preserve
|
||||||
|
ij_xml_keep_whitespaces_inside_cdata = false
|
||||||
|
ij_xml_line_comment_at_first_column = true
|
||||||
|
ij_xml_space_after_tag_name = false
|
||||||
|
ij_xml_space_around_equals_in_attribute = false
|
||||||
|
ij_xml_space_inside_empty_tag = true
|
||||||
|
ij_xml_text_wrap = normal
|
||||||
|
|
||||||
|
[{*.ats,*.cts,*.mts,*.ts,*.tsx}]
|
||||||
|
indent_size = 2
|
||||||
|
tab_width = 2
|
||||||
|
ij_continuation_indent_size = 2
|
||||||
|
ij_visual_guides =
|
||||||
|
ij_typescript_align_imports = false
|
||||||
|
ij_typescript_align_multiline_array_initializer_expression = false
|
||||||
|
ij_typescript_align_multiline_binary_operation = false
|
||||||
|
ij_typescript_align_multiline_chained_methods = false
|
||||||
|
ij_typescript_align_multiline_extends_list = false
|
||||||
|
ij_typescript_align_multiline_for = true
|
||||||
|
ij_typescript_align_multiline_parameters = true
|
||||||
|
ij_typescript_align_multiline_parameters_in_calls = false
|
||||||
|
ij_typescript_align_multiline_ternary_operation = false
|
||||||
|
ij_typescript_align_object_properties = 0
|
||||||
|
ij_typescript_align_union_types = false
|
||||||
|
ij_typescript_align_var_statements = 0
|
||||||
|
ij_typescript_array_initializer_new_line_after_left_brace = false
|
||||||
|
ij_typescript_array_initializer_right_brace_on_new_line = false
|
||||||
|
ij_typescript_array_initializer_wrap = off
|
||||||
|
ij_typescript_assignment_wrap = off
|
||||||
|
ij_typescript_binary_operation_sign_on_next_line = false
|
||||||
|
ij_typescript_binary_operation_wrap = off
|
||||||
|
ij_typescript_blacklist_imports = rxjs/Rx,node_modules/**,**/node_modules/**,@angular/material,@angular/material/typings/**
|
||||||
|
ij_typescript_blank_lines_after_imports = 1
|
||||||
|
ij_typescript_blank_lines_around_class = 1
|
||||||
|
ij_typescript_blank_lines_around_field = 0
|
||||||
|
ij_typescript_blank_lines_around_field_in_interface = 0
|
||||||
|
ij_typescript_blank_lines_around_function = 1
|
||||||
|
ij_typescript_blank_lines_around_method = 1
|
||||||
|
ij_typescript_blank_lines_around_method_in_interface = 1
|
||||||
|
ij_typescript_block_brace_style = end_of_line
|
||||||
|
ij_typescript_block_comment_add_space = false
|
||||||
|
ij_typescript_block_comment_at_first_column = true
|
||||||
|
ij_typescript_call_parameters_new_line_after_left_paren = false
|
||||||
|
ij_typescript_call_parameters_right_paren_on_new_line = false
|
||||||
|
ij_typescript_call_parameters_wrap = off
|
||||||
|
ij_typescript_catch_on_new_line = false
|
||||||
|
ij_typescript_chained_call_dot_on_new_line = true
|
||||||
|
ij_typescript_class_brace_style = end_of_line
|
||||||
|
ij_typescript_class_decorator_wrap = split_into_lines
|
||||||
|
ij_typescript_class_field_decorator_wrap = off
|
||||||
|
ij_typescript_class_method_decorator_wrap = off
|
||||||
|
ij_typescript_comma_on_new_line = false
|
||||||
|
ij_typescript_do_while_brace_force = never
|
||||||
|
ij_typescript_else_on_new_line = false
|
||||||
|
ij_typescript_enforce_trailing_comma = whenmultiline
|
||||||
|
ij_typescript_enum_constants_wrap = on_every_item
|
||||||
|
ij_typescript_extends_keyword_wrap = off
|
||||||
|
ij_typescript_extends_list_wrap = off
|
||||||
|
ij_typescript_field_prefix = _
|
||||||
|
ij_typescript_file_name_style = relaxed
|
||||||
|
ij_typescript_finally_on_new_line = false
|
||||||
|
ij_typescript_for_brace_force = never
|
||||||
|
ij_typescript_for_statement_new_line_after_left_paren = false
|
||||||
|
ij_typescript_for_statement_right_paren_on_new_line = false
|
||||||
|
ij_typescript_for_statement_wrap = off
|
||||||
|
ij_typescript_force_quote_style = true
|
||||||
|
ij_typescript_force_semicolon_style = true
|
||||||
|
ij_typescript_function_expression_brace_style = end_of_line
|
||||||
|
ij_typescript_function_parameter_decorator_wrap = off
|
||||||
|
ij_typescript_if_brace_force = never
|
||||||
|
ij_typescript_import_merge_members = global
|
||||||
|
ij_typescript_import_prefer_absolute_path = global
|
||||||
|
ij_typescript_import_sort_members = true
|
||||||
|
ij_typescript_import_sort_module_name = false
|
||||||
|
ij_typescript_import_use_node_resolution = true
|
||||||
|
ij_typescript_imports_wrap = on_every_item
|
||||||
|
ij_typescript_indent_case_from_switch = true
|
||||||
|
ij_typescript_indent_chained_calls = true
|
||||||
|
ij_typescript_indent_package_children = 0
|
||||||
|
ij_typescript_jsdoc_include_types = false
|
||||||
|
ij_typescript_jsx_attribute_value = braces
|
||||||
|
ij_typescript_keep_blank_lines_in_code = 2
|
||||||
|
ij_typescript_keep_first_column_comment = true
|
||||||
|
ij_typescript_keep_indents_on_empty_lines = false
|
||||||
|
ij_typescript_keep_line_breaks = true
|
||||||
|
ij_typescript_keep_simple_blocks_in_one_line = false
|
||||||
|
ij_typescript_keep_simple_methods_in_one_line = false
|
||||||
|
ij_typescript_jsx_self_closing_tag = true
|
||||||
|
ij_typescript_line_comment_add_space = true
|
||||||
|
ij_typescript_line_comment_at_first_column = false
|
||||||
|
ij_typescript_method_brace_style = end_of_line
|
||||||
|
ij_typescript_method_call_chain_wrap = off
|
||||||
|
ij_typescript_method_parameters_new_line_after_left_paren = false
|
||||||
|
ij_typescript_method_parameters_right_paren_on_new_line = false
|
||||||
|
ij_typescript_method_parameters_wrap = off
|
||||||
|
ij_typescript_object_literal_wrap = on_every_item
|
||||||
|
ij_typescript_object_types_wrap = on_every_item
|
||||||
|
ij_typescript_parentheses_expression_new_line_after_left_paren = false
|
||||||
|
ij_typescript_parentheses_expression_right_paren_on_new_line = false
|
||||||
|
ij_typescript_place_assignment_sign_on_next_line = false
|
||||||
|
ij_typescript_prefer_as_type_cast = false
|
||||||
|
ij_typescript_prefer_explicit_types_function_expression_returns = false
|
||||||
|
ij_typescript_prefer_explicit_types_function_returns = false
|
||||||
|
ij_typescript_prefer_explicit_types_vars_fields = false
|
||||||
|
ij_typescript_prefer_parameters_wrap = false
|
||||||
|
ij_typescript_property_prefix =
|
||||||
|
ij_typescript_reformat_c_style_comments = false
|
||||||
|
ij_typescript_space_after_colon = true
|
||||||
|
ij_typescript_space_after_comma = true
|
||||||
|
ij_typescript_space_after_dots_in_rest_parameter = false
|
||||||
|
ij_typescript_space_after_generator_mult = true
|
||||||
|
ij_typescript_space_after_property_colon = true
|
||||||
|
ij_typescript_space_after_quest = true
|
||||||
|
ij_typescript_space_after_type_colon = true
|
||||||
|
ij_typescript_space_after_unary_not = false
|
||||||
|
ij_typescript_space_before_async_arrow_lparen = true
|
||||||
|
ij_typescript_space_before_catch_keyword = true
|
||||||
|
ij_typescript_space_before_catch_left_brace = true
|
||||||
|
ij_typescript_space_before_catch_parentheses = true
|
||||||
|
ij_typescript_space_before_class_lbrace = true
|
||||||
|
ij_typescript_space_before_class_left_brace = true
|
||||||
|
ij_typescript_space_before_colon = true
|
||||||
|
ij_typescript_space_before_comma = false
|
||||||
|
ij_typescript_space_before_do_left_brace = true
|
||||||
|
ij_typescript_space_before_else_keyword = true
|
||||||
|
ij_typescript_space_before_else_left_brace = true
|
||||||
|
ij_typescript_space_before_finally_keyword = true
|
||||||
|
ij_typescript_space_before_finally_left_brace = true
|
||||||
|
ij_typescript_space_before_for_left_brace = true
|
||||||
|
ij_typescript_space_before_for_parentheses = true
|
||||||
|
ij_typescript_space_before_for_semicolon = false
|
||||||
|
ij_typescript_space_before_function_left_parenth = false
|
||||||
|
ij_typescript_space_before_generator_mult = false
|
||||||
|
ij_typescript_space_before_if_left_brace = true
|
||||||
|
ij_typescript_space_before_if_parentheses = true
|
||||||
|
ij_typescript_space_before_method_call_parentheses = false
|
||||||
|
ij_typescript_space_before_method_left_brace = true
|
||||||
|
ij_typescript_space_before_method_parentheses = false
|
||||||
|
ij_typescript_space_before_property_colon = false
|
||||||
|
ij_typescript_space_before_quest = true
|
||||||
|
ij_typescript_space_before_switch_left_brace = true
|
||||||
|
ij_typescript_space_before_switch_parentheses = true
|
||||||
|
ij_typescript_space_before_try_left_brace = true
|
||||||
|
ij_typescript_space_before_type_colon = false
|
||||||
|
ij_typescript_space_before_unary_not = false
|
||||||
|
ij_typescript_space_before_while_keyword = true
|
||||||
|
ij_typescript_space_before_while_left_brace = true
|
||||||
|
ij_typescript_space_before_while_parentheses = true
|
||||||
|
ij_typescript_spaces_around_additive_operators = true
|
||||||
|
ij_typescript_spaces_around_arrow_function_operator = true
|
||||||
|
ij_typescript_spaces_around_assignment_operators = true
|
||||||
|
ij_typescript_spaces_around_bitwise_operators = true
|
||||||
|
ij_typescript_spaces_around_equality_operators = true
|
||||||
|
ij_typescript_spaces_around_logical_operators = true
|
||||||
|
ij_typescript_spaces_around_multiplicative_operators = true
|
||||||
|
ij_typescript_spaces_around_relational_operators = true
|
||||||
|
ij_typescript_spaces_around_shift_operators = true
|
||||||
|
ij_typescript_spaces_around_unary_operator = false
|
||||||
|
ij_typescript_spaces_within_array_initializer_brackets = true
|
||||||
|
ij_typescript_spaces_within_brackets = false
|
||||||
|
ij_typescript_spaces_within_catch_parentheses = false
|
||||||
|
ij_typescript_spaces_within_for_parentheses = false
|
||||||
|
ij_typescript_spaces_within_if_parentheses = false
|
||||||
|
ij_typescript_spaces_within_imports = true
|
||||||
|
ij_typescript_spaces_within_interpolation_expressions = false
|
||||||
|
ij_typescript_spaces_within_method_call_parentheses = false
|
||||||
|
ij_typescript_spaces_within_method_parentheses = false
|
||||||
|
ij_typescript_spaces_within_object_literal_braces = true
|
||||||
|
ij_typescript_spaces_within_object_type_braces = true
|
||||||
|
ij_typescript_spaces_within_parentheses = false
|
||||||
|
ij_typescript_spaces_within_switch_parentheses = false
|
||||||
|
ij_typescript_spaces_within_type_assertion = false
|
||||||
|
ij_typescript_spaces_within_union_types = true
|
||||||
|
ij_typescript_spaces_within_while_parentheses = false
|
||||||
|
ij_typescript_special_else_if_treatment = true
|
||||||
|
ij_typescript_ternary_operation_signs_on_next_line = false
|
||||||
|
ij_typescript_ternary_operation_wrap = off
|
||||||
|
ij_typescript_union_types_wrap = on_every_item
|
||||||
|
ij_typescript_use_chained_calls_group_indents = false
|
||||||
|
ij_typescript_use_double_quotes = false
|
||||||
|
ij_typescript_use_explicit_js_extension = never
|
||||||
|
ij_typescript_use_import_type = auto
|
||||||
|
ij_typescript_use_path_mapping = always
|
||||||
|
ij_typescript_use_public_modifier = false
|
||||||
|
ij_typescript_use_semicolon_after_statement = true
|
||||||
|
ij_typescript_var_declaration_wrap = normal
|
||||||
|
ij_typescript_while_brace_force = never
|
||||||
|
ij_typescript_while_on_new_line = false
|
||||||
|
ij_typescript_wrap_comments = false
|
||||||
|
|
||||||
|
[{*.bash,*.sh,*.zsh}]
|
||||||
|
indent_size = 2
|
||||||
|
tab_width = 2
|
||||||
|
ij_visual_guides =
|
||||||
|
ij_shell_binary_ops_start_line = false
|
||||||
|
ij_shell_keep_column_alignment_padding = false
|
||||||
|
ij_shell_minify_program = false
|
||||||
|
ij_shell_redirect_followed_by_space = false
|
||||||
|
ij_shell_switch_cases_indented = false
|
||||||
|
ij_shell_use_unix_line_separator = true
|
||||||
|
|
||||||
|
[{*.cjs,*.es6,*.js,*.mjs,*.jsx}]
|
||||||
|
indent_size = 2
|
||||||
|
tab_width = 2
|
||||||
|
ij_continuation_indent_size = 2
|
||||||
|
ij_visual_guides =
|
||||||
|
ij_javascript_align_imports = false
|
||||||
|
ij_javascript_align_multiline_array_initializer_expression = false
|
||||||
|
ij_javascript_align_multiline_binary_operation = false
|
||||||
|
ij_javascript_align_multiline_chained_methods = false
|
||||||
|
ij_javascript_align_multiline_extends_list = false
|
||||||
|
ij_javascript_align_multiline_for = true
|
||||||
|
ij_javascript_align_multiline_parameters = true
|
||||||
|
ij_javascript_align_multiline_parameters_in_calls = false
|
||||||
|
ij_javascript_align_multiline_ternary_operation = false
|
||||||
|
ij_javascript_align_object_properties = 0
|
||||||
|
ij_javascript_align_union_types = false
|
||||||
|
ij_javascript_align_var_statements = 0
|
||||||
|
ij_javascript_array_initializer_new_line_after_left_brace = false
|
||||||
|
ij_javascript_array_initializer_right_brace_on_new_line = false
|
||||||
|
ij_javascript_array_initializer_wrap = off
|
||||||
|
ij_javascript_assignment_wrap = off
|
||||||
|
ij_javascript_binary_operation_sign_on_next_line = false
|
||||||
|
ij_javascript_binary_operation_wrap = off
|
||||||
|
ij_javascript_blacklist_imports = rxjs/Rx,node_modules/**,**/node_modules/**,@angular/material,@angular/material/typings/**
|
||||||
|
ij_javascript_blank_lines_after_imports = 1
|
||||||
|
ij_javascript_blank_lines_around_class = 1
|
||||||
|
ij_javascript_blank_lines_around_field = 0
|
||||||
|
ij_javascript_blank_lines_around_function = 1
|
||||||
|
ij_javascript_blank_lines_around_method = 1
|
||||||
|
ij_javascript_block_brace_style = end_of_line
|
||||||
|
ij_javascript_block_comment_add_space = false
|
||||||
|
ij_javascript_block_comment_at_first_column = true
|
||||||
|
ij_javascript_call_parameters_new_line_after_left_paren = false
|
||||||
|
ij_javascript_call_parameters_right_paren_on_new_line = false
|
||||||
|
ij_javascript_call_parameters_wrap = off
|
||||||
|
ij_javascript_catch_on_new_line = false
|
||||||
|
ij_javascript_chained_call_dot_on_new_line = true
|
||||||
|
ij_javascript_class_brace_style = end_of_line
|
||||||
|
ij_javascript_class_decorator_wrap = split_into_lines
|
||||||
|
ij_javascript_class_field_decorator_wrap = off
|
||||||
|
ij_javascript_class_method_decorator_wrap = off
|
||||||
|
ij_javascript_comma_on_new_line = false
|
||||||
|
ij_javascript_do_while_brace_force = never
|
||||||
|
ij_javascript_else_on_new_line = false
|
||||||
|
ij_javascript_enforce_trailing_comma = whenmultiline
|
||||||
|
ij_javascript_extends_keyword_wrap = off
|
||||||
|
ij_javascript_extends_list_wrap = off
|
||||||
|
ij_javascript_field_prefix = _
|
||||||
|
ij_javascript_file_name_style = relaxed
|
||||||
|
ij_javascript_finally_on_new_line = false
|
||||||
|
ij_javascript_for_brace_force = never
|
||||||
|
ij_javascript_for_statement_new_line_after_left_paren = false
|
||||||
|
ij_javascript_for_statement_right_paren_on_new_line = false
|
||||||
|
ij_javascript_for_statement_wrap = off
|
||||||
|
ij_javascript_force_quote_style = true
|
||||||
|
ij_javascript_force_semicolon_style = true
|
||||||
|
ij_javascript_function_expression_brace_style = end_of_line
|
||||||
|
ij_javascript_function_parameter_decorator_wrap = off
|
||||||
|
ij_javascript_if_brace_force = never
|
||||||
|
ij_javascript_import_merge_members = global
|
||||||
|
ij_javascript_import_prefer_absolute_path = global
|
||||||
|
ij_javascript_import_sort_members = true
|
||||||
|
ij_javascript_import_sort_module_name = false
|
||||||
|
ij_javascript_import_use_node_resolution = true
|
||||||
|
ij_javascript_imports_wrap = on_every_item
|
||||||
|
ij_javascript_indent_case_from_switch = true
|
||||||
|
ij_javascript_indent_chained_calls = true
|
||||||
|
ij_javascript_indent_package_children = 0
|
||||||
|
ij_javascript_jsx_attribute_value = braces
|
||||||
|
ij_javascript_keep_blank_lines_in_code = 2
|
||||||
|
ij_javascript_keep_first_column_comment = true
|
||||||
|
ij_javascript_keep_indents_on_empty_lines = false
|
||||||
|
ij_javascript_keep_line_breaks = true
|
||||||
|
ij_javascript_keep_simple_blocks_in_one_line = false
|
||||||
|
ij_javascript_keep_simple_methods_in_one_line = false
|
||||||
|
ij_javascript_jsx_self_closing_tag = true
|
||||||
|
ij_javascript_line_comment_add_space = true
|
||||||
|
ij_javascript_line_comment_at_first_column = false
|
||||||
|
ij_javascript_method_brace_style = end_of_line
|
||||||
|
ij_javascript_method_call_chain_wrap = off
|
||||||
|
ij_javascript_method_parameters_new_line_after_left_paren = false
|
||||||
|
ij_javascript_method_parameters_right_paren_on_new_line = false
|
||||||
|
ij_javascript_method_parameters_wrap = off
|
||||||
|
ij_javascript_object_literal_wrap = on_every_item
|
||||||
|
ij_javascript_object_types_wrap = on_every_item
|
||||||
|
ij_javascript_parentheses_expression_new_line_after_left_paren = false
|
||||||
|
ij_javascript_parentheses_expression_right_paren_on_new_line = false
|
||||||
|
ij_javascript_place_assignment_sign_on_next_line = false
|
||||||
|
ij_javascript_prefer_as_type_cast = false
|
||||||
|
ij_javascript_prefer_explicit_types_function_expression_returns = false
|
||||||
|
ij_javascript_prefer_explicit_types_function_returns = false
|
||||||
|
ij_javascript_prefer_explicit_types_vars_fields = false
|
||||||
|
ij_javascript_prefer_parameters_wrap = false
|
||||||
|
ij_javascript_property_prefix =
|
||||||
|
ij_javascript_reformat_c_style_comments = true
|
||||||
|
ij_javascript_space_after_colon = true
|
||||||
|
ij_javascript_space_after_comma = true
|
||||||
|
ij_javascript_space_after_dots_in_rest_parameter = false
|
||||||
|
ij_javascript_space_after_generator_mult = true
|
||||||
|
ij_javascript_space_after_property_colon = true
|
||||||
|
ij_javascript_space_after_quest = true
|
||||||
|
ij_javascript_space_after_type_colon = true
|
||||||
|
ij_javascript_space_after_unary_not = false
|
||||||
|
ij_javascript_space_before_async_arrow_lparen = true
|
||||||
|
ij_javascript_space_before_catch_keyword = true
|
||||||
|
ij_javascript_space_before_catch_left_brace = true
|
||||||
|
ij_javascript_space_before_catch_parentheses = true
|
||||||
|
ij_javascript_space_before_class_lbrace = true
|
||||||
|
ij_javascript_space_before_class_left_brace = true
|
||||||
|
ij_javascript_space_before_colon = true
|
||||||
|
ij_javascript_space_before_comma = false
|
||||||
|
ij_javascript_space_before_do_left_brace = true
|
||||||
|
ij_javascript_space_before_else_keyword = true
|
||||||
|
ij_javascript_space_before_else_left_brace = true
|
||||||
|
ij_javascript_space_before_finally_keyword = true
|
||||||
|
ij_javascript_space_before_finally_left_brace = true
|
||||||
|
ij_javascript_space_before_for_left_brace = true
|
||||||
|
ij_javascript_space_before_for_parentheses = true
|
||||||
|
ij_javascript_space_before_for_semicolon = false
|
||||||
|
ij_javascript_space_before_function_left_parenth = false
|
||||||
|
ij_javascript_space_before_generator_mult = false
|
||||||
|
ij_javascript_space_before_if_left_brace = true
|
||||||
|
ij_javascript_space_before_if_parentheses = true
|
||||||
|
ij_javascript_space_before_method_call_parentheses = false
|
||||||
|
ij_javascript_space_before_method_left_brace = true
|
||||||
|
ij_javascript_space_before_method_parentheses = false
|
||||||
|
ij_javascript_space_before_property_colon = false
|
||||||
|
ij_javascript_space_before_quest = true
|
||||||
|
ij_javascript_space_before_switch_left_brace = true
|
||||||
|
ij_javascript_space_before_switch_parentheses = true
|
||||||
|
ij_javascript_space_before_try_left_brace = true
|
||||||
|
ij_javascript_space_before_type_colon = false
|
||||||
|
ij_javascript_space_before_unary_not = false
|
||||||
|
ij_javascript_space_before_while_keyword = true
|
||||||
|
ij_javascript_space_before_while_left_brace = true
|
||||||
|
ij_javascript_space_before_while_parentheses = true
|
||||||
|
ij_javascript_spaces_around_additive_operators = true
|
||||||
|
ij_javascript_spaces_around_arrow_function_operator = true
|
||||||
|
ij_javascript_spaces_around_assignment_operators = true
|
||||||
|
ij_javascript_spaces_around_bitwise_operators = true
|
||||||
|
ij_javascript_spaces_around_equality_operators = true
|
||||||
|
ij_javascript_spaces_around_logical_operators = true
|
||||||
|
ij_javascript_spaces_around_multiplicative_operators = true
|
||||||
|
ij_javascript_spaces_around_relational_operators = true
|
||||||
|
ij_javascript_spaces_around_shift_operators = true
|
||||||
|
ij_javascript_spaces_around_unary_operator = false
|
||||||
|
ij_javascript_spaces_within_array_initializer_brackets = false
|
||||||
|
ij_javascript_spaces_within_brackets = false
|
||||||
|
ij_javascript_spaces_within_catch_parentheses = false
|
||||||
|
ij_javascript_spaces_within_for_parentheses = false
|
||||||
|
ij_javascript_spaces_within_if_parentheses = false
|
||||||
|
ij_javascript_spaces_within_imports = true
|
||||||
|
ij_javascript_spaces_within_interpolation_expressions = false
|
||||||
|
ij_javascript_spaces_within_method_call_parentheses = false
|
||||||
|
ij_javascript_spaces_within_method_parentheses = false
|
||||||
|
ij_javascript_spaces_within_object_literal_braces = true
|
||||||
|
ij_javascript_spaces_within_object_type_braces = true
|
||||||
|
ij_javascript_spaces_within_parentheses = false
|
||||||
|
ij_javascript_spaces_within_switch_parentheses = false
|
||||||
|
ij_javascript_spaces_within_type_assertion = false
|
||||||
|
ij_javascript_spaces_within_union_types = true
|
||||||
|
ij_javascript_spaces_within_while_parentheses = false
|
||||||
|
ij_javascript_special_else_if_treatment = true
|
||||||
|
ij_javascript_ternary_operation_signs_on_next_line = false
|
||||||
|
ij_javascript_ternary_operation_wrap = off
|
||||||
|
ij_javascript_union_types_wrap = on_every_item
|
||||||
|
ij_javascript_use_chained_calls_group_indents = false
|
||||||
|
ij_javascript_use_double_quotes = false
|
||||||
|
ij_javascript_use_explicit_js_extension = never
|
||||||
|
ij_javascript_use_import_type = auto
|
||||||
|
ij_javascript_use_path_mapping = always
|
||||||
|
ij_javascript_use_public_modifier = false
|
||||||
|
ij_javascript_use_semicolon_after_statement = true
|
||||||
|
ij_javascript_var_declaration_wrap = normal
|
||||||
|
ij_javascript_while_brace_force = never
|
||||||
|
ij_javascript_while_on_new_line = false
|
||||||
|
ij_javascript_wrap_comments = true
|
||||||
|
|
||||||
|
[{*.ctp,*.hphp,*.inc,*.module,*.php,*.php4,*.php5,*.phtml}]
|
||||||
|
indent_size = 4
|
||||||
|
tab_width = 4
|
||||||
|
ij_continuation_indent_size = 4
|
||||||
|
ij_visual_guides =
|
||||||
|
ij_php_align_assignments = false
|
||||||
|
ij_php_align_class_constants = true
|
||||||
|
ij_php_align_enum_cases = false
|
||||||
|
ij_php_align_group_field_declarations = false
|
||||||
|
ij_php_align_inline_comments = false
|
||||||
|
ij_php_align_key_value_pairs = true
|
||||||
|
ij_php_align_match_arm_bodies = true
|
||||||
|
ij_php_align_multiline_array_initializer_expression = false
|
||||||
|
ij_php_align_multiline_binary_operation = false
|
||||||
|
ij_php_align_multiline_chained_methods = true
|
||||||
|
ij_php_align_multiline_extends_list = false
|
||||||
|
ij_php_align_multiline_for = true
|
||||||
|
ij_php_align_multiline_parameters = true
|
||||||
|
ij_php_align_multiline_parameters_in_calls = false
|
||||||
|
ij_php_align_multiline_ternary_operation = false
|
||||||
|
ij_php_align_named_arguments = false
|
||||||
|
ij_php_align_phpdoc_comments = true
|
||||||
|
ij_php_align_phpdoc_param_names = true
|
||||||
|
ij_php_anonymous_brace_style = end_of_line
|
||||||
|
ij_php_api_weight = 28
|
||||||
|
ij_php_array_initializer_new_line_after_left_brace = false
|
||||||
|
ij_php_array_initializer_right_brace_on_new_line = true
|
||||||
|
ij_php_array_initializer_wrap = off
|
||||||
|
ij_php_assignment_wrap = off
|
||||||
|
ij_php_attributes_wrap = off
|
||||||
|
ij_php_author_weight = 1
|
||||||
|
ij_php_binary_operation_sign_on_next_line = false
|
||||||
|
ij_php_binary_operation_wrap = off
|
||||||
|
ij_php_blank_lines_after_class_header = 0
|
||||||
|
ij_php_blank_lines_after_function = 1
|
||||||
|
ij_php_blank_lines_after_imports = 1
|
||||||
|
ij_php_blank_lines_after_opening_tag = 0
|
||||||
|
ij_php_blank_lines_after_package = 0
|
||||||
|
ij_php_blank_lines_around_class = 1
|
||||||
|
ij_php_blank_lines_around_constants = 0
|
||||||
|
ij_php_blank_lines_around_enum_cases = 0
|
||||||
|
ij_php_blank_lines_around_field = 1
|
||||||
|
ij_php_blank_lines_around_method = 1
|
||||||
|
ij_php_blank_lines_before_class_end = 0
|
||||||
|
ij_php_blank_lines_before_imports = 1
|
||||||
|
ij_php_blank_lines_before_method_body = 0
|
||||||
|
ij_php_blank_lines_before_package = 1
|
||||||
|
ij_php_blank_lines_before_return_statement = 0
|
||||||
|
ij_php_blank_lines_between_imports = 0
|
||||||
|
ij_php_block_brace_style = end_of_line
|
||||||
|
ij_php_call_parameters_new_line_after_left_paren = false
|
||||||
|
ij_php_call_parameters_right_paren_on_new_line = false
|
||||||
|
ij_php_call_parameters_wrap = off
|
||||||
|
ij_php_catch_on_new_line = false
|
||||||
|
ij_php_category_weight = 28
|
||||||
|
ij_php_class_brace_style = next_line
|
||||||
|
ij_php_comma_after_last_argument = false
|
||||||
|
ij_php_comma_after_last_argument_style = when_multiline
|
||||||
|
ij_php_comma_after_last_array_element = true
|
||||||
|
ij_php_comma_after_last_closure_use_var = false
|
||||||
|
ij_php_comma_after_last_closure_use_var_style = when_multiline
|
||||||
|
ij_php_comma_after_last_match_arm = false
|
||||||
|
ij_php_comma_after_last_parameter = false
|
||||||
|
ij_php_comma_after_last_parameter_style = when_multiline
|
||||||
|
ij_php_concat_spaces = true
|
||||||
|
ij_php_copyright_weight = 28
|
||||||
|
ij_php_deprecated_weight = 28
|
||||||
|
ij_php_do_while_brace_force = never
|
||||||
|
ij_php_else_if_style = as_is
|
||||||
|
ij_php_else_on_new_line = false
|
||||||
|
ij_php_example_weight = 28
|
||||||
|
ij_php_extends_keyword_wrap = off
|
||||||
|
ij_php_extends_list_wrap = off
|
||||||
|
ij_php_fields_default_visibility = private
|
||||||
|
ij_php_filesource_weight = 28
|
||||||
|
ij_php_finally_on_new_line = false
|
||||||
|
ij_php_for_brace_force = never
|
||||||
|
ij_php_for_statement_new_line_after_left_paren = false
|
||||||
|
ij_php_for_statement_right_paren_on_new_line = false
|
||||||
|
ij_php_for_statement_wrap = off
|
||||||
|
ij_php_force_empty_classes_in_one_line = false
|
||||||
|
ij_php_force_empty_methods_in_one_line = false
|
||||||
|
ij_php_force_short_declaration_array_style = false
|
||||||
|
ij_php_getters_setters_naming_style = camel_case
|
||||||
|
ij_php_getters_setters_order_style = getters_first
|
||||||
|
ij_php_global_weight = 28
|
||||||
|
ij_php_group_use_wrap = on_every_item
|
||||||
|
ij_php_heredoc_on_same_line = false
|
||||||
|
ij_php_if_brace_force = never
|
||||||
|
ij_php_if_lparen_on_next_line = false
|
||||||
|
ij_php_if_rparen_on_next_line = false
|
||||||
|
ij_php_ignore_weight = 28
|
||||||
|
ij_php_import_sorting = alphabetic
|
||||||
|
ij_php_indent_break_from_case = true
|
||||||
|
ij_php_indent_case_from_switch = true
|
||||||
|
ij_php_indent_code_in_php_tags = false
|
||||||
|
ij_php_internal_weight = 28
|
||||||
|
ij_php_keep_blank_lines_after_lbrace = 2
|
||||||
|
ij_php_keep_blank_lines_before_right_brace = 2
|
||||||
|
ij_php_keep_blank_lines_in_code = 2
|
||||||
|
ij_php_keep_blank_lines_in_declarations = 2
|
||||||
|
ij_php_keep_control_statement_in_one_line = true
|
||||||
|
ij_php_keep_first_column_comment = false
|
||||||
|
ij_php_keep_indents_on_empty_lines = false
|
||||||
|
ij_php_keep_line_breaks = true
|
||||||
|
ij_php_keep_rparen_and_lbrace_on_one_line = true
|
||||||
|
ij_php_keep_simple_classes_in_one_line = true
|
||||||
|
ij_php_keep_simple_methods_in_one_line = true
|
||||||
|
ij_php_lambda_brace_style = end_of_line
|
||||||
|
ij_php_license_weight = 28
|
||||||
|
ij_php_line_comment_add_space = false
|
||||||
|
ij_php_line_comment_at_first_column = true
|
||||||
|
ij_php_link_weight = 28
|
||||||
|
ij_php_lower_case_boolean_const = true
|
||||||
|
ij_php_lower_case_keywords = true
|
||||||
|
ij_php_lower_case_null_const = true
|
||||||
|
ij_php_method_brace_style = next_line
|
||||||
|
ij_php_method_call_chain_wrap = normal
|
||||||
|
ij_php_method_parameters_new_line_after_left_paren = true
|
||||||
|
ij_php_method_parameters_right_paren_on_new_line = true
|
||||||
|
ij_php_method_parameters_wrap = on_every_item
|
||||||
|
ij_php_method_weight = 5
|
||||||
|
ij_php_modifier_list_wrap = false
|
||||||
|
ij_php_multiline_chained_calls_first_call_on_new_line = false
|
||||||
|
ij_php_multiline_chained_calls_semicolon_on_new_line = false
|
||||||
|
ij_php_multiline_closure_lambda_on_new_line = false
|
||||||
|
ij_php_namespace_brace_style = 2
|
||||||
|
ij_php_new_line_after_php_opening_tag = false
|
||||||
|
ij_php_null_type_position = in_the_end
|
||||||
|
ij_php_package_weight = 0
|
||||||
|
ij_php_param_weight = 2
|
||||||
|
ij_php_parameters_attributes_wrap = off
|
||||||
|
ij_php_parentheses_expression_new_line_after_left_paren = false
|
||||||
|
ij_php_parentheses_expression_right_paren_on_new_line = false
|
||||||
|
ij_php_phpdoc_blank_line_before_tags = true
|
||||||
|
ij_php_phpdoc_blank_lines_around_parameters = true
|
||||||
|
ij_php_phpdoc_keep_blank_lines = true
|
||||||
|
ij_php_phpdoc_param_spaces_between_name_and_description = 1
|
||||||
|
ij_php_phpdoc_param_spaces_between_tag_and_type = 1
|
||||||
|
ij_php_phpdoc_param_spaces_between_type_and_name = 1
|
||||||
|
ij_php_phpdoc_use_fqcn = false
|
||||||
|
ij_php_phpdoc_wrap_long_lines = true
|
||||||
|
ij_php_place_assignment_sign_on_next_line = false
|
||||||
|
ij_php_place_parens_for_constructor = 0
|
||||||
|
ij_php_property_read_weight = 28
|
||||||
|
ij_php_property_weight = 28
|
||||||
|
ij_php_property_write_weight = 28
|
||||||
|
ij_php_return_type_on_new_line = false
|
||||||
|
ij_php_return_weight = 3
|
||||||
|
ij_php_see_weight = 28
|
||||||
|
ij_php_since_weight = 28
|
||||||
|
ij_php_sort_phpdoc_elements = true
|
||||||
|
ij_php_space_after_colon = true
|
||||||
|
ij_php_space_after_colon_in_enum_backed_type = true
|
||||||
|
ij_php_space_after_colon_in_named_argument = true
|
||||||
|
ij_php_space_after_colon_in_return_type = true
|
||||||
|
ij_php_space_after_comma = true
|
||||||
|
ij_php_space_after_for_semicolon = true
|
||||||
|
ij_php_space_after_quest = true
|
||||||
|
ij_php_space_after_type_cast = false
|
||||||
|
ij_php_space_after_unary_not = false
|
||||||
|
ij_php_space_before_array_initializer_left_brace = false
|
||||||
|
ij_php_space_before_catch_keyword = true
|
||||||
|
ij_php_space_before_catch_left_brace = true
|
||||||
|
ij_php_space_before_catch_parentheses = true
|
||||||
|
ij_php_space_before_class_left_brace = true
|
||||||
|
ij_php_space_before_closure_left_parenthesis = true
|
||||||
|
ij_php_space_before_colon = true
|
||||||
|
ij_php_space_before_colon_in_enum_backed_type = false
|
||||||
|
ij_php_space_before_colon_in_named_argument = false
|
||||||
|
ij_php_space_before_colon_in_return_type = false
|
||||||
|
ij_php_space_before_comma = false
|
||||||
|
ij_php_space_before_do_left_brace = true
|
||||||
|
ij_php_space_before_else_keyword = true
|
||||||
|
ij_php_space_before_else_left_brace = true
|
||||||
|
ij_php_space_before_finally_keyword = true
|
||||||
|
ij_php_space_before_finally_left_brace = true
|
||||||
|
ij_php_space_before_for_left_brace = true
|
||||||
|
ij_php_space_before_for_parentheses = true
|
||||||
|
ij_php_space_before_for_semicolon = false
|
||||||
|
ij_php_space_before_if_left_brace = true
|
||||||
|
ij_php_space_before_if_parentheses = true
|
||||||
|
ij_php_space_before_method_call_parentheses = false
|
||||||
|
ij_php_space_before_method_left_brace = true
|
||||||
|
ij_php_space_before_method_parentheses = false
|
||||||
|
ij_php_space_before_quest = true
|
||||||
|
ij_php_space_before_short_closure_left_parenthesis = false
|
||||||
|
ij_php_space_before_switch_left_brace = true
|
||||||
|
ij_php_space_before_switch_parentheses = true
|
||||||
|
ij_php_space_before_try_left_brace = true
|
||||||
|
ij_php_space_before_unary_not = false
|
||||||
|
ij_php_space_before_while_keyword = true
|
||||||
|
ij_php_space_before_while_left_brace = true
|
||||||
|
ij_php_space_before_while_parentheses = true
|
||||||
|
ij_php_space_between_ternary_quest_and_colon = false
|
||||||
|
ij_php_spaces_around_additive_operators = true
|
||||||
|
ij_php_spaces_around_arrow = false
|
||||||
|
ij_php_spaces_around_assignment_in_declare = false
|
||||||
|
ij_php_spaces_around_assignment_operators = true
|
||||||
|
ij_php_spaces_around_bitwise_operators = true
|
||||||
|
ij_php_spaces_around_equality_operators = true
|
||||||
|
ij_php_spaces_around_logical_operators = true
|
||||||
|
ij_php_spaces_around_multiplicative_operators = true
|
||||||
|
ij_php_spaces_around_null_coalesce_operator = true
|
||||||
|
ij_php_spaces_around_pipe_in_union_type = false
|
||||||
|
ij_php_spaces_around_relational_operators = true
|
||||||
|
ij_php_spaces_around_shift_operators = true
|
||||||
|
ij_php_spaces_around_unary_operator = false
|
||||||
|
ij_php_spaces_around_var_within_brackets = false
|
||||||
|
ij_php_spaces_within_array_initializer_braces = false
|
||||||
|
ij_php_spaces_within_brackets = false
|
||||||
|
ij_php_spaces_within_catch_parentheses = false
|
||||||
|
ij_php_spaces_within_for_parentheses = false
|
||||||
|
ij_php_spaces_within_if_parentheses = false
|
||||||
|
ij_php_spaces_within_method_call_parentheses = false
|
||||||
|
ij_php_spaces_within_method_parentheses = false
|
||||||
|
ij_php_spaces_within_parentheses = false
|
||||||
|
ij_php_spaces_within_short_echo_tags = true
|
||||||
|
ij_php_spaces_within_switch_parentheses = false
|
||||||
|
ij_php_spaces_within_while_parentheses = false
|
||||||
|
ij_php_special_else_if_treatment = false
|
||||||
|
ij_php_subpackage_weight = 28
|
||||||
|
ij_php_ternary_operation_signs_on_next_line = true
|
||||||
|
ij_php_ternary_operation_wrap = off
|
||||||
|
ij_php_throws_weight = 4
|
||||||
|
ij_php_todo_weight = 28
|
||||||
|
ij_php_treat_multiline_arrays_and_lambdas_multiline = false
|
||||||
|
ij_php_unknown_tag_weight = 28
|
||||||
|
ij_php_upper_case_boolean_const = false
|
||||||
|
ij_php_upper_case_null_const = false
|
||||||
|
ij_php_uses_weight = 28
|
||||||
|
ij_php_var_weight = 28
|
||||||
|
ij_php_variable_naming_style = mixed
|
||||||
|
ij_php_version_weight = 28
|
||||||
|
ij_php_while_brace_force = never
|
||||||
|
ij_php_while_on_new_line = false
|
||||||
|
|
||||||
|
[{*.har,*.jsb2,*.jsb3,*.json,*.jsonc,*.postman_collection,*.postman_collection.json,*.postman_environment,*.postman_environment.json,.babelrc,.eslintrc,.prettierrc,.stylelintrc,.ws-context,composer.lock,jest.config}]
|
||||||
|
indent_size = 2
|
||||||
|
ij_visual_guides =
|
||||||
|
ij_json_array_wrapping = split_into_lines
|
||||||
|
ij_json_keep_blank_lines_in_code = 0
|
||||||
|
ij_json_keep_indents_on_empty_lines = false
|
||||||
|
ij_json_keep_line_breaks = true
|
||||||
|
ij_json_keep_trailing_comma = false
|
||||||
|
ij_json_object_wrapping = split_into_lines
|
||||||
|
ij_json_property_alignment = do_not_align
|
||||||
|
ij_json_space_after_colon = true
|
||||||
|
ij_json_space_after_comma = true
|
||||||
|
ij_json_space_before_colon = false
|
||||||
|
ij_json_space_before_comma = false
|
||||||
|
ij_json_spaces_within_braces = false
|
||||||
|
ij_json_spaces_within_brackets = false
|
||||||
|
ij_json_wrap_long_lines = false
|
||||||
|
|
||||||
|
[{*.htm,*.html,*.sht,*.shtm,*.shtml}]
|
||||||
|
indent_size = 2
|
||||||
|
tab_width = 2
|
||||||
|
ij_continuation_indent_size = 2
|
||||||
|
ij_visual_guides =
|
||||||
|
ij_html_add_new_line_before_tags = body,div,p,form,h1,h2,h3
|
||||||
|
ij_html_align_attributes = true
|
||||||
|
ij_html_align_text = false
|
||||||
|
ij_html_attribute_wrap = on_every_item
|
||||||
|
ij_html_block_comment_add_space = false
|
||||||
|
ij_html_block_comment_at_first_column = true
|
||||||
|
ij_html_do_not_align_children_of_min_lines = 0
|
||||||
|
ij_html_do_not_break_if_inline_tags = title,h1,h2,h3,h4,h5,h6
|
||||||
|
ij_html_do_not_indent_children_of_tags = thead,tbody,tfoot
|
||||||
|
ij_html_enforce_quotes = true
|
||||||
|
ij_html_inline_tags = a,abbr,acronym,b,basefont,bdo,big,br,cite,cite,code,dfn,em,font,i,img,input,kbd,label,q,s,samp,select,small,strike,strong,sub,sup,textarea,tt,u,var
|
||||||
|
ij_html_keep_blank_lines = 2
|
||||||
|
ij_html_keep_indents_on_empty_lines = false
|
||||||
|
ij_html_keep_line_breaks = true
|
||||||
|
ij_html_keep_line_breaks_in_text = true
|
||||||
|
ij_html_keep_whitespaces = false
|
||||||
|
ij_html_keep_whitespaces_inside = span,pre,textarea
|
||||||
|
ij_html_line_comment_at_first_column = true
|
||||||
|
ij_html_new_line_after_last_attribute = when_multiline
|
||||||
|
ij_html_new_line_before_first_attribute = when_multiline
|
||||||
|
ij_html_quote_style = double
|
||||||
|
ij_html_remove_new_line_before_tags = br
|
||||||
|
ij_html_space_after_tag_name = false
|
||||||
|
ij_html_space_around_equality_in_attribute = false
|
||||||
|
ij_html_space_inside_empty_tag = true
|
||||||
|
ij_html_self_closing_tag = true
|
||||||
|
ij_html_text_wrap = normal
|
||||||
|
|
||||||
|
[{*.http,*.rest}]
|
||||||
|
ij_continuation_indent_size = 4
|
||||||
|
ij_visual_guides =
|
||||||
|
ij_http-request_call_parameters_wrap = normal
|
||||||
|
ij_http-request_method_parameters_wrap = split_into_lines
|
||||||
|
ij_http-request_space_before_comma = true
|
||||||
|
ij_http-request_spaces_around_assignment_operators = true
|
||||||
|
|
||||||
|
[{*.markdown,*.md}]
|
||||||
|
ij_visual_guides =
|
||||||
|
ij_markdown_force_one_space_after_blockquote_symbol = true
|
||||||
|
ij_markdown_force_one_space_after_header_symbol = true
|
||||||
|
ij_markdown_force_one_space_after_list_bullet = true
|
||||||
|
ij_markdown_force_one_space_between_words = true
|
||||||
|
ij_markdown_format_tables = true
|
||||||
|
ij_markdown_insert_quote_arrows_on_wrap = true
|
||||||
|
ij_markdown_keep_indents_on_empty_lines = false
|
||||||
|
ij_markdown_keep_line_breaks_inside_text_blocks = true
|
||||||
|
ij_markdown_max_lines_around_block_elements = 1
|
||||||
|
ij_markdown_max_lines_around_header = 1
|
||||||
|
ij_markdown_max_lines_between_paragraphs = 1
|
||||||
|
ij_markdown_min_lines_around_block_elements = 1
|
||||||
|
ij_markdown_min_lines_around_header = 1
|
||||||
|
ij_markdown_min_lines_between_paragraphs = 1
|
||||||
|
ij_markdown_wrap_text_if_long = true
|
||||||
|
ij_markdown_wrap_text_inside_blockquotes = true
|
||||||
|
|
||||||
|
[{*.mk,GNUmakefile,GNUmakefile.inc,makefile,makefile.inc}]
|
||||||
|
ij_visual_guides =
|
||||||
|
|
||||||
|
[{*.yaml,*.yml}]
|
||||||
|
indent_size = 2
|
||||||
|
ij_visual_guides =
|
||||||
|
ij_yaml_align_values_properties = do_not_align
|
||||||
|
ij_yaml_autoinsert_sequence_marker = true
|
||||||
|
ij_yaml_block_mapping_on_new_line = false
|
||||||
|
ij_yaml_indent_sequence_value = true
|
||||||
|
ij_yaml_keep_indents_on_empty_lines = false
|
||||||
|
ij_yaml_keep_line_breaks = true
|
||||||
|
ij_yaml_line_comment_add_space = false
|
||||||
|
ij_yaml_line_comment_add_space_on_reformat = false
|
||||||
|
ij_yaml_line_comment_at_first_column = true
|
||||||
|
ij_yaml_sequence_on_new_line = false
|
||||||
|
ij_yaml_space_before_colon = false
|
||||||
|
ij_yaml_spaces_within_braces = true
|
||||||
|
ij_yaml_spaces_within_brackets = true
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
# This file is a "template" of which env vars need to be defined for your application
|
||||||
|
# Copy this file to .env file for development, create environment variables when deploying to production
|
||||||
|
# https://symfony.com/doc/current/best_practices/configuration.html#infrastructure-related-configuration
|
||||||
|
|
||||||
|
###> symfony/framework-bundle ###
|
||||||
|
APP_ENV=dev
|
||||||
|
APP_SECRET=changethis
|
||||||
|
APP_NAME=mineseeker
|
||||||
|
# Public application release identifier, e.g. 2026.3.0. In Gitea production
|
||||||
|
# deployments this is supplied by the APP_VERSION repository variable.
|
||||||
|
APP_VERSION=1.0.0
|
||||||
|
# APP_PUBLIC_HOSTNAME: The public hostname for your application (used for generating absolute URLs in emails)
|
||||||
|
# For production, set this to your domain (e.g., mineseeker.com)
|
||||||
|
APP_PUBLIC_HOSTNAME=localhost
|
||||||
|
# TRUSTED_PROXIES: Only needed for bare-metal dev behind a reverse proxy
|
||||||
|
# For Docker development, this is set in compose.override.yaml
|
||||||
|
# For production, set in PROD_ENV_FILE Gitea secret (use 172.18.0.0/16 initially)
|
||||||
|
#TRUSTED_PROXIES=127.0.0.1,127.0.0.2
|
||||||
|
#TRUSTED_HOSTS=localhost,example.com
|
||||||
|
###< symfony/framework-bundle ###
|
||||||
|
|
||||||
|
###> doctrine/doctrine-bundle ###
|
||||||
|
# Docker PostgreSQL is exposed on port 5452 — use that for bare-metal dev.
|
||||||
|
# Replace POSTGRES_USER / POSTGRES_PASSWORD / POSTGRES_DB with the values from your .env.
|
||||||
|
DATABASE_URL=postgresql://POSTGRES_USER:POSTGRES_PASSWORD@127.0.0.1:5452/POSTGRES_DB?serverVersion=18&charset=utf8
|
||||||
|
###< doctrine/doctrine-bundle ###
|
||||||
|
|
||||||
|
###> trycap.dev/cap ###
|
||||||
|
# CAP_API_ENDPOINT: Full URL including site-key path, e.g. https://cap.example.com/my-site-key/
|
||||||
|
# After deploying the Cap service, create a site in the dashboard and paste the endpoint here.
|
||||||
|
CAP_API_ENDPOINT=http://localhost:3000/changethis/
|
||||||
|
CAP_SECRET_KEY=changethis
|
||||||
|
###< trycap.dev/cap ###
|
||||||
|
|
||||||
|
###> minio/minio ###
|
||||||
|
MINIO_ROOT_USER=changethis
|
||||||
|
MINIO_ROOT_PASSWORD=changethis
|
||||||
|
MINIO_ENDPOINT=http://localhost:9010
|
||||||
|
MINIO_PUBLIC_URL=https://your-minio-subdomain.example.com
|
||||||
|
###< minio/minio ###
|
||||||
|
|
||||||
|
###> symfony/mailer ###
|
||||||
|
MAILER_DSN=smtp://localhost:1025
|
||||||
|
MAIL_DOMAIN=localhost
|
||||||
|
###< symfony/mailer ###
|
||||||
|
|
||||||
|
###> symfony/mercure-bundle ###
|
||||||
|
# See https://symfony.com/doc/current/mercure.html#configuration
|
||||||
|
# The URL of the Mercure hub, used by the app to publish updates (can be a local URL)
|
||||||
|
MERCURE_URL=https://mine.home.arpa/.well-known/mercure
|
||||||
|
# The public URL of the Mercure hub, used by the browser to connect
|
||||||
|
MERCURE_PUBLIC_URL=https://mine.home.arpa/.well-known/mercure
|
||||||
|
# The secret used to sign the JWTs
|
||||||
|
MERCURE_JWT_SECRET="!ChangeThisMercureHubJWTSecretKey!"
|
||||||
|
# Publisher JWT (must match publisher_jwt in your Caddyfile)
|
||||||
|
MERCURE_JWT_TOKEN=changethis
|
||||||
|
# Subscriber JWT sent to the browser so it can authenticate its EventSource connection
|
||||||
|
MERCURE_SUBSCRIBER_JWT=changethis
|
||||||
|
###< symfony/mercure-bundle ###
|
||||||
|
|
||||||
|
###> web-auth/webauthn-framework ###
|
||||||
|
WEBAUTHN_RP_ID=mine.home.arpa
|
||||||
|
WEBAUTHN_ORIGIN=https://mine.home.arpa
|
||||||
|
###< web-auth/webauthn-framework ###
|
||||||
|
|
||||||
|
###> symfony/messenger ###
|
||||||
|
# Choose one of the transports below
|
||||||
|
# MESSENGER_TRANSPORT_DSN=amqp://guest:guest@localhost:5672/%2f/messages
|
||||||
|
# MESSENGER_TRANSPORT_DSN=redis://localhost:6379/messages
|
||||||
|
MESSENGER_TRANSPORT_DSN=doctrine://default?auto_setup=0
|
||||||
|
###< symfony/messenger ###
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
# define your env variables for the test env here
|
||||||
|
KERNEL_CLASS='App\Kernel'
|
||||||
|
APP_SECRET='$ecretf0rt3st'
|
||||||
|
DATABASE_URL="postgresql://system7:bazmeg@127.0.0.1:5452/mineseeker_test?serverVersion=18&charset=utf8"
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
ignore_regexps = [
|
||||||
|
r'@minor', r'!minor',
|
||||||
|
r'@cosmetic', r'!cosmetic',
|
||||||
|
r'@refactor', r'!refactor',
|
||||||
|
r'@wip', r'!wip',
|
||||||
|
r'^(.{3,3}\s*:)?\s*[fF]irst commit.?\s*$',
|
||||||
|
r'^$', ## ignore commits with empty messages
|
||||||
|
r'@skipChangelog', r'!skipChangelog', r'skipChangeLog', r'!skipChangeLog',
|
||||||
|
r'Merge branch', r'Merge remote-tracking branch', r'!deploy',
|
||||||
|
]
|
||||||
|
section_regexps = [
|
||||||
|
('New', [
|
||||||
|
r'^[nN]ew\s*:\s*((dev|use?r|pkg|test|doc)\s*:\s*)?([^\n]*)$',
|
||||||
|
]),
|
||||||
|
('Changes', [
|
||||||
|
r'^[cC]hg\s*:\s*((dev|use?r|pkg|test|doc)\s*:\s*)?([^\n]*)$',
|
||||||
|
]),
|
||||||
|
('Fix', [
|
||||||
|
r'^[fF]ix\s*:\s*((dev|use?r|pkg|test|doc)\s*:\s*)?([^\n]*)$',
|
||||||
|
]),
|
||||||
|
('Other', None ## Match all lines
|
||||||
|
),
|
||||||
|
]
|
||||||
|
body_process = ReSub(r'((^|\n)[A-Z]\w+(-\w+)*: .*(\n\s+.*)*)+$', r'') | strip
|
||||||
|
subject_process = (strip |
|
||||||
|
ReSub(r'^([cC]hg|[fF]ix|[nN]ew)\s*:\s*((dev|use?r|pkg|test|doc)\s*:\s*)?([^\n@]*)(@[a-z]+\s+)*$', r'\4') |
|
||||||
|
SetIfEmpty("No commit message.") | ucfirst | final_dot)
|
||||||
|
tag_filter_regexp = r'^(v)?[0-9]+\.[0-9]+(\.[0-9]+)?(\-[0-9]+)?$'
|
||||||
|
unreleased_version_label = "(unreleased)"
|
||||||
|
output_engine = mustache("markdown")
|
||||||
|
include_merge = True
|
||||||
|
revs = []
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
name: CI - Tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
- develop
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
- develop
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
tests:
|
||||||
|
runs-on: splendid-bear
|
||||||
|
|
||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres:18-alpine
|
||||||
|
env:
|
||||||
|
POSTGRES_USER: mineseeker_test
|
||||||
|
POSTGRES_PASSWORD: test_password
|
||||||
|
POSTGRES_DB: mineseeker_test
|
||||||
|
options: >-
|
||||||
|
--health-cmd pg_isready
|
||||||
|
--health-interval 10s
|
||||||
|
--health-timeout 5s
|
||||||
|
--health-retries 5
|
||||||
|
ports:
|
||||||
|
- 5432:5432
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup PHP 8.3
|
||||||
|
uses: shivammathur/setup-php@v2
|
||||||
|
with:
|
||||||
|
php-version: '8.3'
|
||||||
|
extensions: pdo_pgsql, gd, intl, zip, sodium
|
||||||
|
coverage: none
|
||||||
|
tools: composer:v2
|
||||||
|
|
||||||
|
- name: Validate composer.json
|
||||||
|
run: composer validate --strict
|
||||||
|
|
||||||
|
- name: Cache Composer dependencies
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: vendor
|
||||||
|
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-composer-
|
||||||
|
|
||||||
|
- name: Install PHP dependencies
|
||||||
|
run: composer install --prefer-dist --no-progress --no-interaction
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '20'
|
||||||
|
|
||||||
|
- name: Cache node modules
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: node_modules
|
||||||
|
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-node-
|
||||||
|
|
||||||
|
- name: Install Node dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Build assets
|
||||||
|
run: npm run build
|
||||||
|
|
||||||
|
- name: Create .env.test file
|
||||||
|
run: |
|
||||||
|
cat > .env.test << 'ENVEOF'
|
||||||
|
APP_ENV=test
|
||||||
|
APP_SECRET=test-secret-key-for-ci-testing-only
|
||||||
|
DATABASE_URL="postgresql://mineseeker_test:test_password@localhost:5432/mineseeker_test?serverVersion=18&charset=utf8"
|
||||||
|
KERNEL_CLASS='App\Kernel'
|
||||||
|
SYMFONY_DEPRECATIONS_HELPER=999999
|
||||||
|
PANTHER_APP_ENV=panther
|
||||||
|
PANTHER_ERROR_SCREENSHOT_DIR=./var/error-screenshots
|
||||||
|
ENVEOF
|
||||||
|
|
||||||
|
- name: Setup test database
|
||||||
|
run: make test-db-setup
|
||||||
|
|
||||||
|
- name: Run PHPUnit tests
|
||||||
|
run: vendor/bin/phpunit --testdox --colors=always
|
||||||
|
|
||||||
|
- name: Run PHPUnit tests with coverage (optional)
|
||||||
|
if: github.event_name == 'pull_request'
|
||||||
|
run: |
|
||||||
|
XDEBUG_MODE=coverage vendor/bin/phpunit --coverage-text --coverage-clover=coverage.xml
|
||||||
|
|
||||||
|
- name: Upload coverage reports (optional)
|
||||||
|
if: github.event_name == 'pull_request'
|
||||||
|
uses: codecov/codecov-action@v4
|
||||||
|
with:
|
||||||
|
file: ./coverage.xml
|
||||||
|
flags: unittests
|
||||||
|
name: phpunit-coverage
|
||||||
|
fail_ci_if_error: false
|
||||||
|
|
||||||
|
lint:
|
||||||
|
runs-on: splendid-bear
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup PHP 8.3
|
||||||
|
uses: shivammathur/setup-php@v2
|
||||||
|
with:
|
||||||
|
php-version: '8.3'
|
||||||
|
tools: composer:v2, phpstan
|
||||||
|
|
||||||
|
- name: Install PHP dependencies
|
||||||
|
run: composer install --prefer-dist --no-progress --no-interaction
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '20'
|
||||||
|
|
||||||
|
- name: Install Node dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Run ESLint
|
||||||
|
run: npm run lint || true
|
||||||
|
|
||||||
|
- name: Check code style (PHP)
|
||||||
|
run: |
|
||||||
|
if [ -f "vendor/bin/php-cs-fixer" ]; then
|
||||||
|
vendor/bin/php-cs-fixer fix --dry-run --diff
|
||||||
|
else
|
||||||
|
echo "PHP-CS-Fixer not installed, skipping..."
|
||||||
|
fi
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
name: Deploy to Production
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on: splendid-bear
|
||||||
|
steps:
|
||||||
|
- name: Checkout tag
|
||||||
|
env:
|
||||||
|
GITEA_TOKEN: ${{ gitea.token }}
|
||||||
|
run: |
|
||||||
|
set -e
|
||||||
|
export HOME=/tmp
|
||||||
|
git config --global credential.helper '!f() { echo "username=oauth2"; echo "password=$GITEA_TOKEN"; }; f'
|
||||||
|
git config --global --add safe.directory "${{ vars.PROD_APP_DIR }}"
|
||||||
|
cd "${{ vars.PROD_APP_DIR }}"
|
||||||
|
git remote set-url origin "${{ gitea.server_url }}/${{ gitea.repository }}.git"
|
||||||
|
git fetch --tags --force
|
||||||
|
git checkout "${{ gitea.ref_name }}"
|
||||||
|
|
||||||
|
- name: Write .env
|
||||||
|
env:
|
||||||
|
PROD_ENV_FILE: ${{ secrets.PROD_ENV_FILE }}
|
||||||
|
run: |
|
||||||
|
printf '%s' "$PROD_ENV_FILE" > "${{ vars.PROD_APP_DIR }}/.env"
|
||||||
|
|
||||||
|
- name: Build image
|
||||||
|
run: |
|
||||||
|
cd "${{ vars.PROD_APP_DIR }}"
|
||||||
|
docker compose build
|
||||||
|
|
||||||
|
- name: Migrate legacy MinIO and deploy
|
||||||
|
env:
|
||||||
|
APP_VERSION: ${{ vars.APP_VERSION }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
cd "${{ vars.PROD_APP_DIR }}"
|
||||||
|
|
||||||
|
MIGRATION_COMPOSE="docker compose --profile storage-migration"
|
||||||
|
LEGACY_MINIO_CONTAINER=$($MIGRATION_COMPOSE ps -a -q minio)
|
||||||
|
|
||||||
|
if [ -n "$LEGACY_MINIO_CONTAINER" ]; then
|
||||||
|
echo "Legacy MinIO found; pausing writers and migrating its buckets to RustFS."
|
||||||
|
APP_WAS_RUNNING=$(docker compose ps --status running -q app)
|
||||||
|
SCHEDULER_WAS_RUNNING=$(docker compose ps --status running -q scheduler)
|
||||||
|
MIGRATION_SUCCEEDED=0
|
||||||
|
|
||||||
|
restart_old_workers_on_failure() {
|
||||||
|
result=$?
|
||||||
|
if [ "$result" -ne 0 ] && [ "$MIGRATION_SUCCEEDED" -eq 0 ]; then
|
||||||
|
# Before a complete copy, restore the old public MinIO endpoint
|
||||||
|
# before bringing back the old app.
|
||||||
|
docker compose stop rustfs || true
|
||||||
|
MINIO_PUBLISHED_API_PORT=9010 MINIO_PUBLISHED_CONSOLE_PORT=9011 $MIGRATION_COMPOSE up -d minio || true
|
||||||
|
[ -z "$APP_WAS_RUNNING" ] || docker compose start app || true
|
||||||
|
[ -z "$SCHEDULER_WAS_RUNNING" ] || docker compose start scheduler || true
|
||||||
|
elif [ "$result" -ne 0 ]; then
|
||||||
|
echo "Data migration succeeded; leaving RustFS in place for recovery from the app rollout failure." >&2
|
||||||
|
fi
|
||||||
|
return "$result"
|
||||||
|
}
|
||||||
|
trap restart_old_workers_on_failure EXIT
|
||||||
|
|
||||||
|
docker compose stop app scheduler || true
|
||||||
|
|
||||||
|
docker compose up -d db mail
|
||||||
|
# Move the legacy service off the RustFS public port before starting RustFS.
|
||||||
|
$MIGRATION_COMPOSE up -d minio
|
||||||
|
docker compose up -d rustfs
|
||||||
|
$MIGRATION_COMPOSE up --build --abort-on-container-exit --exit-code-from storage_migrate storage_migrate
|
||||||
|
MIGRATION_SUCCEEDED=1
|
||||||
|
|
||||||
|
echo "Migration succeeded; deploying the RustFS-only stack."
|
||||||
|
docker compose up -d --wait --wait-timeout 180 app scheduler
|
||||||
|
|
||||||
|
# Remove only the legacy service containers. Do not use -v: the old
|
||||||
|
# MinIO volume is retained as a rollback/backup until manually retired.
|
||||||
|
$MIGRATION_COMPOSE rm -sf minio storage_migrate
|
||||||
|
trap - EXIT
|
||||||
|
else
|
||||||
|
echo "No legacy MinIO container found; deploying the RustFS-only stack."
|
||||||
|
docker compose up -d
|
||||||
|
fi
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
name: Deploy to Production
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: splendid-bear
|
||||||
|
|
||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres:18-alpine
|
||||||
|
env:
|
||||||
|
POSTGRES_USER: mineseeker_test
|
||||||
|
POSTGRES_PASSWORD: test_password
|
||||||
|
POSTGRES_DB: mineseeker_test
|
||||||
|
options: >-
|
||||||
|
--health-cmd pg_isready
|
||||||
|
--health-interval 10s
|
||||||
|
--health-timeout 5s
|
||||||
|
--health-retries 5
|
||||||
|
ports:
|
||||||
|
- 5432:5432
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup PHP 8.3
|
||||||
|
uses: shivammathur/setup-php@v2
|
||||||
|
with:
|
||||||
|
php-version: '8.3'
|
||||||
|
extensions: pdo_pgsql, gd, intl, zip, sodium
|
||||||
|
coverage: none
|
||||||
|
tools: composer:v2
|
||||||
|
|
||||||
|
- name: Install PHP dependencies
|
||||||
|
run: composer install --prefer-dist --no-progress --no-interaction
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '20'
|
||||||
|
|
||||||
|
- name: Install Node dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Build assets
|
||||||
|
run: npm run build
|
||||||
|
|
||||||
|
- name: Create .env.test file
|
||||||
|
run: |
|
||||||
|
cat > .env.test << 'ENVEOF'
|
||||||
|
APP_ENV=test
|
||||||
|
APP_SECRET=test-secret-key-for-ci-testing-only
|
||||||
|
DATABASE_URL="postgresql://mineseeker_test:test_password@localhost:5432/mineseeker_test?serverVersion=18&charset=utf8"
|
||||||
|
KERNEL_CLASS='App\Kernel'
|
||||||
|
SYMFONY_DEPRECATIONS_HELPER=999999
|
||||||
|
ENVEOF
|
||||||
|
|
||||||
|
- name: Setup test database
|
||||||
|
run: make test-db-setup
|
||||||
|
|
||||||
|
- name: Run PHPUnit tests
|
||||||
|
run: vendor/bin/phpunit --testdox --colors=always --stop-on-failure
|
||||||
|
|
||||||
|
deploy:
|
||||||
|
needs: test
|
||||||
|
runs-on: splendid-bear
|
||||||
|
steps:
|
||||||
|
- name: Checkout tag
|
||||||
|
env:
|
||||||
|
GITEA_TOKEN: ${{ gitea.token }}
|
||||||
|
run: |
|
||||||
|
set -e
|
||||||
|
export HOME=/tmp
|
||||||
|
git config --global credential.helper '!f() { echo "username=oauth2"; echo "password=$GITEA_TOKEN"; }; f'
|
||||||
|
git config --global --add safe.directory "${{ vars.PROD_APP_DIR }}"
|
||||||
|
cd "${{ vars.PROD_APP_DIR }}"
|
||||||
|
git remote set-url origin "${{ gitea.server_url }}/${{ gitea.repository }}.git"
|
||||||
|
git fetch --tags --force
|
||||||
|
git checkout "${{ gitea.ref_name }}"
|
||||||
|
|
||||||
|
- name: Write .env
|
||||||
|
env:
|
||||||
|
PROD_ENV_FILE: ${{ secrets.PROD_ENV_FILE }}
|
||||||
|
run: |
|
||||||
|
printf '%s' "$PROD_ENV_FILE" > "${{ vars.PROD_APP_DIR }}/.env"
|
||||||
|
|
||||||
|
- name: Build image
|
||||||
|
run: |
|
||||||
|
cd "${{ vars.PROD_APP_DIR }}"
|
||||||
|
docker compose build
|
||||||
|
|
||||||
|
- name: Run database migrations
|
||||||
|
run: |
|
||||||
|
cd "${{ vars.PROD_APP_DIR }}"
|
||||||
|
docker compose run --rm app php bin/console doctrine:migrations:migrate --no-interaction
|
||||||
|
|
||||||
|
- name: Clear cache
|
||||||
|
run: |
|
||||||
|
cd "${{ vars.PROD_APP_DIR }}"
|
||||||
|
docker compose run --rm app php bin/console cache:clear --env=prod
|
||||||
|
|
||||||
|
- name: Start services
|
||||||
|
run: |
|
||||||
|
cd "${{ vars.PROD_APP_DIR }}"
|
||||||
|
docker compose up -d
|
||||||
|
|
||||||
|
- name: Health check
|
||||||
|
run: |
|
||||||
|
sleep 5
|
||||||
|
curl -f http://localhost:10080/ || exit 1
|
||||||
|
|
||||||
|
- name: Notify deployment success
|
||||||
|
if: success()
|
||||||
|
run: |
|
||||||
|
echo "✅ Deployment successful for tag ${{ gitea.ref_name }}"
|
||||||
|
|
||||||
|
- name: Notify deployment failure
|
||||||
|
if: failure()
|
||||||
|
run: |
|
||||||
|
echo "❌ Deployment failed for tag ${{ gitea.ref_name }}"
|
||||||
|
exit 1
|
||||||
+21
-40
@@ -1,41 +1,22 @@
|
|||||||
/app/config/parameters.yml
|
###> system7 - jotunheimr ###
|
||||||
/build/
|
compose.override.yaml
|
||||||
/phpunit.xml
|
+bak/
|
||||||
/var/*
|
/backups/
|
||||||
!/var/cache
|
.idea/
|
||||||
/var/cache/*
|
node_modules/
|
||||||
!var/cache/.gitkeep
|
|
||||||
!/var/logs
|
|
||||||
/var/logs/*
|
|
||||||
!var/logs/.gitkeep
|
|
||||||
!/var/sessions
|
|
||||||
/var/sessions/*
|
|
||||||
!var/sessions/.gitkeep
|
|
||||||
!var/SymfonyRequirements.php
|
|
||||||
/vendor/
|
|
||||||
/web/bundles/
|
|
||||||
|
|
||||||
# s7 mods
|
|
||||||
/!/
|
|
||||||
/.idea/
|
|
||||||
/.idea/*
|
|
||||||
/.idea/workspace.xml
|
|
||||||
/.idea/dataSources.ids
|
|
||||||
/.idea/dataSources.xml
|
|
||||||
/.idea/dataSources.local.xml
|
|
||||||
|
|
||||||
web/css/*
|
|
||||||
web/js/*
|
|
||||||
web/uploads/*
|
|
||||||
|
|
||||||
phpunit.phar
|
|
||||||
phpunit-report/*
|
|
||||||
/node_modules/
|
|
||||||
/src/Mine/SeekerBundle/Resources/public/js/node/
|
|
||||||
/src/Mine/SeekerBundle/Resources/public/js/src/
|
|
||||||
|
|
||||||
nohup.out
|
nohup.out
|
||||||
src/Mine/SeekerBundle/Resources/public/js/index.js
|
/public/build/
|
||||||
src/Mine/SeekerBundle/Resources/public/js/index.min.js
|
/config/reference.php
|
||||||
/src/Mine/SeekerBundle/Resources/public/js/index.js.map
|
###< system7 - jotunheimr ###
|
||||||
npm-debug.log
|
|
||||||
|
###> symfony/framework-bundle ###
|
||||||
|
/.env
|
||||||
|
/public/bundles/
|
||||||
|
/var/
|
||||||
|
/vendor/
|
||||||
|
###< symfony/framework-bundle ###
|
||||||
|
|
||||||
|
###> phpunit/phpunit ###
|
||||||
|
/phpunit.xml
|
||||||
|
/.phpunit.cache/
|
||||||
|
###< phpunit/phpunit ###
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"$schema": "/phpactor.schema.json",
|
||||||
|
"symfony.enabled": true,
|
||||||
|
"indexer.exclude_patterns": [
|
||||||
|
"/vendor/**/Tests/**/*",
|
||||||
|
"/vendor/**/tests/**/*",
|
||||||
|
"/var/cache/**/*",
|
||||||
|
"/vendor/composer/**/*"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,463 @@
|
|||||||
|
# AI Agent Guidelines for MineSeeker
|
||||||
|
|
||||||
|
This document provides guidelines and context for AI coding agents working on the MineSeeker project.
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
**MineSeeker** is a real-time multiplayer 1v1 minesweeper game built with Symfony (PHP) and React. Players compete to claim mines on a shared 16×16 grid, with the first to reach 26 mines winning.
|
||||||
|
|
||||||
|
### Tech Stack
|
||||||
|
|
||||||
|
- **Backend:** Symfony 7.2 (PHP 8.3)
|
||||||
|
- **Frontend:** React 18, Vite 6
|
||||||
|
- **Database:** PostgreSQL 17 with materialized views
|
||||||
|
- **Storage:** MinIO (S3-compatible)
|
||||||
|
- **Real-time:** Mercure (Server-Sent Events)
|
||||||
|
- **Styling:** SCSS, MUI (Material-UI), Emotion
|
||||||
|
- **Fonts:** @fontsource packages (web), Carlito-Bold.ttf (server-side images)
|
||||||
|
|
||||||
|
### Key Features
|
||||||
|
|
||||||
|
**Core Gameplay:**
|
||||||
|
- **Multiplayer-focused:** 1v1 competitive gameplay where players race to claim more mines than their opponent
|
||||||
|
- **Win condition:** First player to claim 26 out of 51 mines wins
|
||||||
|
- **Real-time updates:** WebSocket-like gameplay using Mercure (Server-Sent Events)
|
||||||
|
- **Game restoration:** Players can resume unfinished games from where they left off
|
||||||
|
- **Bonus points system:** Rewards skilled play (blind hits, chain combos, edge mines, endgame mines, safe cell reveals)
|
||||||
|
|
||||||
|
**User Features:**
|
||||||
|
- **Authentication:** Password + optional TOTP + optional WebAuthn passkeys
|
||||||
|
- **Anonymous play:** Guest players can play without creating an account
|
||||||
|
- **Profile statistics:** Detailed stats including wins, losses, draws, win rate, average score, total mines hit, and bonus points
|
||||||
|
- **Battle history:** View and replay past games move-by-move
|
||||||
|
|
||||||
|
**Sharing & Social:**
|
||||||
|
- **Battle reports:** Shareable public pages for each completed game (`/battle/{uuid}`)
|
||||||
|
- **OG image generation:** Automatic creation of 1200×630 PNG images for social media sharing (using PHP GD)
|
||||||
|
- **Open Graph tags:** Battle share pages include rich preview cards with player names, avatars, scores, and bonus points
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Architecture Overview
|
||||||
|
|
||||||
|
### Backend (Symfony)
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── Controller/ # HTTP endpoints (game, profile, battle sharing)
|
||||||
|
├── Entity/ # Doctrine ORM entities
|
||||||
|
├── Repository/ # Database queries (uses QueryBuilder, not raw SQL)
|
||||||
|
├── Service/ # Business logic (BattleCardGenerator, WebAuthn, Email)
|
||||||
|
├── Dto/ # Data Transfer Objects (immutable, readonly)
|
||||||
|
├── Util/ # Game logic (TopicManager for Mercure)
|
||||||
|
└── Migrations/ # Database schema changes
|
||||||
|
```
|
||||||
|
|
||||||
|
**Important patterns:**
|
||||||
|
- Use Doctrine ORM QueryBuilder (not raw SQL) in repositories
|
||||||
|
- DTOs are `final readonly` classes with constructor property promotion
|
||||||
|
- Services use dependency injection via `config/services.yaml`
|
||||||
|
- Materialized views for performance (auto-refreshed via triggers)
|
||||||
|
|
||||||
|
### Frontend (React)
|
||||||
|
|
||||||
|
```
|
||||||
|
assets/
|
||||||
|
├── js/
|
||||||
|
│ ├── mine-seeker/ # Main game bundle (self-contained)
|
||||||
|
│ │ ├── MineSeeker.jsx # Root component, wraps GameProvider + QueryClientProvider
|
||||||
|
│ │ ├── components/ # Game-specific components
|
||||||
|
│ │ │ ├── GameBoard.jsx # Main game board grid
|
||||||
|
│ │ │ ├── GameTimer.jsx # Game timer display
|
||||||
|
│ │ │ ├── BonusBox.jsx # Bonus points indicator
|
||||||
|
│ │ │ ├── BonusStatsDialog.jsx # Bonus statistics modal
|
||||||
|
│ │ │ ├── CaptchaOverlay.jsx # Captcha challenge overlay
|
||||||
|
│ │ │ ├── ChallengeCountdown.jsx # Challenge timer
|
||||||
|
│ │ │ ├── OnlinePlayersDialog.jsx # Online players list
|
||||||
|
│ │ │ ├── WaitingOverlayContent.jsx # Waiting for opponent
|
||||||
|
│ │ │ ├── grid/ # Grid-related components (cells, mines)
|
||||||
|
│ │ │ ├── profile/ # In-game profile components (PlayerColumn)
|
||||||
|
│ │ │ ├── timer/ # Timer-related components
|
||||||
|
│ │ │ └── user/ # User-related components
|
||||||
|
│ │ ├── contexts/ # React Context API
|
||||||
|
│ │ │ ├── GameContext.jsx # Game state context
|
||||||
|
│ │ │ └── GameProvider.jsx # Context provider with state logic
|
||||||
|
│ │ ├── hooks/ # Custom React hooks
|
||||||
|
│ │ │ ├── useGameDataProvider.js # React Query data provider
|
||||||
|
│ │ │ ├── useGameRefs.jsx # Refs for DOM elements
|
||||||
|
│ │ │ ├── useGameState.jsx # Game state management
|
||||||
|
│ │ │ ├── useServerCommunication.jsx # Mercure SSE connection
|
||||||
|
│ │ │ └── useStepTimer.jsx # Step-by-step timer
|
||||||
|
│ │ └── utils/ # Game-specific utilities
|
||||||
|
│ │ └── constants.jsx # Game constants, colors, defaults
|
||||||
|
│ ├── components/ # Shared UI components
|
||||||
|
│ ├── utils/ # Shared utilities
|
||||||
|
│ ├── profile.jsx # Profile page entry
|
||||||
|
│ ├── passkey.jsx # Passkey management entry
|
||||||
|
├── css/
|
||||||
|
│ └── homepage/ # SCSS partials (imported by style.homepage.scss)
|
||||||
|
└── fonts/
|
||||||
|
└── Carlito-Bold.ttf # TTF font for PHP GD image generation
|
||||||
|
```
|
||||||
|
|
||||||
|
**Important patterns:**
|
||||||
|
- Vite aliases: `@mine-components`, `@mine-contexts`, `@mine-hooks`, `@mine-utils`, `@global-components`, `@global-utils`
|
||||||
|
- React Query only available inside `mine-seeker` bundle
|
||||||
|
- Avoid circular dependencies (e.g., don't import from `@global-components` inside `components/` directory)
|
||||||
|
- PropTypes required on all components
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Common Tasks
|
||||||
|
|
||||||
|
### Adding a New Feature
|
||||||
|
|
||||||
|
1. **Backend:**
|
||||||
|
- Create migration for schema changes
|
||||||
|
- Add/update entities and repositories
|
||||||
|
- Create DTOs for data transfer
|
||||||
|
- Add controller endpoints
|
||||||
|
- Update service configuration if needed
|
||||||
|
|
||||||
|
2. **Frontend:**
|
||||||
|
- Create components in appropriate bundle
|
||||||
|
- Add PropTypes to all components
|
||||||
|
- Use existing hooks and utilities
|
||||||
|
- Follow styled-components pattern for MUI customization
|
||||||
|
|
||||||
|
3. **Documentation:**
|
||||||
|
- Update relevant docs in `docs/` folder
|
||||||
|
- Add examples if introducing new patterns
|
||||||
|
|
||||||
|
### Database Changes
|
||||||
|
|
||||||
|
- Always create migrations: `bin/console make:migration`
|
||||||
|
- Use Doctrine QueryBuilder in repositories (not raw SQL)
|
||||||
|
- For PostgreSQL-specific features (materialized views, triggers), use raw SQL in migrations only
|
||||||
|
- Materialized views should auto-refresh via triggers
|
||||||
|
|
||||||
|
### Styling
|
||||||
|
|
||||||
|
- **Web fonts:** Use `@fontsource` packages (WOFF/WOFF2)
|
||||||
|
- **Server-side images:** Use TTF fonts in `assets/fonts/` (PHP GD requires TTF)
|
||||||
|
- **CSS:** Create SCSS partials in `assets/css/homepage/`, import in main file
|
||||||
|
- **Components:** Use Emotion styled-components or CSS classes
|
||||||
|
|
||||||
|
### File Headers
|
||||||
|
|
||||||
|
All PHP and JS/JSX files should have this header:
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php declare(strict_types=1);
|
||||||
|
/*
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
```
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
/**
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Coding Standards
|
||||||
|
|
||||||
|
### PHP
|
||||||
|
|
||||||
|
- **PSR Standards:** Follow PSR-1, PSR-12 coding standards
|
||||||
|
- **Type declarations:** Use strict types (`declare(strict_types=1)`)
|
||||||
|
- **Property promotion:** Use constructor property promotion for DTOs and services
|
||||||
|
- **Readonly:** Use `readonly` for immutable properties
|
||||||
|
- **Final:** Mark DTOs as `final`
|
||||||
|
- **Doctrine:** Use QueryBuilder with `expr()` methods, not string concatenation
|
||||||
|
- **Null safety:** Use null coalescing `??` and null-safe operator `?->`
|
||||||
|
- **Formatting:** 4-space indentation, opening braces on same line for methods/classes
|
||||||
|
|
||||||
|
**Example DTO:**
|
||||||
|
|
||||||
|
```php
|
||||||
|
final readonly class ProfileGameDto implements JsonSerializable
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
public ?int $id,
|
||||||
|
public string $redName,
|
||||||
|
public string $blueName,
|
||||||
|
public bool $bothRegistered,
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### JavaScript/React
|
||||||
|
|
||||||
|
- **Components:** Functional components with hooks
|
||||||
|
- **PropTypes:** Required on all components
|
||||||
|
- **Imports:** Use aliases (`@global-components`, `@mine-hooks`, etc.)
|
||||||
|
- **State:** Use `useState`, `useEffect`, `useCallback`, `useMemo` appropriately
|
||||||
|
- **Avoid:** Circular dependencies, especially with barrel exports
|
||||||
|
|
||||||
|
**Example component:**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { string, number } from 'prop-types';
|
||||||
|
|
||||||
|
export const MyComponent = ({ title, count }) => {
|
||||||
|
const [value, setValue] = useState(0);
|
||||||
|
|
||||||
|
return <div>{title}: {count + value}</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
MyComponent.propTypes = {
|
||||||
|
title: string.isRequired,
|
||||||
|
count: number.isRequired,
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Important Files & Locations
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
|
||||||
|
- `config/services.yaml` - Service definitions and parameters
|
||||||
|
- `vite.config.js` - Vite build config, aliases
|
||||||
|
- `.env` - Environment variables (not in git)
|
||||||
|
- `composer.json` - PHP dependencies
|
||||||
|
- `package.json` - Node dependencies
|
||||||
|
|
||||||
|
### Key Services
|
||||||
|
|
||||||
|
- `BattleCardGenerator` - Generates 1200×630 PNG OG images using PHP GD
|
||||||
|
- `TopicManager` - Mercure topic management and game logic
|
||||||
|
- `WebAuthnService` - Passkey authentication
|
||||||
|
- `Email services` - Various email senders in `src/Service/Email/`
|
||||||
|
|
||||||
|
### Important Entities
|
||||||
|
|
||||||
|
- `User` - Registered users
|
||||||
|
- `PlayedGame` - Game records with moves, grid, scores
|
||||||
|
- `RecentBattle` - Read-only entity from materialized view
|
||||||
|
- `UserStats` - Read-only entity from materialized view
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
|
||||||
|
- `docs/game-mechanics/BONUS_POINTS_SYSTEM.md` - Bonus points reference
|
||||||
|
- `docs/FONTS.md` - Font usage and management
|
||||||
|
- `AGENTS.md` - This file
|
||||||
|
- `CHANGELOG.md` - Project changelog
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Common Pitfalls
|
||||||
|
|
||||||
|
### ❌ Don't Do
|
||||||
|
|
||||||
|
- **Don't use raw SQL in repositories** (use Doctrine QueryBuilder)
|
||||||
|
- **Don't create circular imports** (e.g., importing `@global-components` from within `components/`)
|
||||||
|
- **Don't use system fonts directly** (bundle TTF in `assets/fonts/`)
|
||||||
|
- **Don't skip PropTypes** on React components
|
||||||
|
- **Don't use `var`** in JavaScript (use `const` or `let`)
|
||||||
|
- **Don't define variables after using them** (hoisting issues with `const`)
|
||||||
|
|
||||||
|
### ✅ Do
|
||||||
|
|
||||||
|
- **Use Doctrine QueryBuilder** with `expr()` methods
|
||||||
|
- **Import components directly** to avoid circular dependencies
|
||||||
|
- **Bundle fonts in project** for portability
|
||||||
|
- **Add PropTypes** to all components
|
||||||
|
- **Use `const` for immutable values**, `let` for mutable
|
||||||
|
- **Define variables before using them**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Keeping Components in Sync
|
||||||
|
|
||||||
|
### Battle Report Display
|
||||||
|
|
||||||
|
The battle report/statistics appear in **two places** that must be kept synchronized:
|
||||||
|
|
||||||
|
#### 1. BattleDialog Component (React)
|
||||||
|
**File:** `assets/js/components/BattleDialog.jsx`
|
||||||
|
- React dialog component shown on profile page
|
||||||
|
- Uses Material-UI and styled-components
|
||||||
|
- Displays game stats, bonus points, winner information
|
||||||
|
|
||||||
|
#### 2. Battle Share Page (Twig)
|
||||||
|
**File:** `templates/Game/battle_share.html.twig`
|
||||||
|
- Public battle share page (accessible via `/battle/{uuid}`)
|
||||||
|
- Server-rendered HTML with SCSS styling
|
||||||
|
- Shows same information as BattleDialog
|
||||||
|
|
||||||
|
### Synchronization Rules
|
||||||
|
|
||||||
|
**When updating BattleDialog.jsx, also update battle_share.html.twig:**
|
||||||
|
|
||||||
|
✅ Adding new stats or fields
|
||||||
|
✅ Changing display logic (winner calculation, formatting)
|
||||||
|
✅ Modifying bonus points display
|
||||||
|
✅ Updating labels or text
|
||||||
|
|
||||||
|
**Keep in sync:**
|
||||||
|
- Game outcome logic (win/loss/draw/abandoned)
|
||||||
|
- Bonus points formatting
|
||||||
|
- Player name display
|
||||||
|
- Score display
|
||||||
|
- Stats and metadata shown
|
||||||
|
|
||||||
|
**Example:** If you add a new stat to BattleDialog showing "fastest mine claim time", you must also add it to battle_share.html.twig so both displays show the same information.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Testing & Building
|
||||||
|
|
||||||
|
### Backend
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run migrations
|
||||||
|
bin/console doctrine:migrations:migrate
|
||||||
|
|
||||||
|
# Clear cache
|
||||||
|
bin/console cache:clear
|
||||||
|
|
||||||
|
# Refresh materialized views
|
||||||
|
bin/console dbal:run-sql "REFRESH MATERIALIZED VIEW CONCURRENTLY recent_battles"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Frontend
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Development build with watch
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
# Production build
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
# After changing fonts
|
||||||
|
rm -rf var/og-cache/*
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Git Workflow
|
||||||
|
|
||||||
|
### Commit Messages
|
||||||
|
|
||||||
|
Follow conventional commits:
|
||||||
|
|
||||||
|
- `feat: add bonus points to battle cards`
|
||||||
|
- `fix: resolve circular dependency in BattleDialog`
|
||||||
|
- `refactor: use Doctrine QueryBuilder in RecentBattleRepository`
|
||||||
|
- `docs: add AGENTS.md for AI coding agents`
|
||||||
|
- `chore: update dependencies`
|
||||||
|
|
||||||
|
### Creating Pull Requests
|
||||||
|
|
||||||
|
When creating PRs, include:
|
||||||
|
|
||||||
|
1. **Summary** - What was changed and why
|
||||||
|
2. **Changes** - List of modified files/components
|
||||||
|
3. **Testing** - How to verify the changes
|
||||||
|
4. **Screenshots** - For UI changes
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Performance Considerations
|
||||||
|
|
||||||
|
### Database
|
||||||
|
|
||||||
|
- Use materialized views for expensive queries (profile stats, recent battles)
|
||||||
|
- Auto-refresh materialized views via triggers on source table changes
|
||||||
|
- Index frequently queried columns
|
||||||
|
- Use `COALESCE()` for nullable aggregates
|
||||||
|
|
||||||
|
### Frontend
|
||||||
|
|
||||||
|
- Lazy load large components
|
||||||
|
- Use React Query for data fetching and caching
|
||||||
|
- Avoid unnecessary re-renders (use `useMemo`, `useCallback`)
|
||||||
|
- Bundle code per entry point (profile, passkey, contact, game)
|
||||||
|
|
||||||
|
### Images
|
||||||
|
|
||||||
|
- Battle card images are cached in `var/og-cache/`
|
||||||
|
- Images regenerated when games change (via deterministic UUID)
|
||||||
|
- Use appropriate image sizes (1200×630 for OG images)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Security
|
||||||
|
|
||||||
|
### Authentication
|
||||||
|
|
||||||
|
- Password + TOTP (optional) + WebAuthn passkeys (optional)
|
||||||
|
- Backup codes for TOTP recovery
|
||||||
|
- Session-based authentication with `IS_AUTHENTICATED_REMEMBERED`
|
||||||
|
|
||||||
|
### Data Access
|
||||||
|
|
||||||
|
- Controllers check `denyAccessUnlessGranted()`
|
||||||
|
- Materialized views filter by `user_id`
|
||||||
|
- Guest players use separate `Gamer` entity (no `User` account)
|
||||||
|
|
||||||
|
### Input Validation
|
||||||
|
|
||||||
|
- Symfony forms for user input
|
||||||
|
- File upload validation (size, MIME type)
|
||||||
|
- WebAuthn challenge validation
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Useful Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Symfony
|
||||||
|
bin/console debug:container ServiceName # Inspect service configuration
|
||||||
|
bin/console debug:router # List all routes
|
||||||
|
bin/console make:migration # Create new migration
|
||||||
|
bin/console doctrine:migrations:list # List migrations
|
||||||
|
|
||||||
|
# Database
|
||||||
|
bin/console dbal:run-sql "SELECT * FROM ..." # Run SQL query
|
||||||
|
|
||||||
|
# Assets
|
||||||
|
npm run build # Build production assets
|
||||||
|
npm run dev # Build with watch mode
|
||||||
|
|
||||||
|
# Git
|
||||||
|
git log --oneline --graph # View commit history
|
||||||
|
git diff origin/main...HEAD # See changes since main
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Need Help?
|
||||||
|
|
||||||
|
- **Bonus Points:** See `docs/game-mechanics/BONUS_POINTS_SYSTEM.md`
|
||||||
|
- **Fonts:** See `docs/FONTS.md`
|
||||||
|
- **Symfony Docs:** https://symfony.com/doc/current/
|
||||||
|
- **React Docs:** https://react.dev/
|
||||||
|
- **Doctrine ORM:** https://www.doctrine-project.org/projects/doctrine-orm/en/current/
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Version History
|
||||||
|
|
||||||
|
- **2026-04-21:** Initial AGENTS.md created
|
||||||
|
- Document common patterns, pitfalls, and project structure
|
||||||
|
- Include coding standards and examples
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Happy coding! 🚀**
|
||||||
+607
-101
@@ -1,105 +1,611 @@
|
|||||||
Changelog
|
# Changelog
|
||||||
=========
|
|
||||||
|
|
||||||
|
|
||||||
0.4.0 (2019-10-26)
|
## v2026.3.1-2 (2026-09-21)
|
||||||
------------------
|
|
||||||
- Change session driver to REDIS. [Lang]
|
### Changes
|
||||||
- Add created, updated field to db && improve graph design. [Lang]
|
|
||||||
- Cache setup && optimalize for google pagespeed && optimalize all
|
* Completely disable the contact form #15. [Lang]
|
||||||
images. [Lang]
|
|
||||||
- Improve graph design on homepage && add footer and techs && add
|
|
||||||
official pages. [Lang]
|
## v2026.3.1-0 (2026-08-18)
|
||||||
- Bugfix mine websocket periodic mysql calling. [Lang]
|
|
||||||
- Bugfix hwioauth remember me && centralize hwioauth and facebook
|
### Changes
|
||||||
settings. [Lang]
|
|
||||||
- Centralize jquery && bugfix mysql auto-termination problem w/ user
|
* Add backup/restore scripts #15. [Lang]
|
||||||
auth. [Lang]
|
|
||||||
- Release beta4. [Lang]
|
* Fix the changing local url names from legacy .local - & change the minio default port #15. [Lang]
|
||||||
- Gitignore npm debug log. [Lang]
|
|
||||||
- Add english lang everywhere && add snowfall && add centralized version
|
|
||||||
nbr && improve stylesheet && slack integration. [Lang]
|
## v2026.3.0-0 (2026-07-27)
|
||||||
- Bugfix #30 && random bg in game. [Lang]
|
|
||||||
- Add google analytics and facebook scripts && improve url share method
|
### New
|
||||||
w/ fb && enforce https in prod. [Lang]
|
|
||||||
- Reg and login buttons on index && remove list method && facebook
|
* Hardening the registration process with removing the not activated registrations #14. [Lang]
|
||||||
centralize. [Lang]
|
|
||||||
- Redesign user frontend. [Lang]
|
### Changes
|
||||||
- Mods for performance; one js.min file on prod. [Lang]
|
|
||||||
- Improve webpack config for prod compile #23. [Lang]
|
* Add current version of the app in the footer #14. [Lang]
|
||||||
- Ssl handling #22 && reconnection issues #20, #21. [Lang]
|
|
||||||
- Facebook prod settings w/ app; hwi/HWIOAuthBundle. [Lang]
|
* Upgrade FE to the latest possible version #14. [Lang]
|
||||||
- Refact && game reconnection and restore w/o refresh #3 && bugfix bomb
|
|
||||||
explosion on opponent mines #19. [Lang]
|
* All tests passed and migrated to Symfony 8 #14. [Lang]
|
||||||
- Typo in rpc. [Lang]
|
|
||||||
- Handle prod mysql timeout && graphics improve. [Lang]
|
* Upgrade the BE deps to the latest possible version #14. [Lang]
|
||||||
- Gitignore webpacked index.js. [Lang]
|
|
||||||
- Add production mods. [Lang]
|
* Remove registration notification #14. [Lang]
|
||||||
- Bugfix points saving and exploded bombs to db && you can resign #6.
|
|
||||||
[Lang]
|
* Add a process that removes the unnecessary minio-init when it does not needed anymore #14. [Lang]
|
||||||
- Bugfix resign button existence #11. [Lang]
|
|
||||||
- Bugfix opponent bomb btn buzz on hover #10. [Lang]
|
* Removed the obsolete contact form entry - legacy code from Google ReCapthca #14. [Lang]
|
||||||
- Bugfix points problem in the end #16. [Lang]
|
|
||||||
- Add desc to every user #9. [Lang]
|
|
||||||
- Clipboard - not working #8. [Lang]
|
## v2026.2.10-0 (2026-06-02)
|
||||||
- Random player on start #5. [Lang]
|
|
||||||
- Show left mines after end #2 && reduce network traffic && better
|
### Changes
|
||||||
active field checking method. [Lang]
|
|
||||||
- Some refactor #13. [Lang]
|
* Add clickable badges for Cap app-wide #13. [Lang]
|
||||||
- Bugfix grid field render #12. [Lang]
|
|
||||||
- Game ends after x mines. [Lang]
|
* Upgrade both backend and frontend to the latest available version #13. [Lang]
|
||||||
- Add new sounds && refactor && new bg images && form redesigns. [Lang]
|
|
||||||
- Bugfix entities gridrow, grid && improve graph design on homepage.
|
* Replace Google ReCaptcha with Cap instance #13. [Lang]
|
||||||
[Lang]
|
|
||||||
- Some refactor && prod settings. [Lang]
|
* Fix all eslint issues - & add example of the testing env #10. [Lang]
|
||||||
- Improve graphics design in game. [Lang]
|
|
||||||
- Bugfix grid row in entity. [Lang]
|
|
||||||
- Bugfix changePlayer after bomb explosion. [Lang]
|
## v2026.2.9-0 (2026-04-23)
|
||||||
- Improve game graph design. [Lang]
|
|
||||||
- Login and register form more design. [Lang]
|
### New
|
||||||
- Add basic design to userbundle && refactor. [Lang]
|
|
||||||
- Add font-awesome. [Lang]
|
* Add tracking code for the app #10. [Lang]
|
||||||
- Working user authentication w/ fb and plain login. [Lang]
|
|
||||||
- Add facebook login module, hwi/HWIOAuthBundle. [Lang]
|
|
||||||
- Login && register form overrided. [Lang]
|
## v2026.2.8-3 (2026-04-22)
|
||||||
- Js and config refactor. [Lang]
|
|
||||||
- Replace gridcol object to json array in db. [Lang]
|
### Fix
|
||||||
- Refactor. [Lang]
|
|
||||||
- Save steps and point info to db. [Lang]
|
* The error message cannot be seen during avatar changing #10. [Lang]
|
||||||
- Save the step data to db. [Lang]
|
|
||||||
- Renamed the acme to mineseeker && handle when the user connection has
|
|
||||||
been lost. [Lang]
|
## v2026.2.8-2 (2026-04-21)
|
||||||
- Add player names to UI. [Lang]
|
|
||||||
- Add overlay && game do not start until the opponent came. [Lang]
|
### Changes
|
||||||
- Add base64 encryption to grid when it has been sended to server.
|
|
||||||
[Lang]
|
* Increase the 2 MB avatar maximum file size to 10 MB #10. [Lang]
|
||||||
- On click opponents bomb, you cannot target && refactor. [Lang]
|
|
||||||
- Warning when player has been found more than 20 mines. [Lang]
|
|
||||||
- Bugfix center mine counter animation. [Lang]
|
## v2026.2.8-1 (2026-04-21)
|
||||||
- The opponent is the next when bomb is exploded. [Lang]
|
|
||||||
- Current username checked && refactor && remove players in channel when
|
### Changes
|
||||||
they are more than 2. [Lang]
|
|
||||||
- Send bomb info and use it on opponent. [Lang]
|
* Upgrade front-end & back-end deps to the latest available version #10. [Lang]
|
||||||
- Add sounds w/ howler. [Lang]
|
|
||||||
- Bugfix multiple empty fields w/ one click on opponent view. [Lang]
|
|
||||||
- Refact && remove sound and logging && bugfix BIGBUG - handleGridField
|
## v2026.2.8-0 (2026-04-21)
|
||||||
and showAppropriateFields sort order... [Lang]
|
|
||||||
- Create first working communication. [Lang]
|
### New
|
||||||
- Create entities and repositories. [Lang]
|
|
||||||
- Changed websocket default port && debug RPC. [Lang]
|
* Add CI/CD improvements - add new CI workflow - & improve the deployment w/ tests #10. [Lang]
|
||||||
- Created working session and client handler w/ websocket. [Lang]
|
|
||||||
- Working websocket client and server w/o session handling and storage.
|
* Add test cases to back-end w/ real database connection in it #10. [Lang]
|
||||||
[Lang]
|
|
||||||
- Composer update. [Lang]
|
### Changes
|
||||||
- Improve game && start sound creating. [Lang]
|
|
||||||
- Refactor grid control and grid field. [Lang]
|
* The original CI/CD workflow is restored - the work with tests is postponed #10. [Lang]
|
||||||
- Created basic game w/ table and animations. [Lang]
|
|
||||||
- Websocket basic setup FE & BE && working basic game w/ react &&
|
* Add sound when the game start #10. [Lang]
|
||||||
webpack & babel config. [Lang]
|
|
||||||
- Gitignore node_modules && add symlink to node_modules (just for
|
* Create AGENTS.md file for future maintenance #9. [Lang]
|
||||||
install) && basic react. [Lang]
|
|
||||||
- Add react hello world. [Lang]
|
* Remove the wrongly implemented font installation in docker - & replace it with static font on BattleCardGenerator (it solves the shareable image problem on bare-metal too) #8. [Lang]
|
||||||
- Rename project in config. [Lang]
|
|
||||||
- Initial commit && create project in symfony3. [Lang]
|
|
||||||
|
## v2026.2.7-1 (2026-04-21)
|
||||||
|
|
||||||
|
### Changes
|
||||||
|
|
||||||
|
* Fine-tune the recent battle list #8. [Lang]
|
||||||
|
|
||||||
|
|
||||||
|
## v2026.2.7-0 (2026-04-21)
|
||||||
|
|
||||||
|
### Changes
|
||||||
|
|
||||||
|
* Small changes on docs - and improve text on homepage #8. [Lang]
|
||||||
|
|
||||||
|
* Massive refactor on front-end for unification and readiness #8. [Lang]
|
||||||
|
|
||||||
|
* Update all doc blocks on back-end #8. [Lang]
|
||||||
|
|
||||||
|
* Small refactors on back-end #8. [Lang]
|
||||||
|
|
||||||
|
* Add RecentBattle entity that is a Materialized View to speed up the view - and further refactor on ProfileController #8. [Lang]
|
||||||
|
|
||||||
|
* Create the UserStats entity what is a Materialized View to store Profile stats for every user - & massive ProfileController refactor #8. [Lang]
|
||||||
|
|
||||||
|
* Refactor the SecurityController #7. [Lang]
|
||||||
|
|
||||||
|
* Refactor the code - there was unnecessary codes and wrongly formatted or designed code that are related to Repositories #7. [Lang]
|
||||||
|
|
||||||
|
* Upgrade the doctrine related back-end pkgs to the latest available version #7. [Lang]
|
||||||
|
|
||||||
|
* Add filter to the Profile page's recent plays and an infite list too #7. [Lang]
|
||||||
|
|
||||||
|
* Upgrade to the latest doctrine pkg on back-end #7. [Lang]
|
||||||
|
|
||||||
|
### Fix
|
||||||
|
|
||||||
|
* Do not hide the end-game overlay ever #8. [Lang]
|
||||||
|
|
||||||
|
* The username was not recognized properly #7. [Lang]
|
||||||
|
|
||||||
|
|
||||||
|
## v2026.2.6-1 (2026-04-19)
|
||||||
|
|
||||||
|
### Fix
|
||||||
|
|
||||||
|
* The PostgreSQL logo was horrible #7. [Lang]
|
||||||
|
|
||||||
|
|
||||||
|
## v2026.2.6-0 (2026-04-19)
|
||||||
|
|
||||||
|
### Changes
|
||||||
|
|
||||||
|
* Add ReCaptcha overlay again to protect the game #7. [Lang]
|
||||||
|
|
||||||
|
* Upgrade all front-end packages to the latest available version - and fix all eslint warnings & errors #7. [Lang]
|
||||||
|
|
||||||
|
* Upgrade fe packages #7. [Lang]
|
||||||
|
|
||||||
|
* Massive refactor on fetches - create centralized dataProvider #7. [Lang]
|
||||||
|
|
||||||
|
|
||||||
|
## v2026.2.5-0 (2026-04-19)
|
||||||
|
|
||||||
|
### New
|
||||||
|
|
||||||
|
* Add Firebase deps to back-end #7. [Lang]
|
||||||
|
|
||||||
|
* Add missing buttons for overlays #7. [Lang]
|
||||||
|
|
||||||
|
* A new feature came up - the abandoned plays can be restored, if both users are registered users #7. [Lang]
|
||||||
|
|
||||||
|
### Changes
|
||||||
|
|
||||||
|
* Fix the '0' in Battle reports #6. [Lang]
|
||||||
|
|
||||||
|
* Fix missing icons on "Battle report" #6. [Lang]
|
||||||
|
|
||||||
|
### Fix
|
||||||
|
|
||||||
|
* The bomb using was not recorded correctly - the old data will be corrupted #6. [Lang]
|
||||||
|
|
||||||
|
|
||||||
|
## v2026.2.4-0 (2026-04-18)
|
||||||
|
|
||||||
|
### New
|
||||||
|
|
||||||
|
* Add new profile charts and stats - & add new logo to the tech stack #5. [Lang]
|
||||||
|
|
||||||
|
### Changes
|
||||||
|
|
||||||
|
* Upgrade fe deps #5. [Lang]
|
||||||
|
|
||||||
|
* Improve the Battle reports to change unnecessary data with interesting data #5. [Lang]
|
||||||
|
|
||||||
|
### Fix
|
||||||
|
|
||||||
|
* The react is crashing on some cases #5. [Lang]
|
||||||
|
|
||||||
|
|
||||||
|
## v2026.2.3-0 (2026-04-18)
|
||||||
|
|
||||||
|
### New
|
||||||
|
|
||||||
|
* Add initialization bonus points' system to the gameplay #5. [Lang]
|
||||||
|
|
||||||
|
### Changes
|
||||||
|
|
||||||
|
* Add extended data to battle reports and sharing image to make viewable bonus points #5. [Lang]
|
||||||
|
|
||||||
|
|
||||||
|
## v2026.2.2-9 (2026-04-18)
|
||||||
|
|
||||||
|
### New
|
||||||
|
|
||||||
|
* Add rules page #4. [Lang]
|
||||||
|
|
||||||
|
### Fix
|
||||||
|
|
||||||
|
* The font-awesome simplifying to work on bare-metal - & fix all warnings at build time #4. [Lang]
|
||||||
|
|
||||||
|
* The css problem had been solved on reponsive gfx on homepage #4. [Lang]
|
||||||
|
|
||||||
|
|
||||||
|
## v2026.2.1-8 (2026-04-18)
|
||||||
|
|
||||||
|
### Fix
|
||||||
|
|
||||||
|
* Quickfix for https-only login - & add user data when the user is not logged in #4. [Lang]
|
||||||
|
|
||||||
|
|
||||||
|
## v2026.2.1-7 (2026-04-16)
|
||||||
|
|
||||||
|
### Changes
|
||||||
|
|
||||||
|
* Add consent checkbox to user's registration - and fix the sharing pics #4. [Lang]
|
||||||
|
|
||||||
|
* Add correct version numbering and CHANGELOG - and add the LICENSE #4. [Lang]
|
||||||
|
|
||||||
|
|
||||||
|
## v2026.2.1-6 (2026-04-16)
|
||||||
|
|
||||||
|
### Changes
|
||||||
|
|
||||||
|
* Update all texts on all pages - extend them with the game specific things #4. [Lang]
|
||||||
|
|
||||||
|
|
||||||
|
## v2026.2.1-5 (2026-04-16)
|
||||||
|
|
||||||
|
### Fix
|
||||||
|
|
||||||
|
* The meta tags does not have https scheme - nothing worked in configuration #4. [Lang]
|
||||||
|
|
||||||
|
|
||||||
|
## v2026.2.1-4 (2026-04-15)
|
||||||
|
|
||||||
|
### New
|
||||||
|
|
||||||
|
* Add notification email when a user is registered #4. [Lang]
|
||||||
|
|
||||||
|
### Changes
|
||||||
|
|
||||||
|
* Add notification on activation too #4. [Lang]
|
||||||
|
|
||||||
|
|
||||||
|
## v2026.2.1-2 (2026-04-15)
|
||||||
|
|
||||||
|
### Fix
|
||||||
|
|
||||||
|
* Another attempt to fix the email assets #4. [Lang]
|
||||||
|
|
||||||
|
|
||||||
|
## v2026.2.1-1 (2026-04-15)
|
||||||
|
|
||||||
|
### Fix
|
||||||
|
|
||||||
|
* The images does not shows in emails #4. [Lang]
|
||||||
|
|
||||||
|
|
||||||
|
## v2026.2.1-0 (2026-04-15)
|
||||||
|
|
||||||
|
### New
|
||||||
|
|
||||||
|
* Add Contact page with email sending behaviour #4. [Lang]
|
||||||
|
|
||||||
|
### Changes
|
||||||
|
|
||||||
|
* Add missing .env variable and increase the version number and add missing data from front-end and back-end deps descriptor #4. [Lang]
|
||||||
|
|
||||||
|
* Change the shareable battle - add avatars to it - even on the og tags #4. [Lang]
|
||||||
|
|
||||||
|
* Change text #4. [Lang]
|
||||||
|
|
||||||
|
### Fix
|
||||||
|
|
||||||
|
* The mailhog is crashed on development env #4. [Lang]
|
||||||
|
|
||||||
|
* The og tags did not have proper http schema - they should have https #4. [Lang]
|
||||||
|
|
||||||
|
|
||||||
|
## v2026.2.0-5 (2026-04-14)
|
||||||
|
|
||||||
|
### Changes
|
||||||
|
|
||||||
|
* Add donation button #4. [Lang]
|
||||||
|
|
||||||
|
|
||||||
|
## v2026.2.0-4 (2026-04-14)
|
||||||
|
|
||||||
|
### New
|
||||||
|
|
||||||
|
* Add timer for the acceptance of the challenge #4. [Lang]
|
||||||
|
|
||||||
|
### Changes
|
||||||
|
|
||||||
|
* Protect the gameplay with recaptcha #4. [Lang]
|
||||||
|
|
||||||
|
* The waiting dialog is uncloseable until the time is up #4. [Lang]
|
||||||
|
|
||||||
|
* Add share button to the overlay when the game ends #4. [Lang]
|
||||||
|
|
||||||
|
* Make fancy og tags - and create a special one for battle sharing #4. [Lang]
|
||||||
|
|
||||||
|
### Fix
|
||||||
|
|
||||||
|
* Missing font-awesome icons on bare-metal environment #4. [Lang]
|
||||||
|
|
||||||
|
|
||||||
|
## v2026.2.0-3 (2026-04-14)
|
||||||
|
|
||||||
|
### Changes
|
||||||
|
|
||||||
|
* The user's avatar will be saved as a uuid.extension #4. [Lang]
|
||||||
|
|
||||||
|
|
||||||
|
## v2026.2.0-1 (2026-04-14)
|
||||||
|
|
||||||
|
### Fix
|
||||||
|
|
||||||
|
* Quickfix for email sending #4. [Lang]
|
||||||
|
|
||||||
|
|
||||||
|
## v2026.2.0-0 (2026-04-14)
|
||||||
|
|
||||||
|
### New
|
||||||
|
|
||||||
|
* Registered users have avatars next to the timer #4. [Lang]
|
||||||
|
|
||||||
|
* Add opportunity to use profile picture. #4. [Lang]
|
||||||
|
|
||||||
|
* Add more stats and a dialog for the recent battle that can be shareable #4. [Lang]
|
||||||
|
|
||||||
|
* Implement the 2FA authentication (TOTP and backup codes) #4. [Lang]
|
||||||
|
|
||||||
|
* Add beta logo to the corner #3. [Lang]
|
||||||
|
|
||||||
|
* Add mineseeker game to the symfony 4 project #3. [Lang]
|
||||||
|
|
||||||
|
* Upgrade to the latest symfony v4 #3. [Lang]
|
||||||
|
|
||||||
|
### Changes
|
||||||
|
|
||||||
|
* Implement CD script to Gitea and add docs to the process #4. [Lang]
|
||||||
|
|
||||||
|
* Remove unnecessary cdn based fonts #4. [Lang]
|
||||||
|
|
||||||
|
* Update docs #4. [Lang]
|
||||||
|
|
||||||
|
* Add JWT generation script to make Mercure safe #4. [Lang]
|
||||||
|
|
||||||
|
* Fix missing favicon #4. [Lang]
|
||||||
|
|
||||||
|
* Make compatible the whole project with bare metal AND with docker #4. [Lang]
|
||||||
|
|
||||||
|
* Add modern Webauthn authentication #4. [Lang]
|
||||||
|
|
||||||
|
* Refactor all forms to have Symfony Form Types & Validation Constrainsts - & implement Google ReCapthca v3 #4. [Lang]
|
||||||
|
|
||||||
|
* Add forgot password functionality #4. [Lang]
|
||||||
|
|
||||||
|
* Increase the minimum PHP version to the latest major - and massive refactor on back-end, like Controllers and Repositories #4. [Lang]
|
||||||
|
|
||||||
|
* Redesign the resign dialog #4. [Lang]
|
||||||
|
|
||||||
|
* Re-implement the waiting for opponent dialog - refactor its gfx - & add online user selection dialog #4. [Lang]
|
||||||
|
|
||||||
|
* Improve the gfx on homepage - implement login/register and activation for authentication - and add the first version of profile page #4. [Lang]
|
||||||
|
|
||||||
|
* Refactor and redesign the gfx on front-end #4. [Lang]
|
||||||
|
|
||||||
|
* Upgrade to the latest LTS Symfony package and backend #4. [Lang]
|
||||||
|
|
||||||
|
* Add timers to each player - renew the whole migration #4. [Lang]
|
||||||
|
|
||||||
|
* Update the vite related stuff because CORS and React errors - reinit the miration #4. [Lang]
|
||||||
|
|
||||||
|
* Use namespaces for front-end #4. [Lang]
|
||||||
|
|
||||||
|
* Replace webpack w/ vite & remove old, legacy jQuery from the code #4. [Lang]
|
||||||
|
|
||||||
|
* More, massive refactor for front-end #4. [Lang]
|
||||||
|
|
||||||
|
* Massive refactor on front-end - and remove unnecessary deps #4. [Lang]
|
||||||
|
|
||||||
|
* Change the code style to fit the current standard #4. [Lang]
|
||||||
|
|
||||||
|
* Refactor to use Attributes instead of yaml markdown #4. [Lang]
|
||||||
|
|
||||||
|
* Outsource the Grid generation and interactions to the backend #4. [Lang]
|
||||||
|
|
||||||
|
* Remove unnecessary variables and prune the Facebook registration method #4. [Lang]
|
||||||
|
|
||||||
|
* Replace the legacy gos/web-socket-bundle & replace it with Mercure protocol #4. [Lang]
|
||||||
|
|
||||||
|
* Make a massive refactor to the backend and remove all unnecessary deps - and make small refactors for the frontend too #4. [Lang]
|
||||||
|
|
||||||
|
* Created the first working solution since 7 yrs #4. [Lang]
|
||||||
|
|
||||||
|
* Add some changes on BE - add eslint and editorconfig - and add some deps #4. [Lang]
|
||||||
|
|
||||||
|
* Make the first working version - the stepping is broken due to the algorythm structure #4. [Lang]
|
||||||
|
|
||||||
|
* Change the composer default php minimum environment #3. [Lang]
|
||||||
|
|
||||||
|
* Change the default url to wss on frontend #3. [Lang]
|
||||||
|
|
||||||
|
* Refactor Rpc and Topic classes #3. [Lang]
|
||||||
|
|
||||||
|
* Refactor classes and reformat some layout #3. [Lang]
|
||||||
|
|
||||||
|
* Remove deprecated files #3. [Lang]
|
||||||
|
|
||||||
|
* Doc in README.md #3. [Lang]
|
||||||
|
|
||||||
|
* Gitignore a js.map file #2. [Lang]
|
||||||
|
|
||||||
|
### Other
|
||||||
|
|
||||||
|
* Pkg: usr: solve the not-working mailing on dev env under docker #4. [Lang]
|
||||||
|
|
||||||
|
|
||||||
|
## 1.1.0 (2019-10-26)
|
||||||
|
|
||||||
|
### Changes
|
||||||
|
|
||||||
|
* Reinit project - disable redis module and make the project compatible w/ PHP7.3 #2. [Lang]
|
||||||
|
|
||||||
|
|
||||||
|
## 0.4.0 (2019-10-26)
|
||||||
|
|
||||||
|
### Other
|
||||||
|
|
||||||
|
* Change session driver to REDIS. [Lang]
|
||||||
|
|
||||||
|
* Add created, updated field to db && improve graph design. [Lang]
|
||||||
|
|
||||||
|
* Cache setup && optimalize for google pagespeed && optimalize all images. [Lang]
|
||||||
|
|
||||||
|
* Improve graph design on homepage && add footer and techs && add official pages. [Lang]
|
||||||
|
|
||||||
|
* Bugfix mine websocket periodic mysql calling. [Lang]
|
||||||
|
|
||||||
|
* Bugfix hwioauth remember me && centralize hwioauth and facebook settings. [Lang]
|
||||||
|
|
||||||
|
* Centralize jquery && bugfix mysql auto-termination problem w/ user auth. [Lang]
|
||||||
|
|
||||||
|
* Release beta4. [Lang]
|
||||||
|
|
||||||
|
* Gitignore npm debug log. [Lang]
|
||||||
|
|
||||||
|
* Add english lang everywhere && add snowfall && add centralized version nbr && improve stylesheet && slack integration. [Lang]
|
||||||
|
|
||||||
|
* Bugfix #30 && random bg in game. [Lang]
|
||||||
|
|
||||||
|
* Add google analytics and facebook scripts && improve url share method w/ fb && enforce https in prod. [Lang]
|
||||||
|
|
||||||
|
* Reg and login buttons on index && remove list method && facebook centralize. [Lang]
|
||||||
|
|
||||||
|
* Redesign user frontend. [Lang]
|
||||||
|
|
||||||
|
* Mods for performance; one js.min file on prod. [Lang]
|
||||||
|
|
||||||
|
* Improve webpack config for prod compile #23. [Lang]
|
||||||
|
|
||||||
|
* Ssl handling #22 && reconnection issues #20, #21. [Lang]
|
||||||
|
|
||||||
|
* Facebook prod settings w/ app; hwi/HWIOAuthBundle. [Lang]
|
||||||
|
|
||||||
|
* Refact && game reconnection and restore w/o refresh #3 && bugfix bomb explosion on opponent mines #19. [Lang]
|
||||||
|
|
||||||
|
* Typo in rpc. [Lang]
|
||||||
|
|
||||||
|
* Handle prod mysql timeout && graphics improve. [Lang]
|
||||||
|
|
||||||
|
* Gitignore webpacked index.js. [Lang]
|
||||||
|
|
||||||
|
* Add production mods. [Lang]
|
||||||
|
|
||||||
|
* Bugfix points saving and exploded bombs to db && you can resign #6. [Lang]
|
||||||
|
|
||||||
|
* Bugfix resign button existence #11. [Lang]
|
||||||
|
|
||||||
|
* Bugfix opponent bomb btn buzz on hover #10. [Lang]
|
||||||
|
|
||||||
|
* Bugfix points problem in the end #16. [Lang]
|
||||||
|
|
||||||
|
* Add desc to every user #9. [Lang]
|
||||||
|
|
||||||
|
* Clipboard - not working #8. [Lang]
|
||||||
|
|
||||||
|
* Random player on start #5. [Lang]
|
||||||
|
|
||||||
|
* Show left mines after end #2 && reduce network traffic && better active field checking method. [Lang]
|
||||||
|
|
||||||
|
* Some refactor #13. [Lang]
|
||||||
|
|
||||||
|
* Bugfix grid field render #12. [Lang]
|
||||||
|
|
||||||
|
* Game ends after x mines. [Lang]
|
||||||
|
|
||||||
|
* Add new sounds && refactor && new bg images && form redesigns. [Lang]
|
||||||
|
|
||||||
|
* Bugfix entities gridrow, grid && improve graph design on homepage. [Lang]
|
||||||
|
|
||||||
|
* Some refactor && prod settings. [Lang]
|
||||||
|
|
||||||
|
* Improve graphics design in game. [Lang]
|
||||||
|
|
||||||
|
* Bugfix grid row in entity. [Lang]
|
||||||
|
|
||||||
|
* Bugfix changePlayer after bomb explosion. [Lang]
|
||||||
|
|
||||||
|
* Improve game graph design. [Lang]
|
||||||
|
|
||||||
|
* Login and register form more design. [Lang]
|
||||||
|
|
||||||
|
* Add basic design to userbundle && refactor. [Lang]
|
||||||
|
|
||||||
|
* Add font-awesome. [Lang]
|
||||||
|
|
||||||
|
* Working user authentication w/ fb and plain login. [Lang]
|
||||||
|
|
||||||
|
* Add facebook login module, hwi/HWIOAuthBundle. [Lang]
|
||||||
|
|
||||||
|
* Login && register form overrided. [Lang]
|
||||||
|
|
||||||
|
* Js and config refactor. [Lang]
|
||||||
|
|
||||||
|
* Replace gridcol object to json array in db. [Lang]
|
||||||
|
|
||||||
|
* Refactor. [Lang]
|
||||||
|
|
||||||
|
* Save steps and point info to db. [Lang]
|
||||||
|
|
||||||
|
* Save the step data to db. [Lang]
|
||||||
|
|
||||||
|
* Renamed the acme to mineseeker && handle when the user connection has been lost. [Lang]
|
||||||
|
|
||||||
|
* Add player names to UI. [Lang]
|
||||||
|
|
||||||
|
* Add overlay && game do not start until the opponent came. [Lang]
|
||||||
|
|
||||||
|
* Add base64 encryption to grid when it has been sended to server. [Lang]
|
||||||
|
|
||||||
|
* On click opponents bomb, you cannot target && refactor. [Lang]
|
||||||
|
|
||||||
|
* Warning when player has been found more than 20 mines. [Lang]
|
||||||
|
|
||||||
|
* Bugfix center mine counter animation. [Lang]
|
||||||
|
|
||||||
|
* The opponent is the next when bomb is exploded. [Lang]
|
||||||
|
|
||||||
|
* Current username checked && refactor && remove players in channel when they are more than 2. [Lang]
|
||||||
|
|
||||||
|
* Send bomb info and use it on opponent. [Lang]
|
||||||
|
|
||||||
|
* Add sounds w/ howler. [Lang]
|
||||||
|
|
||||||
|
* Bugfix multiple empty fields w/ one click on opponent view. [Lang]
|
||||||
|
|
||||||
|
* Refact && remove sound and logging && bugfix BIGBUG - handleGridField and showAppropriateFields sort order... [Lang]
|
||||||
|
|
||||||
|
* Create first working communication. [Lang]
|
||||||
|
|
||||||
|
* Create entities and repositories. [Lang]
|
||||||
|
|
||||||
|
* Changed websocket default port && debug RPC. [Lang]
|
||||||
|
|
||||||
|
* Created working session and client handler w/ websocket. [Lang]
|
||||||
|
|
||||||
|
* Working websocket client and server w/o session handling and storage. [Lang]
|
||||||
|
|
||||||
|
* Composer update. [Lang]
|
||||||
|
|
||||||
|
* Improve game && start sound creating. [Lang]
|
||||||
|
|
||||||
|
* Refactor grid control and grid field. [Lang]
|
||||||
|
|
||||||
|
* Created basic game w/ table and animations. [Lang]
|
||||||
|
|
||||||
|
* Websocket basic setup FE & BE && working basic game w/ react && webpack & babel config. [Lang]
|
||||||
|
|
||||||
|
* Gitignore node_modules && add symlink to node_modules (just for install) && basic react. [Lang]
|
||||||
|
|
||||||
|
* Add react hello world. [Lang]
|
||||||
|
|
||||||
|
* Rename project in config. [Lang]
|
||||||
|
|
||||||
|
* Initial commit && create project in symfony3. [Lang]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
{$CADDY_GLOBAL_OPTIONS}
|
||||||
|
|
||||||
|
frankenphp {
|
||||||
|
{$FRANKENPHP_CONFIG}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{$SERVER_NAME:localhost} {
|
||||||
|
log
|
||||||
|
|
||||||
|
root * /app/public
|
||||||
|
|
||||||
|
encode zstd br gzip
|
||||||
|
|
||||||
|
# Forward scheme information to the PHP application
|
||||||
|
header X-Forwarded-Proto {scheme}
|
||||||
|
header X-Forwarded-Host {host}
|
||||||
|
|
||||||
|
mercure {
|
||||||
|
transport bolt {
|
||||||
|
path /data/mercure.db
|
||||||
|
}
|
||||||
|
protocol_version_compatibility 8
|
||||||
|
publisher_jwt {$MERCURE_JWT_SECRET} HS256
|
||||||
|
subscriber_jwt {$MERCURE_JWT_SECRET} HS256
|
||||||
|
anonymous
|
||||||
|
subscriptions
|
||||||
|
}
|
||||||
|
|
||||||
|
php_server
|
||||||
|
}
|
||||||
+53
@@ -0,0 +1,53 @@
|
|||||||
|
FROM oven/bun:1-alpine AS assets
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY package.json bun.lock ./
|
||||||
|
RUN bun install --frozen-lockfile
|
||||||
|
|
||||||
|
COPY vite.config.js ./
|
||||||
|
COPY assets/ assets/
|
||||||
|
COPY public/ public/
|
||||||
|
|
||||||
|
RUN bun run build
|
||||||
|
|
||||||
|
FROM dunglas/frankenphp:latest
|
||||||
|
|
||||||
|
RUN install-php-extensions \
|
||||||
|
pdo_pgsql \
|
||||||
|
gd \
|
||||||
|
intl \
|
||||||
|
zip \
|
||||||
|
opcache \
|
||||||
|
apcu \
|
||||||
|
sodium
|
||||||
|
|
||||||
|
RUN mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini"
|
||||||
|
RUN printf '[PHP]\nupload_max_filesize=10M\npost_max_size=11M\n' > "$PHP_INI_DIR/conf.d/uploads.ini"
|
||||||
|
RUN printf '[opcache]\nopcache.enable=1\nopcache.memory_consumption=256\nopcache.max_accelerated_files=20000\nopcache.validate_timestamps=0\n' \
|
||||||
|
> "$PHP_INI_DIR/conf.d/opcache.ini"
|
||||||
|
|
||||||
|
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
|
||||||
|
|
||||||
|
ENV APP_ENV=prod
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
RUN composer install \
|
||||||
|
--no-dev \
|
||||||
|
--no-interaction \
|
||||||
|
--no-scripts \
|
||||||
|
--optimize-autoloader
|
||||||
|
|
||||||
|
COPY --from=assets /app/public/build ./public/build
|
||||||
|
|
||||||
|
RUN mkdir -p var/cache var/log var/sessions && \
|
||||||
|
chown -R www-data:www-data var/
|
||||||
|
|
||||||
|
COPY Caddyfile /etc/caddy/Caddyfile
|
||||||
|
COPY docker/entrypoint.sh /entrypoint.sh
|
||||||
|
RUN chmod +x /entrypoint.sh
|
||||||
|
|
||||||
|
ENTRYPOINT ["/entrypoint.sh"]
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
GNU GENERAL PUBLIC LICENSE
|
||||||
|
Version 3, 29 June 2007
|
||||||
|
|
||||||
|
Copyright (C) 2026 SplendidBear (https://www.splendidbear.org)
|
||||||
|
|
||||||
|
MineSeeker is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
MineSeeker is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS:
|
||||||
|
|
||||||
|
0. Definitions.
|
||||||
|
|
||||||
|
"This License" refers to version 3 of the GNU General Public License.
|
||||||
|
|
||||||
|
"Copyright" also means copyright-like laws that apply to other kinds of works.
|
||||||
|
|
||||||
|
"The Program" refers to any copyrightable work licensed under this License.
|
||||||
|
|
||||||
|
"You" refers to each licensee.
|
||||||
|
|
||||||
|
"Legal entities" means the union of the acting entity and all other entities
|
||||||
|
that control, are controlled by, or are under common control with that entity.
|
||||||
|
|
||||||
|
"Modify" means to copy from or adapt all or part of the work in a fashion
|
||||||
|
requiring copyright permission, other than the making of an exact copy.
|
||||||
|
|
||||||
|
1. Source Code.
|
||||||
|
|
||||||
|
The "source code" for a work means the preferred form of the work for making
|
||||||
|
modifications to it. "Object code" means any non-source form of a work.
|
||||||
|
|
||||||
|
2. Basic Permissions.
|
||||||
|
|
||||||
|
All rights granted under this License are granted for the term of copyright
|
||||||
|
on the Program. You are granted all permissions necessary to run, modify and
|
||||||
|
propagate covered works by this License.
|
||||||
|
|
||||||
|
3. Copyleft - Derivative Works.
|
||||||
|
|
||||||
|
If you modify the Program, your modified version must:
|
||||||
|
|
||||||
|
- Carry prominent notices stating that you have modified it
|
||||||
|
- License the entire work under this License or a compatible license
|
||||||
|
- Make the source code available to recipients
|
||||||
|
- Preserve all notices of previous licensing
|
||||||
|
|
||||||
|
4. Conveying Verbatim Copies.
|
||||||
|
|
||||||
|
You may convey verbatim copies of the Program's source code as you receive it,
|
||||||
|
provided that you:
|
||||||
|
|
||||||
|
- Keep intact all notices of authorship and licensing
|
||||||
|
- Give recipients access to the source code along with this License
|
||||||
|
- Do not modify anything except the License itself
|
||||||
|
|
||||||
|
5. Conveying Modified Source Versions.
|
||||||
|
|
||||||
|
You may convey a work based on the Program under this License provided that:
|
||||||
|
|
||||||
|
- The work must be licensed as a whole under this License
|
||||||
|
- You must give prominent notice of any modifications
|
||||||
|
- You must provide access to the Corresponding Source code
|
||||||
|
- You preserve all licensing notices
|
||||||
|
|
||||||
|
6. Conveying Non-Source Forms.
|
||||||
|
|
||||||
|
If you convey object code or compiled versions, you must also provide:
|
||||||
|
|
||||||
|
- The Corresponding Source code (in machine-readable form)
|
||||||
|
- A notice of the terms under which it is licensed
|
||||||
|
|
||||||
|
7. Additional Terms.
|
||||||
|
|
||||||
|
No additional restrictions may be placed on the exercise of the rights
|
||||||
|
granted or affirmed under this License.
|
||||||
|
|
||||||
|
8. Disclaimer of Warranty.
|
||||||
|
|
||||||
|
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||||
|
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||||
|
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||||
|
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||||
|
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||||
|
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||||
|
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||||
|
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||||
|
|
||||||
|
9. Limitation of Liability.
|
||||||
|
|
||||||
|
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||||
|
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||||
|
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING
|
||||||
|
ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF
|
||||||
|
THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS
|
||||||
|
OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR
|
||||||
|
THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||||
|
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||||
|
SUCH DAMAGES.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
For the complete GPL-3.0-or-later license text, visit:
|
||||||
|
https://www.gnu.org/licenses/gpl-3.0.html
|
||||||
|
|
||||||
|
For more information about GNU GPL, visit:
|
||||||
|
https://www.gnu.org/licenses/
|
||||||
|
|
||||||
|
MineSeeker is a multiplayer minesweeper game inspired by MSN Messenger's game.
|
||||||
|
Project: https://www.mineseeker.hu
|
||||||
|
Author: SplendidBear (https://www.splendidbear.org)
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
.PHONY: help start start-build stop build down ps logs prune-everything db-reset mercure-jwt cache-clear og-cache-clear test-db-setup test-db-reset test
|
||||||
|
|
||||||
|
.DEFAULT_GOAL := help
|
||||||
|
|
||||||
|
help:
|
||||||
|
@echo "Available commands:"
|
||||||
|
@echo " make start - Start services (storage migration runs automatically once)"
|
||||||
|
@echo " make start-build - Build and start services (storage migration runs automatically once)"
|
||||||
|
@echo " make stop - Stop running services"
|
||||||
|
@echo " make build - Build Docker images only"
|
||||||
|
@echo " make down - Stop and remove containers/networks"
|
||||||
|
@echo " make prune-everything - Prune volumes, networks and images (DANGEROUS!)"
|
||||||
|
@echo " make db-reset - Reset the database (drop, create, migrate) (DANGEROUS!)"
|
||||||
|
@echo " make ccp - Clear the production cache"
|
||||||
|
@echo " make cache-clear - Clear all caches (Vite, Symfony, node_modules)"
|
||||||
|
@echo " make og-cache-clear - Clear Open Graph cache only"
|
||||||
|
@echo " make test-db-setup - One-time setup: Create test database and run migrations"
|
||||||
|
@echo " make test-db-reset - Reset test database (drop, create, migrate)"
|
||||||
|
@echo " make test - Run PHPUnit tests"
|
||||||
|
|
||||||
|
start:
|
||||||
|
docker compose up -d db mail
|
||||||
|
@if [ -n "$$(docker compose --profile storage-migration ps -a -q minio)" ]; then \
|
||||||
|
echo "Legacy MinIO found; migrating its data to RustFS..."; \
|
||||||
|
docker compose --profile storage-migration up -d minio; \
|
||||||
|
docker compose up -d rustfs; \
|
||||||
|
docker compose --profile storage-migration up --abort-on-container-exit --exit-code-from storage_migrate storage_migrate; \
|
||||||
|
else \
|
||||||
|
echo "No legacy MinIO container found; starting RustFS only."; \
|
||||||
|
docker compose up -d rustfs; \
|
||||||
|
fi
|
||||||
|
docker compose up -d app scheduler
|
||||||
|
|
||||||
|
start-build:
|
||||||
|
composer i
|
||||||
|
bun run build
|
||||||
|
docker compose up -d --build db mail
|
||||||
|
@if [ -n "$$(docker compose --profile storage-migration ps -a -q minio)" ]; then \
|
||||||
|
echo "Legacy MinIO found; migrating its data to RustFS..."; \
|
||||||
|
docker compose --profile storage-migration up -d minio; \
|
||||||
|
docker compose up -d rustfs; \
|
||||||
|
docker compose --profile storage-migration up --build --abort-on-container-exit --exit-code-from storage_migrate storage_migrate; \
|
||||||
|
else \
|
||||||
|
echo "No legacy MinIO container found; starting RustFS only."; \
|
||||||
|
docker compose up -d rustfs; \
|
||||||
|
fi
|
||||||
|
docker compose up -d --build app scheduler
|
||||||
|
|
||||||
|
stop:
|
||||||
|
docker compose --profile storage-migration stop
|
||||||
|
|
||||||
|
build:
|
||||||
|
docker compose --profile storage-migration build
|
||||||
|
|
||||||
|
down:
|
||||||
|
docker compose --profile storage-migration down
|
||||||
|
|
||||||
|
prune-everything:
|
||||||
|
@echo "WARNING: This will remove ALL containers, volumes, networks and images!"
|
||||||
|
@read -p "Type 'yes' to confirm: " confirm; \
|
||||||
|
if [ "$$confirm" != "yes" ]; then \
|
||||||
|
echo "Aborted."; \
|
||||||
|
exit 1; \
|
||||||
|
fi
|
||||||
|
docker compose --profile storage-migration down -v --rmi all --remove-orphans
|
||||||
|
|
||||||
|
mercure-jwt:
|
||||||
|
@php bin/generate-mercure-jwt.php
|
||||||
|
|
||||||
|
db-reset:
|
||||||
|
@echo "WARNING: This will DROP and RECREATE the database!"
|
||||||
|
@read -p "Type 'yes' to confirm: " confirm; \
|
||||||
|
if [ "$$confirm" != "yes" ]; then \
|
||||||
|
echo "Aborted."; \
|
||||||
|
exit 1; \
|
||||||
|
fi
|
||||||
|
bin/console doctrine:database:drop --force --if-exists --no-interaction
|
||||||
|
bin/console doctrine:database:create --if-not-exists --no-interaction
|
||||||
|
bin/console doctrine:migrations:migrate --no-interaction
|
||||||
|
|
||||||
|
ccp:
|
||||||
|
bin/console cache:clear --no-warmup --env=prod
|
||||||
|
|
||||||
|
cache-clear:
|
||||||
|
@echo "Clearing all caches..."
|
||||||
|
@rm -rf node_modules/.vite
|
||||||
|
@rm -rf .vite
|
||||||
|
@rm -rf var/og-cache
|
||||||
|
@php bin/console cache:clear --no-warmup
|
||||||
|
@echo "✓ Vite cache cleared"
|
||||||
|
@echo "✓ OG cache cleared"
|
||||||
|
@echo "✓ Symfony cache cleared"
|
||||||
|
@echo ""
|
||||||
|
@echo "Rebuilding assets..."
|
||||||
|
@bun run build
|
||||||
|
@echo ""
|
||||||
|
@echo "✓ All caches cleared and assets rebuilt!"
|
||||||
|
@echo " Next step: Refresh browser with Ctrl+Shift+R"
|
||||||
|
|
||||||
|
og-cache-clear:
|
||||||
|
@echo "Clearing Open Graph cache..."
|
||||||
|
@rm -rf var/og-cache
|
||||||
|
@echo "✓ OG cache cleared!"
|
||||||
|
@echo " Battle card images will be regenerated on next access"
|
||||||
|
|
||||||
|
test-db-setup:
|
||||||
|
@echo "Setting up test database..."
|
||||||
|
@bin/console dbal:run-sql "SELECT 1 FROM pg_database WHERE datname='mineseeker_test'" 2>/dev/null | grep -q 1 || \
|
||||||
|
(bin/console dbal:run-sql "CREATE DATABASE mineseeker_test" && echo "✓ Database 'mineseeker_test' created")
|
||||||
|
@bin/console doctrine:migrations:migrate --env=test --no-interaction --allow-no-migration 2>&1 | grep -v "WARNING" || true
|
||||||
|
@echo "✓ Test database setup complete!"
|
||||||
|
@echo " Database: mineseeker_test"
|
||||||
|
@echo " Run tests with: make test"
|
||||||
|
|
||||||
|
test-db-reset:
|
||||||
|
@echo "Resetting test database..."
|
||||||
|
@bin/console dbal:run-sql "DROP DATABASE IF EXISTS mineseeker_test" --quiet
|
||||||
|
@bin/console dbal:run-sql "CREATE DATABASE mineseeker_test" --quiet
|
||||||
|
@bin/console doctrine:migrations:migrate --env=test --no-interaction --quiet
|
||||||
|
@echo "✓ Test database reset complete!"
|
||||||
|
@echo " Database: mineseeker_test"
|
||||||
|
@echo " Run tests with: make test"
|
||||||
|
|
||||||
|
test:
|
||||||
|
@php -d memory_limit=512M bin/phpunit --testdox --colors=always
|
||||||
@@ -1,42 +1,380 @@
|
|||||||
mineseeker
|
# MineSeeker
|
||||||
=========
|
|
||||||
|
|
||||||
A Symfony project created on September 22, 2016, 13:56 pm.
|
A real-time **1v1 multiplayer minesweeper** game played in the browser.
|
||||||
|
Two players race on the same hidden minefield — uncover safe cells to score points, but hit a mine and you hand the advantage to your opponent.
|
||||||
|
Games are live and synchronised instantly via a Mercure hub; no page reloads, no polling.
|
||||||
|
|
||||||
PROJECT VERSION 1.1.0-20191026
|
Created by [SplendidBear](https://www.splendidbear.org).
|
||||||
|
|
||||||
This is a Symfony 3 project w/ React JS in standalone mode and w/ WebSocket.
|
---
|
||||||
|
|
||||||
0.) Must installed modules w/ npm are in package.json + to global:
|
## Features
|
||||||
|
|
||||||
$ npm install webpack -g
|
- **Real-time 1v1 gameplay** — moves broadcast instantly over Mercure (server-sent events)
|
||||||
|
- **Guest & registered play** — jump in anonymously or create an account for stats and history
|
||||||
|
- **Full authentication stack** — email/password, passkeys (WebAuthn), TOTP 2FA with backup codes
|
||||||
|
- **Player profiles** — win/loss/draw stats, per-month charts, recent battle history with shareable replay links
|
||||||
|
- **Profile pictures** — uploaded to RustFS object storage, thumbnails generated on-the-fly by LiipImagine
|
||||||
|
- **Battle replay sharing** — share a direct link to any finished game
|
||||||
|
- **Docker-ready** — single `make start-build` brings up the full production-like stack
|
||||||
|
|
||||||
You will need a
|
---
|
||||||
.babelrc file w/ the presets
|
|
||||||
webpack.config.js - https://webpack.github.io/docs/webpack-for-browserify-users.html
|
|
||||||
same as dir where the package.json!!
|
|
||||||
|
|
||||||
(!) Tutorial: https://egghead.io/lessons/react-introduction-to-properties
|
## Tech stack
|
||||||
|
|
||||||
|
| Layer | Technology |
|
||||||
|
|---|---|
|
||||||
|
| Backend | PHP 8.5, Symfony 7.4, Doctrine ORM |
|
||||||
|
| Frontend | React 19, Vite, MUI, SCSS |
|
||||||
|
| Database | PostgreSQL 18 |
|
||||||
|
| Real-time | Mercure (built into FrankenPHP / Caddy) |
|
||||||
|
| File storage | RustFS (S3-compatible; one-time migration from legacy MinIO) |
|
||||||
|
| Image processing | LiipImagine + Flysystem |
|
||||||
|
| Server | FrankenPHP (Caddy + PHP in one binary) |
|
||||||
|
| Auth | Symfony Security, Scheb 2FA, web-auth/webauthn-framework |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
1.) Backend WebSocket server start as daemon - GeniusesOfSymfony/WebSocketBundle
|
## Requirements
|
||||||
|
|
||||||
$ nohup bin/console gos:websocket:server &
|
### Bare-metal development
|
||||||
|
- PHP >= 8.5 with extensions: `pdo_pgsql`, `gd`, `intl`, `zip`, `sodium`
|
||||||
|
- Composer 2
|
||||||
|
- Node.js 22 + Bun
|
||||||
|
- PostgreSQL 18
|
||||||
|
- Caddy with FrankenPHP and the Mercure module
|
||||||
|
- MailHog (or any SMTP server on port 1025)
|
||||||
|
- RustFS (API on port 9010, console on port 9011); legacy MinIO source on 9012/9013 during migration
|
||||||
|
|
||||||
2.) React JS WebPack watch generator w/ babel presets: es2015, react
|
### Docker
|
||||||
|
- Docker Engine 24+
|
||||||
|
- Docker Compose v2
|
||||||
|
- Bun (for building frontend assets before `make start-build`)
|
||||||
|
- Composer (for `make start-build`)
|
||||||
|
|
||||||
$ webpack -p --config=webpack-prod.config.js
|
---
|
||||||
|
|
||||||
PROD
|
## Installation
|
||||||
|
|
||||||
$ webpack --progress --colors --watch -d
|
### 1. Clone the repository
|
||||||
|
|
||||||
DEV
|
```bash
|
||||||
|
git clone https://github.com/splendidbear/mineseeker.git
|
||||||
|
cd mineseeker
|
||||||
|
```
|
||||||
|
|
||||||
-d --> Debugger; If you write this line somewhere: debugger;
|
### 2. Configure environment
|
||||||
The browser will stop the code here!!!
|
|
||||||
|
|
||||||
3.) Connect to Prod
|
```bash
|
||||||
|
cp .env.dist .env
|
||||||
|
```
|
||||||
|
|
||||||
ssh xxsvci@laszlolang.com -i ~/.ssh/id_rsa_laszlolang
|
Edit `.env` and fill in every value. Key ones:
|
||||||
|
|
||||||
|
| Variable | What to set |
|
||||||
|
|---|---|
|
||||||
|
| `APP_SECRET` | Random 32-byte hex: `openssl rand -hex 32` |
|
||||||
|
| `POSTGRES_USER/PASSWORD/DB` | Your PostgreSQL credentials |
|
||||||
|
| `MINIO_ROOT_USER/PASSWORD` | S3 storage credentials (used by RustFS and legacy MinIO during migration) |
|
||||||
|
| `MINIO_ENDPOINT` | `http://localhost:9010` (bare-metal) |
|
||||||
|
| `MINIO_PUBLIC_URL` | Public URL browsers use to reach RustFS (including media and LiipImagine thumbnails) |
|
||||||
|
| `CAP_API_ENDPOINT` | Full URL of your Cap instance including the site-key path (e.g. `https://cap.example.com/my-site-key/`) |
|
||||||
|
| `CAP_SECRET_KEY` | Secret key from your Cap site dashboard (stored in `PROD_ENV_FILE` Gitea secret on prod) |
|
||||||
|
| `MERCURE_JWT_SECRET` | Random secret (generated in step 3) |
|
||||||
|
| `MERCURE_JWT_TOKEN` | Signed publisher JWT (generated in step 3) |
|
||||||
|
| `MERCURE_SUBSCRIBER_JWT` | Signed subscriber JWT (generated in step 3) |
|
||||||
|
| `MAILER_DSN` | `smtp://localhost:1025` for MailHog in dev |
|
||||||
|
|
||||||
|
### 3. Generate Mercure JWT tokens
|
||||||
|
|
||||||
|
```bash
|
||||||
|
composer install
|
||||||
|
make mercure-jwt
|
||||||
|
```
|
||||||
|
|
||||||
|
Copy the printed values into `.env` and into the `publisher_jwt` / `subscriber_jwt` lines of your Caddy Mercure block, then reload Caddy:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo systemctl reload caddy
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4a. Run with Docker
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make start-build
|
||||||
|
```
|
||||||
|
|
||||||
|
This installs PHP dependencies, builds the frontend assets with Bun, builds the Docker images, and starts all services. RustFS migration runs automatically once before the app and scheduler start.
|
||||||
|
|
||||||
|
The app is available at `http://localhost:10080` (or the domain set in `APP_PUBLIC_HOSTNAME`).
|
||||||
|
|
||||||
|
To apply any code changes later, run the same command again.
|
||||||
|
|
||||||
|
### 4b. Run bare-metal (development)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
composer install
|
||||||
|
bun install
|
||||||
|
bun run dev # Vite dev server with hot-reload
|
||||||
|
php bin/console doctrine:migrations:migrate --no-interaction
|
||||||
|
```
|
||||||
|
|
||||||
|
Start RustFS and MailHog, then open the URL configured in your Caddy vhost.
|
||||||
|
|
||||||
|
### 5. Object storage setup
|
||||||
|
|
||||||
|
**Docker** — `make start` and `make start-build` run the one-shot migration before starting the app. Production releases pause writers, migrate and verify all MinIO buckets, start the RustFS-backed app, then remove the MinIO container while retaining its volume. See [Object storage migration](docs/MINIO_INITIALIZATION.md).
|
||||||
|
|
||||||
|
**Bare-metal** — run once after MinIO is up:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mc alias set local http://localhost:9010 $MINIO_ROOT_USER $MINIO_ROOT_PASSWORD
|
||||||
|
mc mb local/mineseeker
|
||||||
|
echo '' | mc pipe local/mineseeker/media/.keep
|
||||||
|
echo '' | mc pipe local/mineseeker/cache/.keep
|
||||||
|
# Apply public-read policy for media/ and cache/ — see docker/minio-init.sh for the JSON
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Development environment
|
||||||
|
|
||||||
|
When running the Docker stack locally you typically want to catch outgoing emails instead of relaying them through a real SMTP server.
|
||||||
|
The production `compose.yaml` uses Postfix for actual mail delivery and must not be edited for local overrides.
|
||||||
|
Use a `compose.override.yaml` file instead — Docker Compose merges it automatically on top of the base file whenever both are present.
|
||||||
|
|
||||||
|
### Email: replace Postfix with MailHog
|
||||||
|
|
||||||
|
Create `compose.override.yaml` in the project root (it is git-ignored and never reaches production):
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
app:
|
||||||
|
environment:
|
||||||
|
MAILER_DSN: smtp://mail:1025?verify_peer=0
|
||||||
|
TRUSTED_PROXIES: "0.0.0.0/0"
|
||||||
|
mail:
|
||||||
|
image: mailhog/mailhog:latest
|
||||||
|
ports:
|
||||||
|
- "1025:1025"
|
||||||
|
- "8025:8025"
|
||||||
|
```
|
||||||
|
|
||||||
|
This replaces the `mail` service image with MailHog and points the application's mailer at its SMTP port (`1025`).
|
||||||
|
No other files need to change.
|
||||||
|
|
||||||
|
After adding the file, restart the stack:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make start-build
|
||||||
|
```
|
||||||
|
|
||||||
|
All emails sent by the application are now captured by MailHog.
|
||||||
|
Open the web UI at **http://localhost:8025** to inspect them.
|
||||||
|
|
||||||
|
> **Production note** — `compose.override.yaml` is listed in `.gitignore`.
|
||||||
|
> Never commit it; the production server must only see `compose.yaml` with Postfix.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Deploying to production
|
||||||
|
|
||||||
|
Releases are automated via Gitea Actions. Pushing a tag that starts with `v` (e.g. `v2026.01`) triggers the workflow at `.gitea/workflows/deploy.yml`.
|
||||||
|
The job runs on a **self-hosted runner** installed on the production server — the server only needs an outbound connection to Gitea, no open SSH port required.
|
||||||
|
The `app` image is rebuilt with the new code; the database and storage containers are untouched so all data is preserved.
|
||||||
|
|
||||||
|
### Gitea repository variables and secrets
|
||||||
|
|
||||||
|
**Variable** (plaintext, editable — **Repository → Settings → Variables**):
|
||||||
|
|
||||||
|
| Variable | Value |
|
||||||
|
|---|---|
|
||||||
|
| `PROD_APP_DIR` | Absolute path on the server (e.g. `/var/www/mineseeker`) |
|
||||||
|
|
||||||
|
**Secret** (encrypted, write-only — **Repository → Settings → Secrets**):
|
||||||
|
|
||||||
|
| Secret | Value |
|
||||||
|
|---|---|
|
||||||
|
| `PROD_ENV_FILE` | Full content of the production `.env` file (see below) |
|
||||||
|
|
||||||
|
The workflow writes `PROD_ENV_FILE` to `.env` on every deploy, so you never need to manage the file on the server manually. To update a credential, overwrite the secret in Gitea and push a new tag.
|
||||||
|
|
||||||
|
#### `PROD_ENV_FILE` contents
|
||||||
|
|
||||||
|
Paste the filled-in `.env` file as the secret value:
|
||||||
|
|
||||||
|
```dotenv
|
||||||
|
APP_NAME=mineseeker
|
||||||
|
APP_ENV=prod
|
||||||
|
APP_SECRET="<openssl rand -hex 32>"
|
||||||
|
|
||||||
|
DATABASE_URL=postgresql://POSTGRES_USER:POSTGRES_PASSWORD@db:5432/POSTGRES_DB?serverVersion=18&charset=utf8
|
||||||
|
|
||||||
|
POSTGRES_USER=mineseeker
|
||||||
|
POSTGRES_PASSWORD="<strong password>"
|
||||||
|
POSTGRES_DB=mineseeker
|
||||||
|
POSTGRES_VERSION=18
|
||||||
|
|
||||||
|
MINIO_ROOT_USER=mineseeker
|
||||||
|
MINIO_ROOT_PASSWORD="<strong password>"
|
||||||
|
MINIO_ENDPOINT=http://rustfs:9000
|
||||||
|
MINIO_PUBLIC_URL=https://aws.mineseeker.hu
|
||||||
|
|
||||||
|
MAILER_DSN=smtp://mail:25?verify_peer=0
|
||||||
|
MAIL_DOMAIN=mineseeker.hu
|
||||||
|
|
||||||
|
CAP_API_ENDPOINT="https://cap.example.com/your-site-key/"
|
||||||
|
CAP_SECRET_KEY="<secret key from your Cap site dashboard>"
|
||||||
|
|
||||||
|
MERCURE_URL=https://mineseeker.hu/.well-known/mercure
|
||||||
|
MERCURE_PUBLIC_URL=https://mineseeker.hu/.well-known/mercure
|
||||||
|
MERCURE_JWT_SECRET="<generated by make mercure-jwt>"
|
||||||
|
MERCURE_JWT_TOKEN="<generated by make mercure-jwt>"
|
||||||
|
MERCURE_SUBSCRIBER_JWT="<generated by make mercure-jwt>"
|
||||||
|
|
||||||
|
APP_PUBLIC_HOSTNAME=mineseeker.hu
|
||||||
|
WEBAUTHN_RP_ID=mineseeker.hu
|
||||||
|
WEBAUTHN_ORIGIN=https://mineseeker.hu
|
||||||
|
|
||||||
|
# OG Tags & Social Media Sharing (IMPORTANT for Docker deployments)
|
||||||
|
# TRUSTED_PROXIES: IP address (or range) of your reverse proxy (Caddy/Nginx)
|
||||||
|
# This ensures OG image tags use HTTPS URLs instead of HTTP
|
||||||
|
TRUSTED_PROXIES="172.18.0.0/16"
|
||||||
|
TRUSTED_HOSTS="mineseeker.hu,www.mineseeker.hu"
|
||||||
|
```
|
||||||
|
### Production server: one-time setup
|
||||||
|
|
||||||
|
The server needs Docker, Git, and a self-hosted `act_runner` registered against the Gitea repository. Bun and Composer run inside the multi-stage Dockerfile, so they are not needed on the server.
|
||||||
|
|
||||||
|
#### 1. Clone the repository
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://gitea.mineseeker.hu/youruser/mineseeker.git /var/www/mineseeker
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. Generate Mercure JWT tokens (run once locally)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
composer install # only needed for this step
|
||||||
|
make mercure-jwt
|
||||||
|
```
|
||||||
|
|
||||||
|
Copy the three printed values into the `PROD_ENV_FILE` secret.
|
||||||
|
|
||||||
|
#### 3. First deploy
|
||||||
|
|
||||||
|
Trigger it by pushing the first tag:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git tag v2026.01
|
||||||
|
git push origin v2026.01
|
||||||
|
```
|
||||||
|
|
||||||
|
This writes `.env`, builds the Docker images, runs database migrations, and—if the legacy MinIO container exists—pauses app writers, copies and verifies all its buckets in RustFS, then removes the MinIO container after the app is running on RustFS. The MinIO data volume is retained.
|
||||||
|
|
||||||
|
#### 4. Verify
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose ps # all services should be healthy/running
|
||||||
|
docker compose logs app # look for "Starting FrankenPHP"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Releasing
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git tag v2026.01
|
||||||
|
git push origin v2026.01
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
For detailed information about game mechanics, bonus systems, fonts, testing, and other technical details, see the [docs](./docs/) directory:
|
||||||
|
|
||||||
|
- **[AI Agent Guidelines](./AGENTS.md)** — Comprehensive guide for AI coding agents working on this project
|
||||||
|
- **[Bonus Points System](./docs/game-mechanics/BONUS_POINTS_SYSTEM.md)** — Complete reference for all bonus point types, calculation rules, and implementation details
|
||||||
|
- **[Fonts](./docs/FONTS.md)** — TrueType fonts used for server-side image generation
|
||||||
|
- **[Testing Guide](./docs/testing/TESTING.md)** — Complete testing setup with Foundry factories, database isolation, and best practices
|
||||||
|
- **[Factory Documentation](./docs/testing/FACTORIES.md)** — Detailed API reference for all test data factories
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
LGPL-3.0 — see [LICENSE](LICENSE) for details.
|
||||||
|
|
||||||
|
© 2026 [SplendidBear](https://www.splendidbear.org)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Testing & CI/CD
|
||||||
|
|
||||||
|
MineSeeker has a comprehensive test suite with **71 automated tests** and continuous integration/deployment pipelines.
|
||||||
|
|
||||||
|
### Quick Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Setup test database (first time only)
|
||||||
|
make test-db-setup
|
||||||
|
|
||||||
|
# Run all tests
|
||||||
|
make test
|
||||||
|
|
||||||
|
# Run with documentation output
|
||||||
|
vendor/bin/phpunit --testdox
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test Suite
|
||||||
|
|
||||||
|
- **71 tests** with **227 assertions**
|
||||||
|
- **Controller tests** - HTTP endpoints, authentication, routing
|
||||||
|
- **DTO tests** - Data serialization and calculations
|
||||||
|
- **Entity tests** - Domain logic and defaults
|
||||||
|
- **Service tests** - Business logic and external APIs
|
||||||
|
- **Integration tests** - Foundry factories and database isolation
|
||||||
|
|
||||||
|
**Test execution time:** ~6-8 seconds
|
||||||
|
|
||||||
|
### Continuous Integration
|
||||||
|
|
||||||
|
**Automated testing** runs on every push/pull request:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# .gitea/workflows/ci.yml-bak
|
||||||
|
✓ PHP 8.3 setup with all extensions
|
||||||
|
✓ PostgreSQL 18 service container
|
||||||
|
✓ Composer and npm dependency installation
|
||||||
|
✓ Asset building with Vite
|
||||||
|
✓ Database migrations
|
||||||
|
✓ Full test suite execution
|
||||||
|
✓ Code linting (ESLint, PHP-CS-Fixer)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Continuous Deployment
|
||||||
|
|
||||||
|
**Automated deployment** on version tags (e.g., `v1.2.3`):
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# .gitea/workflows/deploy.yml
|
||||||
|
1. Run full test suite (blocks deployment if fails)
|
||||||
|
2. Checkout tagged version
|
||||||
|
3. Build Docker image
|
||||||
|
4. Run database migrations
|
||||||
|
5. Restart services
|
||||||
|
6. Health check verification
|
||||||
|
```
|
||||||
|
|
||||||
|
**Deploy to production:**
|
||||||
|
```bash
|
||||||
|
git tag -a v1.2.3 -m "Release version 1.2.3"
|
||||||
|
git push origin v1.2.3
|
||||||
|
```
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
|
||||||
|
- **[Testing Guide](docs/testing/TESTING.md)** - Comprehensive testing documentation
|
||||||
|
- **[Factory Reference](docs/testing/FACTORIES.md)** - Foundry factory API
|
||||||
|
- **[CI/CD Guide](docs/CI_CD.md)** - Pipeline configuration and workflows
|
||||||
|
|
||||||
|
---
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
<IfModule mod_authz_core.c>
|
|
||||||
Require all denied
|
|
||||||
</IfModule>
|
|
||||||
<IfModule !mod_authz_core.c>
|
|
||||||
Order deny,allow
|
|
||||||
Deny from all
|
|
||||||
</IfModule>
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
use Symfony\Bundle\FrameworkBundle\HttpCache\HttpCache;
|
|
||||||
|
|
||||||
class AppCache extends HttpCache
|
|
||||||
{
|
|
||||||
}
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
use Symfony\Component\HttpKernel\Kernel;
|
|
||||||
use Symfony\Component\Config\Loader\LoaderInterface;
|
|
||||||
|
|
||||||
class AppKernel extends Kernel
|
|
||||||
{
|
|
||||||
public function registerBundles()
|
|
||||||
{
|
|
||||||
$bundles = [
|
|
||||||
new Symfony\Bundle\FrameworkBundle\FrameworkBundle(),
|
|
||||||
new Symfony\Bundle\SecurityBundle\SecurityBundle(),
|
|
||||||
new Symfony\Bundle\TwigBundle\TwigBundle(),
|
|
||||||
new Symfony\Bundle\MonologBundle\MonologBundle(),
|
|
||||||
new Symfony\Bundle\SwiftmailerBundle\SwiftmailerBundle(),
|
|
||||||
new Doctrine\Bundle\DoctrineBundle\DoctrineBundle(),
|
|
||||||
new Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle(),
|
|
||||||
|
|
||||||
new Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle(),
|
|
||||||
new Symfony\Bundle\AsseticBundle\AsseticBundle(),
|
|
||||||
new FOS\UserBundle\FOSUserBundle(),
|
|
||||||
new HWI\Bundle\OAuthBundle\HWIOAuthBundle(),
|
|
||||||
new Gos\Bundle\WebSocketBundle\GosWebSocketBundle(),
|
|
||||||
new Gos\Bundle\PubSubRouterBundle\GosPubSubRouterBundle(),
|
|
||||||
new Doctrine\Bundle\DoctrineCacheBundle\DoctrineCacheBundle(),
|
|
||||||
new Snc\RedisBundle\SncRedisBundle(),
|
|
||||||
new CL\Bundle\SlackBundle\CLSlackBundle(),
|
|
||||||
|
|
||||||
new Jotunheimr\AdminBundle\JotunheimrAdminBundle(),
|
|
||||||
new Jotunheimr\UserBundle\JotunheimrUserBundle(),
|
|
||||||
new Mine\SeekerBundle\MineSeekerBundle(),
|
|
||||||
];
|
|
||||||
|
|
||||||
if (in_array($this->getEnvironment(), ['dev', 'test'], true)) {
|
|
||||||
$bundles[] = new Symfony\Bundle\DebugBundle\DebugBundle();
|
|
||||||
$bundles[] = new Symfony\Bundle\WebProfilerBundle\WebProfilerBundle();
|
|
||||||
$bundles[] = new Sensio\Bundle\DistributionBundle\SensioDistributionBundle();
|
|
||||||
$bundles[] = new Sensio\Bundle\GeneratorBundle\SensioGeneratorBundle();
|
|
||||||
}
|
|
||||||
|
|
||||||
return $bundles;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getRootDir()
|
|
||||||
{
|
|
||||||
return __DIR__;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getCacheDir()
|
|
||||||
{
|
|
||||||
return dirname(__DIR__).'/var/cache/'.$this->getEnvironment();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getLogDir()
|
|
||||||
{
|
|
||||||
return dirname(__DIR__).'/var/logs';
|
|
||||||
}
|
|
||||||
|
|
||||||
public function registerContainerConfiguration(LoaderInterface $loader)
|
|
||||||
{
|
|
||||||
$loader->load($this->getRootDir().'/config/config_'.$this->getEnvironment().'.yml');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta http-equiv="Cache-control" content="max-age=1209600;public">
|
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
||||||
<meta name="mobile-web-app-capable" content="yes">
|
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
|
||||||
<meta name="keywords" content="game,mineseeker,mine,seeker,laszlolang.com">
|
|
||||||
<meta name="robots" content="index,follow">
|
|
||||||
<meta name="revisit-after" content="2 days">
|
|
||||||
<meta name="resource-type" content="document">
|
|
||||||
<meta name="country" content="Hungary">
|
|
||||||
<meta name="description" content="This is a new minesweeper, multiplayer game.">
|
|
||||||
<meta name="content-language" content="hu,hun,hungarian">
|
|
||||||
{% include '@MineSeeker/Recent/favicon.html.twig' %}
|
|
||||||
<meta property="fb:app_id" content="{{ facebook_api }}">
|
|
||||||
{% block metas %}{% endblock %}
|
|
||||||
<title>MineSeeker{% block title %}{% endblock %}</title>
|
|
||||||
{% block stylesheets %}{% endblock %}
|
|
||||||
{% include '@MineSeeker/Recent/google-analytics.html.twig' %}
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<div id="fb-root"></div>
|
|
||||||
|
|
||||||
{% block bodyTop %}{% endblock %}
|
|
||||||
|
|
||||||
<header>
|
|
||||||
{% block header %}{% endblock %}
|
|
||||||
</header>
|
|
||||||
<main>
|
|
||||||
{% block body %}{% endblock %}
|
|
||||||
</main>
|
|
||||||
<footer>
|
|
||||||
{% block footer %}{% endblock %}
|
|
||||||
</footer>
|
|
||||||
|
|
||||||
{% block javascripts %}
|
|
||||||
{% javascripts filter='?uglifyjs2'
|
|
||||||
'@JotunheimrAdminBundle/Resources/public/js/vendor/plugins/jQuery/jquery-3.0.0.min.js'
|
|
||||||
'@JotunheimrAdminBundle/Resources/public/js/vendor/plugins/jQuery/jquery-migrate-3.0.0.min.js'
|
|
||||||
'@JotunheimrAdminBundle/Resources/public/js/vendor/bootstrap/js/bootstrap.min.js' %}
|
|
||||||
<script type="text/javascript" src="{{ asset_url }}"></script>
|
|
||||||
{% endjavascripts %}
|
|
||||||
|
|
||||||
{% include '@MineSeeker/Recent/facebook.html.twig' %}
|
|
||||||
{% endblock %}
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
use Doctrine\Common\Annotations\AnnotationRegistry;
|
|
||||||
use Composer\Autoload\ClassLoader;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @var ClassLoader $loader
|
|
||||||
*/
|
|
||||||
$loader = require __DIR__.'/../vendor/autoload.php';
|
|
||||||
|
|
||||||
AnnotationRegistry::registerLoader([$loader, 'loadClass']);
|
|
||||||
|
|
||||||
return $loader;
|
|
||||||
@@ -1,112 +0,0 @@
|
|||||||
imports:
|
|
||||||
- { resource: security.yml }
|
|
||||||
- { resource: services.yml }
|
|
||||||
- { resource: "@JotunheimrAdminBundle/Resources/config/config.yml" }
|
|
||||||
- { resource: "@JotunheimrUserBundle/Resources/config/config.yml" }
|
|
||||||
- { resource: "@MineSeekerBundle/Resources/config/config.yml" }
|
|
||||||
- { resource: "@MineSeekerBundle/Resources/config/services.yml" }
|
|
||||||
|
|
||||||
# Put parameters here that don't need to change on each machine where the app is deployed
|
|
||||||
# http://symfony.com/doc/current/best_practices/configuration.html#application-related-configuration
|
|
||||||
parameters:
|
|
||||||
locale: en
|
|
||||||
mailer_user: 7system7@gmail.com
|
|
||||||
mailer_name: Lang
|
|
||||||
|
|
||||||
framework:
|
|
||||||
#esi: ~
|
|
||||||
translator: { fallbacks: ["%locale%"] }
|
|
||||||
secret: "%secret%"
|
|
||||||
router:
|
|
||||||
resource: "%kernel.root_dir%/config/routing.yml"
|
|
||||||
strict_requirements: ~
|
|
||||||
form: ~
|
|
||||||
csrf_protection: ~
|
|
||||||
validation: { enable_annotations: true }
|
|
||||||
#serializer: { enable_annotations: true }
|
|
||||||
templating:
|
|
||||||
engines: ['twig']
|
|
||||||
default_locale: "%locale%"
|
|
||||||
trusted_hosts: ~
|
|
||||||
trusted_proxies: ~
|
|
||||||
session: ~
|
|
||||||
# http://symfony.com/doc/current/reference/configuration/framework.html#handler-id
|
|
||||||
# handler_id: session.handler.native_file
|
|
||||||
# save_path: "%kernel.root_dir%/../var/sessions/%kernel.environment%"
|
|
||||||
# handler_id: session.handler.pdo
|
|
||||||
fragments: ~
|
|
||||||
http_method_override: true
|
|
||||||
assets: ~
|
|
||||||
|
|
||||||
# Twig Configuration
|
|
||||||
twig:
|
|
||||||
debug: "%kernel.debug%"
|
|
||||||
strict_variables: "%kernel.debug%"
|
|
||||||
globals:
|
|
||||||
version: "0.37.18 (beta7)"
|
|
||||||
facebook_api: "%facebook.api%"
|
|
||||||
facebook_scope: "%facebook.scope%"
|
|
||||||
facebook_api_version: "%facebook.version%"
|
|
||||||
|
|
||||||
# Doctrine Configuration
|
|
||||||
doctrine:
|
|
||||||
dbal:
|
|
||||||
driver: pdo_mysql
|
|
||||||
host: "%database_host%"
|
|
||||||
port: "%database_port%"
|
|
||||||
dbname: "%database_name%"
|
|
||||||
user: "%database_user%"
|
|
||||||
password: "%database_password%"
|
|
||||||
charset: UTF8
|
|
||||||
# if using pdo_sqlite as your database driver:
|
|
||||||
# 1. add the path in parameters.yml
|
|
||||||
# e.g. database_path: "%kernel.root_dir%/data/data.db3"
|
|
||||||
# 2. Uncomment database_path in parameters.yml.dist
|
|
||||||
# 3. Uncomment next line:
|
|
||||||
# path: "%database_path%"
|
|
||||||
|
|
||||||
orm:
|
|
||||||
auto_generate_proxy_classes: "%kernel.debug%"
|
|
||||||
naming_strategy: doctrine.orm.naming_strategy.underscore
|
|
||||||
auto_mapping: true
|
|
||||||
|
|
||||||
# Assetic Configuration
|
|
||||||
assetic:
|
|
||||||
debug: "%kernel.debug%"
|
|
||||||
use_controller: "%kernel.debug%"
|
|
||||||
bundles: ~
|
|
||||||
node: /usr/bin/nodejs
|
|
||||||
filters:
|
|
||||||
cssrewrite: ~
|
|
||||||
uglifyjs2:
|
|
||||||
bin: "%kernel.root_dir%/../node_modules/uglify-js/bin/uglifyjs"
|
|
||||||
no_copyright: true
|
|
||||||
uglifycss:
|
|
||||||
bin: "%kernel.root_dir%/../node_modules/uglifycss/uglifycss"
|
|
||||||
|
|
||||||
# FOS User Configuration
|
|
||||||
fos_user:
|
|
||||||
db_driver: orm # other valid values are 'mongodb', 'couchdb' and 'propel'
|
|
||||||
firewall_name: secured_area
|
|
||||||
user_class: Jotunheimr\UserBundle\Entity\User
|
|
||||||
from_email:
|
|
||||||
address: "%mailer_user%"
|
|
||||||
sender_name: "%mailer_name%"
|
|
||||||
|
|
||||||
# Facebook OAuth
|
|
||||||
hwi_oauth:
|
|
||||||
firewall_names: [secured_area]
|
|
||||||
resource_owners:
|
|
||||||
facebook:
|
|
||||||
type: facebook
|
|
||||||
client_id: "%facebook.api%"
|
|
||||||
client_secret: "%facebook.api-secret%"
|
|
||||||
scope: "%facebook.scope%"
|
|
||||||
options:
|
|
||||||
display: popup
|
|
||||||
auth_type: rerequest
|
|
||||||
csrf: true
|
|
||||||
|
|
||||||
# Slack integration
|
|
||||||
cl_slack:
|
|
||||||
api_token: xoxp-107639806167-107029084564-115427085733-cccaa4f96c89c87ce680c7f22acfd001
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
imports:
|
|
||||||
- { resource: parameters_dev.yml }
|
|
||||||
- { resource: config.yml }
|
|
||||||
|
|
||||||
framework:
|
|
||||||
router:
|
|
||||||
resource: "%kernel.root_dir%/config/routing_dev.yml"
|
|
||||||
strict_requirements: true
|
|
||||||
profiler: { only_exceptions: false }
|
|
||||||
|
|
||||||
web_profiler:
|
|
||||||
toolbar: true
|
|
||||||
intercept_redirects: false
|
|
||||||
|
|
||||||
|
|
||||||
# Swiftmailer Configuration
|
|
||||||
swiftmailer:
|
|
||||||
transport: "%mailer_transport%"
|
|
||||||
host: "%mailer_host%"
|
|
||||||
username: "%mailer_user%"
|
|
||||||
password: "%mailer_password%"
|
|
||||||
spool: { type: memory }
|
|
||||||
|
|
||||||
monolog:
|
|
||||||
handlers:
|
|
||||||
main:
|
|
||||||
type: stream
|
|
||||||
path: "%kernel.logs_dir%/%kernel.environment%.log"
|
|
||||||
level: debug
|
|
||||||
channels: [!event]
|
|
||||||
console:
|
|
||||||
type: console
|
|
||||||
channels: [!event, !doctrine]
|
|
||||||
|
|
||||||
parameters:
|
|
||||||
facebook.api: 320599508311862
|
|
||||||
facebook.api-secret: 18d4f48cdd274bccee2678e5eff3f557
|
|
||||||
facebook.version: 'v2.8'
|
|
||||||
facebook.scope: 'public_profile,email,user_friends'
|
|
||||||
mineseeker.websocket: 6450
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
imports:
|
|
||||||
- { resource: parameters_prod.yml }
|
|
||||||
- { resource: config.yml }
|
|
||||||
|
|
||||||
# Swiftmailer Configuration
|
|
||||||
swiftmailer:
|
|
||||||
transport: "%mailer_transport%"
|
|
||||||
host: "%mailer_host%"
|
|
||||||
username: "%mailer_user%"
|
|
||||||
password: "%mailer_password%"
|
|
||||||
spool: { type: memory }
|
|
||||||
|
|
||||||
monolog:
|
|
||||||
handlers:
|
|
||||||
main:
|
|
||||||
type: fingers_crossed
|
|
||||||
action_level: error
|
|
||||||
handler: nested
|
|
||||||
nested:
|
|
||||||
type: stream
|
|
||||||
path: "%kernel.logs_dir%/%kernel.environment%.log"
|
|
||||||
level: debug
|
|
||||||
console:
|
|
||||||
type: console
|
|
||||||
|
|
||||||
parameters:
|
|
||||||
facebook.api: 320597498312063
|
|
||||||
facebook.api-secret: c751bec8a3c5313ff2e5a83769bf1109
|
|
||||||
facebook.version: 'v2.8'
|
|
||||||
facebook.scope: 'public_profile,email,user_friends'
|
|
||||||
mineseeker.websocket: 8080
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
imports:
|
|
||||||
- { resource: config_dev.yml }
|
|
||||||
|
|
||||||
framework:
|
|
||||||
test: ~
|
|
||||||
session:
|
|
||||||
storage_id: session.storage.mock_file
|
|
||||||
profiler:
|
|
||||||
collect: false
|
|
||||||
|
|
||||||
web_profiler:
|
|
||||||
toolbar: false
|
|
||||||
intercept_redirects: false
|
|
||||||
|
|
||||||
swiftmailer:
|
|
||||||
disable_delivery: true
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
# This file is a "template" of what your parameters.yml file should look like
|
|
||||||
# Set parameters here that may be different on each deployment target of the app, e.g. development, staging, production.
|
|
||||||
# http://symfony.com/doc/current/best_practices/configuration.html#infrastructure-related-configuration
|
|
||||||
parameters:
|
|
||||||
database_host: 127.0.0.1
|
|
||||||
database_port: ~
|
|
||||||
database_name: symfony
|
|
||||||
database_user: root
|
|
||||||
database_password: ~
|
|
||||||
# You should uncomment this if you want use pdo_sqlite
|
|
||||||
# database_path: "%kernel.root_dir%/data.db3"
|
|
||||||
|
|
||||||
mailer_transport: smtp
|
|
||||||
mailer_host: 127.0.0.1
|
|
||||||
mailer_user: ~
|
|
||||||
mailer_password: ~
|
|
||||||
|
|
||||||
# A secret key that's used to generate certain security-related tokens
|
|
||||||
secret: ThisTokenIsNotSoSecretChangeIt
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
# This file is auto-generated during the composer install
|
|
||||||
parameters:
|
|
||||||
database_host: 127.0.0.1
|
|
||||||
database_port: null
|
|
||||||
database_name: mine
|
|
||||||
database_user: root
|
|
||||||
database_password: ~
|
|
||||||
mailer_transport: smtp
|
|
||||||
mailer_host: 127.0.0.1
|
|
||||||
mailer_user: null
|
|
||||||
mailer_password: null
|
|
||||||
secret: bbcd5df99fc340558fb3995c198a9b4764db72ba
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
# This file is auto-generated during the composer install
|
|
||||||
parameters:
|
|
||||||
database_host: 127.0.0.1
|
|
||||||
database_port: null
|
|
||||||
database_name: mine
|
|
||||||
database_user: root
|
|
||||||
database_password: 'bazmeg'
|
|
||||||
mailer_transport: smtp
|
|
||||||
mailer_host: 127.0.0.1
|
|
||||||
mailer_user: null
|
|
||||||
mailer_password: null
|
|
||||||
secret: e25d036bb9c7ece0f2049984a1fa2f0cab295aaa
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
fos_user:
|
|
||||||
resource: "@FOSUserBundle/Resources/config/routing/all.xml"
|
|
||||||
|
|
||||||
JotunheimrUserBundle:
|
|
||||||
resource: "@JotunheimrUserBundle/Resources/config/routing.yml"
|
|
||||||
prefix: /
|
|
||||||
|
|
||||||
JotunheimrAdminBundle:
|
|
||||||
resource: "@JotunheimrAdminBundle/Resources/config/routing.yml"
|
|
||||||
prefix: /
|
|
||||||
|
|
||||||
MineSeekerBundle:
|
|
||||||
resource: "@MineSeekerBundle/Resources/config/routing.yml"
|
|
||||||
prefix: /
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
_wdt:
|
|
||||||
resource: "@WebProfilerBundle/Resources/config/routing/wdt.xml"
|
|
||||||
prefix: /_wdt
|
|
||||||
|
|
||||||
_profiler:
|
|
||||||
resource: "@WebProfilerBundle/Resources/config/routing/profiler.xml"
|
|
||||||
prefix: /_profiler
|
|
||||||
|
|
||||||
_errors:
|
|
||||||
resource: "@TwigBundle/Resources/config/routing/errors.xml"
|
|
||||||
prefix: /_error
|
|
||||||
|
|
||||||
_main:
|
|
||||||
resource: routing.yml
|
|
||||||
schemes: [http]
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
security:
|
|
||||||
encoders:
|
|
||||||
FOS\UserBundle\Model\UserInterface: bcrypt
|
|
||||||
|
|
||||||
role_hierarchy:
|
|
||||||
ROLE_ADMIN: ROLE_USER
|
|
||||||
ROLE_SUPER_ADMIN: ROLE_ADMIN
|
|
||||||
|
|
||||||
providers:
|
|
||||||
fos_userbundle:
|
|
||||||
id: fos_user.user_provider.username_email
|
|
||||||
|
|
||||||
firewalls:
|
|
||||||
secured_area:
|
|
||||||
anonymous: ~
|
|
||||||
oauth:
|
|
||||||
resource_owners:
|
|
||||||
facebook: /login/check-facebook
|
|
||||||
google: /login/check-google
|
|
||||||
my_github: /login/check-github
|
|
||||||
login_path: /login
|
|
||||||
failure_path: /login
|
|
||||||
use_forward: false
|
|
||||||
oauth_user_provider:
|
|
||||||
service: jotun.user_provider
|
|
||||||
remember_me:
|
|
||||||
secret: "%secret%"
|
|
||||||
lifetime: 604800
|
|
||||||
path: /
|
|
||||||
domain: ~
|
|
||||||
user_provider: fos_userbundle
|
|
||||||
form_login:
|
|
||||||
provider: fos_userbundle
|
|
||||||
csrf_token_generator: security.csrf.token_manager
|
|
||||||
default_target_path: /
|
|
||||||
remember_me: true
|
|
||||||
logout:
|
|
||||||
path: /logout
|
|
||||||
target: /
|
|
||||||
|
|
||||||
access_control:
|
|
||||||
- { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
|
|
||||||
- { path: ^/register, role: IS_AUTHENTICATED_ANONYMOUSLY }
|
|
||||||
- { path: ^/resetting, role: IS_AUTHENTICATED_ANONYMOUSLY }
|
|
||||||
- { path: ^/play, role: IS_AUTHENTICATED_ANONYMOUSLY }
|
|
||||||
- { path: ^/admin, role: ROLE_SUPER_ADMIN }
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
# Learn more about services, parameters and containers at
|
|
||||||
# http://symfony.com/doc/current/book/service_container.html
|
|
||||||
parameters:
|
|
||||||
# parameter_name: value
|
|
||||||
|
|
||||||
services:
|
|
||||||
# service_name:
|
|
||||||
# class: AppBundle\Directory\ClassName
|
|
||||||
# arguments: ["@another_service_name", "plain_value", "%parameter_name%"]
|
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
/*!*
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
@import '@fortawesome/fontawesome-free/css/all.min.css';
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
/*!*
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
@import '@fontsource/rajdhani';
|
||||||
|
@import '@fontsource/changa-one';
|
||||||
|
@import '@fontsource/open-sans/700';
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
/*!*
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
@keyframes appear {
|
||||||
|
from { opacity: 0; transform: scale(0.94); }
|
||||||
|
to { opacity: 1; transform: scale(1); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes rise {
|
||||||
|
from { opacity: 0; transform: translateY(16px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
/*!*
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#hero-auth {
|
||||||
|
padding: 20px;
|
||||||
|
|
||||||
|
.hero-auth {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 10px;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-auth-user {
|
||||||
|
font: 600 13px 'Rajdhani', sans-serif;
|
||||||
|
color: rgba(149, 207, 245, 0.75);
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
|
||||||
|
i { font-size: 15px; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 1100px) {
|
||||||
|
.hero-auth {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-auth-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
font: 600 12px 'Rajdhani', sans-serif;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 1.5px;
|
||||||
|
text-decoration: none;
|
||||||
|
color: rgba(255, 255, 255, 0.6);
|
||||||
|
background: rgba(255, 255, 255, 0.06);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 7px 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 200ms ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #fff;
|
||||||
|
background: rgba(255, 255, 255, 0.12);
|
||||||
|
border-color: rgba(255, 255, 255, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
&--register {
|
||||||
|
color: rgba(149, 207, 245, 0.8);
|
||||||
|
border-color: rgba(35, 111, 135, 0.4);
|
||||||
|
background: rgba(35, 111, 135, 0.12);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #fff;
|
||||||
|
background: rgba(35, 111, 135, 0.28);
|
||||||
|
border-color: rgba(149, 207, 245, 0.5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&--security {
|
||||||
|
color: rgba(149, 207, 245, 0.55);
|
||||||
|
border-color: rgba(35, 111, 135, 0.22);
|
||||||
|
background: transparent;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: rgba(149, 207, 245, 0.9);
|
||||||
|
background: rgba(35, 111, 135, 0.14);
|
||||||
|
border-color: rgba(35, 111, 135, 0.45);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&--out {
|
||||||
|
background: transparent;
|
||||||
|
border-color: rgba(173, 10, 5, 0.3);
|
||||||
|
color: rgba(246, 125, 82, 0.7);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(173, 10, 5, 0.15);
|
||||||
|
border-color: rgba(246, 125, 82, 0.5);
|
||||||
|
color: #f67d52;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&--profile {
|
||||||
|
color: rgba(149, 207, 245, 0.8);
|
||||||
|
border-color: rgba(35, 111, 135, 0.35);
|
||||||
|
background: rgba(35, 111, 135, 0.08);
|
||||||
|
text-decoration: none;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #fff;
|
||||||
|
background: rgba(35, 111, 135, 0.22);
|
||||||
|
border-color: rgba(149, 207, 245, 0.5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-auth-avatar {
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
border-radius: 50%;
|
||||||
|
object-fit: cover;
|
||||||
|
flex-shrink: 0;
|
||||||
|
border: 1px solid rgba(35, 111, 135, 0.5);
|
||||||
|
|
||||||
|
&--initials {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: linear-gradient(135deg, rgba(35, 111, 135, 0.45) 0%, rgba(173, 10, 5, 0.3) 100%);
|
||||||
|
font: 800 9px 'Rajdhani', sans-serif;
|
||||||
|
color: rgba(149, 207, 245, 0.9);
|
||||||
|
letter-spacing: 1px;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,533 @@
|
|||||||
|
/*!*
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.auth-page {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
padding: 40px 20px 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-flash {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 420px;
|
||||||
|
padding: 12px 18px;
|
||||||
|
border-radius: 5px;
|
||||||
|
font: 600 14px 'Rajdhani', sans-serif;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
&--success {
|
||||||
|
background: rgba(26, 104, 68, 0.25);
|
||||||
|
border: 1px solid rgba(42, 158, 96, 0.4);
|
||||||
|
color: #a0f0c0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--error {
|
||||||
|
background: rgba(173, 10, 5, 0.18);
|
||||||
|
border: 1px solid rgba(173, 10, 5, 0.4);
|
||||||
|
color: #f6a090;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// "Check your inbox" confirmation card
|
||||||
|
.auth-card--sent {
|
||||||
|
text-align: center;
|
||||||
|
padding: 48px 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-sent-icon {
|
||||||
|
font-size: 52px;
|
||||||
|
color: rgba(149, 207, 245, 0.6);
|
||||||
|
margin-bottom: 20px;
|
||||||
|
filter: drop-shadow(0 0 16px rgba(35, 111, 135, 0.4));
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-sent-email {
|
||||||
|
font: 700 16px 'Rajdhani', sans-serif;
|
||||||
|
color: #95cff5;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
margin: 0 0 20px;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-sent-note {
|
||||||
|
font: 400 14px 'Rajdhani', sans-serif;
|
||||||
|
color: rgba(255, 255, 255, 0.5);
|
||||||
|
line-height: 1.7;
|
||||||
|
margin-bottom: 0;
|
||||||
|
|
||||||
|
strong { color: rgba(255, 255, 255, 0.75); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-card {
|
||||||
|
background: rgba(255, 255, 255, 0.03);
|
||||||
|
border: 1px solid rgba(35, 111, 135, 0.2);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 44px 48px 40px;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 520px;
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
|
box-shadow: 0 8px 48px rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-title {
|
||||||
|
font: 800 30px 'Rajdhani', sans-serif;
|
||||||
|
color: #ffffff;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-title-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
|
||||||
|
.auth-title {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-cap-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 4px 10px;
|
||||||
|
border-radius: 999px;
|
||||||
|
border: 1px solid rgba(149, 207, 245, 0.35);
|
||||||
|
background:
|
||||||
|
linear-gradient(115deg, transparent 0 38%, rgba(255, 255, 255, 0.18) 48%, transparent 58% 100%),
|
||||||
|
rgba(35, 111, 135, 0.2);
|
||||||
|
background-position: 120% 50%, 0 0;
|
||||||
|
background-size: 260% 100%, auto;
|
||||||
|
color: #95cff5;
|
||||||
|
font: 600 11px 'Rajdhani', sans-serif;
|
||||||
|
letter-spacing: 0.8px;
|
||||||
|
text-decoration: none;
|
||||||
|
text-transform: uppercase;
|
||||||
|
transition:
|
||||||
|
background-position 650ms ease,
|
||||||
|
border-color 200ms ease,
|
||||||
|
box-shadow 200ms ease,
|
||||||
|
color 200ms ease,
|
||||||
|
transform 200ms ease;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:focus-visible {
|
||||||
|
background-position: -60% 50%, 0 0;
|
||||||
|
border-color: rgba(149, 207, 245, 0.65);
|
||||||
|
box-shadow: 0 0 18px rgba(35, 111, 135, 0.28);
|
||||||
|
color: #d5f1ff;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:focus,
|
||||||
|
&:active,
|
||||||
|
&:visited {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
outline: 2px solid rgba(149, 207, 245, 0.75);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-sub {
|
||||||
|
font: 400 14px 'Rajdhani', sans-serif;
|
||||||
|
color: rgba(149, 207, 245, 0.6);
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-error {
|
||||||
|
background: rgba(173, 10, 5, 0.18);
|
||||||
|
border: 1px solid rgba(173, 10, 5, 0.4);
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 10px 14px;
|
||||||
|
font: 600 13px 'Rajdhani', sans-serif;
|
||||||
|
color: #f6a090;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-field {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-label {
|
||||||
|
font: 700 11px 'Rajdhani', sans-serif;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
color: rgba(255, 255, 255, 0.45);
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-input-wrap {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-input-icon {
|
||||||
|
position: absolute;
|
||||||
|
left: 11px;
|
||||||
|
color: rgba(149, 207, 245, 0.4);
|
||||||
|
font-size: 13px;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-input {
|
||||||
|
width: 100%;
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
border: 1px solid rgba(35, 111, 135, 0.3);
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 11px 14px 11px 34px;
|
||||||
|
font: 500 15px 'Rajdhani', sans-serif;
|
||||||
|
color: #ffffff;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
transition: border-color 200ms ease, background 200ms ease;
|
||||||
|
|
||||||
|
&::placeholder { color: rgba(255, 255, 255, 0.2); }
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
background: rgba(35, 111, 135, 0.1);
|
||||||
|
border-color: rgba(149, 207, 245, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
&--error {
|
||||||
|
border-color: rgba(173, 10, 5, 0.6) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-field-error {
|
||||||
|
font: 500 12px 'Rajdhani', sans-serif;
|
||||||
|
color: #f6a090;
|
||||||
|
letter-spacing: 0.3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-below-password {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-remember {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font: 500 13px 'Rajdhani', sans-serif;
|
||||||
|
color: rgba(255, 255, 255, 0.45);
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
margin-top: -4px;
|
||||||
|
|
||||||
|
input[type="checkbox"] { accent-color: #236f87; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-checkbox {
|
||||||
|
accent-color: #236f87;
|
||||||
|
cursor: pointer;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-checkbox-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
font: 400 14px 'Rajdhani', sans-serif;
|
||||||
|
color: rgba(255, 255, 255, 0.6);
|
||||||
|
line-height: 1.5;
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #95cff5;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: color 180ms;
|
||||||
|
|
||||||
|
&:hover { color: #c5e8ff; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea.auth-input {
|
||||||
|
padding: 11px 14px;
|
||||||
|
min-height: 120px;
|
||||||
|
resize: vertical;
|
||||||
|
font-family: 'Rajdhani', sans-serif;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-submit {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
background: linear-gradient(to bottom, #ad0a05 0%, #d4401a 55%, #f67d52 100%);
|
||||||
|
border: 1px solid rgba(246, 125, 82, 0.3);
|
||||||
|
border-radius: 5px;
|
||||||
|
color: #ffffff;
|
||||||
|
font: 700 16px 'Rajdhani', sans-serif;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 3px;
|
||||||
|
padding: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-top: 6px;
|
||||||
|
box-shadow: 0 4px 20px rgba(173, 10, 5, 0.35);
|
||||||
|
transition: all 220ms ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: linear-gradient(to bottom, #c91008 0%, #e5521e 55%, #ff8c61 100%);
|
||||||
|
box-shadow: 0 6px 28px rgba(173, 10, 5, 0.6);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active { transform: translateY(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-cancel {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
background: linear-gradient(to bottom, #1a1a1a 0%, #2d2d2d 55%, #404040 100%);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
border-radius: 5px;
|
||||||
|
color: #ffffff;
|
||||||
|
font: 700 16px 'Rajdhani', sans-serif;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 3px;
|
||||||
|
padding: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-top: 6px;
|
||||||
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.35);
|
||||||
|
transition: all 220ms ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: linear-gradient(to bottom, #2d2d2d 0%, #3d3d3d 55%, #505050 100%);
|
||||||
|
box-shadow: 0 6px 28px rgba(0, 0, 0, 0.6);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active { transform: translateY(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
width: 100%;
|
||||||
|
align-items: flex-start;
|
||||||
|
|
||||||
|
form {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
&.auth-form {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-cancel-form {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
.auth-cancel {
|
||||||
|
flex: 1;
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-cancel-standalone {
|
||||||
|
margin-top: 12px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-cancel--block {
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-switch {
|
||||||
|
font: 400 13px 'Rajdhani', sans-serif;
|
||||||
|
color: rgba(255, 255, 255, 0.35);
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 24px;
|
||||||
|
letter-spacing: 0.3px;
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #95cff5;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: color 180ms;
|
||||||
|
|
||||||
|
&:hover { color: #c5e8ff; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-forgot-password {
|
||||||
|
font: 400 13px 'Rajdhani', sans-serif;
|
||||||
|
color: rgba(255, 255, 255, 0.35);
|
||||||
|
text-align: center;
|
||||||
|
letter-spacing: 0.3px;
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #95cff5;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: color 180ms;
|
||||||
|
|
||||||
|
&:hover { color: #c5e8ff; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-divider {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin: 20px 0;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-divider::before,
|
||||||
|
.auth-divider::after {
|
||||||
|
content: '';
|
||||||
|
flex: 1;
|
||||||
|
height: 1px;
|
||||||
|
background: #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-divider span {
|
||||||
|
margin: 0 10px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-passkey-btn {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px 16px;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-passkey-btn:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 8px 20px rgba(102, 126, 234, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-passkey-btn:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-input--code {
|
||||||
|
font: 700 22px 'Courier New', monospace;
|
||||||
|
letter-spacing: 6px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-field-hint {
|
||||||
|
font: 400 12px 'Rajdhani', sans-serif;
|
||||||
|
color: rgba(149, 207, 245, 0.45);
|
||||||
|
letter-spacing: 0.3px;
|
||||||
|
margin: 6px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-card--wide {
|
||||||
|
max-width: 520px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-back {
|
||||||
|
margin-top: 20px;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
a {
|
||||||
|
font: 500 13px 'Rajdhani', sans-serif;
|
||||||
|
color: rgba(149, 207, 245, 0.5);
|
||||||
|
text-decoration: none;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
transition: color 180ms;
|
||||||
|
|
||||||
|
&:hover { color: rgba(149, 207, 245, 0.9); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.twofa-setup {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 24px;
|
||||||
|
margin-top: 20px;
|
||||||
|
|
||||||
|
&__qr {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
img {
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 2px solid rgba(35, 111, 135, 0.3);
|
||||||
|
background: #fff;
|
||||||
|
padding: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__manual {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__manual-label {
|
||||||
|
font: 400 12px 'Rajdhani', sans-serif;
|
||||||
|
color: rgba(149, 207, 245, 0.45);
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
margin: 0 0 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__secret {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 6px 14px;
|
||||||
|
background: rgba(0, 0, 0, 0.35);
|
||||||
|
border: 1px solid rgba(35, 111, 135, 0.3);
|
||||||
|
border-radius: 4px;
|
||||||
|
font: 700 14px 'Courier New', monospace;
|
||||||
|
letter-spacing: 3px;
|
||||||
|
color: #95cff5;
|
||||||
|
word-break: break-all;
|
||||||
|
user-select: all;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,200 @@
|
|||||||
|
/*!*
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// ── Avatar ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
.bd-avatar-wrap {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bd-avatar-ring-wrap {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bd-avatar-ring {
|
||||||
|
width: 72px;
|
||||||
|
height: 72px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--bd-avatar-gradient);
|
||||||
|
border: 2px solid var(--bd-avatar-border);
|
||||||
|
box-shadow: var(--bd-avatar-glow);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font: 800 24px 'Rajdhani', sans-serif;
|
||||||
|
color: var(--bd-avatar-color);
|
||||||
|
letter-spacing: 2px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bd-avatar-img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bd-avatar-bonus {
|
||||||
|
position: absolute;
|
||||||
|
bottom: -6px;
|
||||||
|
right: -6px;
|
||||||
|
background: #ffd700;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
box-shadow: 0 0 12px rgba(255, 215, 0, 0.6), 0 0 0 2px rgba(7, 9, 13, 1);
|
||||||
|
border: 2px solid rgba(0, 0, 0, 0.5);
|
||||||
|
z-index: 10;
|
||||||
|
|
||||||
|
i {
|
||||||
|
color: #000;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.bd-avatar-name {
|
||||||
|
font: 700 15px 'Rajdhani', sans-serif;
|
||||||
|
color: var(--bd-avatar-color);
|
||||||
|
letter-spacing: 1px;
|
||||||
|
max-width: 120px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bd-avatar-side {
|
||||||
|
font: 600 10px 'Rajdhani', sans-serif;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
color: rgba(255, 255, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── StatRow ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
.bd-stat-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 9px 0;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
||||||
|
|
||||||
|
&__icon {
|
||||||
|
width: 16px;
|
||||||
|
color: rgba(149, 207, 245, 0.4);
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__label {
|
||||||
|
font: 500 13px 'Rajdhani', sans-serif;
|
||||||
|
color: rgba(255, 255, 255, 0.45);
|
||||||
|
flex: 1;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__value {
|
||||||
|
font: 700 13px 'Rajdhani', sans-serif;
|
||||||
|
color: var(--bd-stat-value-color, rgba(255, 255, 255, 0.75));
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── BonusPoints ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
.bd-bonus {
|
||||||
|
padding: 16px 20px 0;
|
||||||
|
border-top: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
|
margin: 16px 0;
|
||||||
|
|
||||||
|
&__grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__column {
|
||||||
|
padding: 16px;
|
||||||
|
border-radius: 6px;
|
||||||
|
|
||||||
|
&--red {
|
||||||
|
border: 1px solid rgba(173, 10, 5, 0.2);
|
||||||
|
background: rgba(173, 10, 5, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
&--blue {
|
||||||
|
border: 1px solid rgba(149, 207, 245, 0.2);
|
||||||
|
background: rgba(149, 207, 245, 0.05);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__heading {
|
||||||
|
font: 700 12px 'Rajdhani', sans-serif;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
color: #ffd700;
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
|
||||||
|
i {
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__rows {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── BattleDialog header actions & bonus score row ────────────────────────────
|
||||||
|
|
||||||
|
.bd-header-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bd-bonus-score {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
|
||||||
|
&__red {
|
||||||
|
font: 700 13px 'Rajdhani', sans-serif;
|
||||||
|
color: #f67d52;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
|
||||||
|
i {
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__blue {
|
||||||
|
font: 700 13px 'Rajdhani', sans-serif;
|
||||||
|
color: #95cff5;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
|
||||||
|
i {
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.bd-result-badge {
|
||||||
|
background: var(--bd-result-bg);
|
||||||
|
border: 1px solid var(--bd-result-border);
|
||||||
|
color: var(--bd-result-color);
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
/*!*
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
main div.txt {
|
||||||
|
color: rgba(255, 255, 255, 0.85);
|
||||||
|
max-width: 900px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 60px 40px 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
main div.txt h2 {
|
||||||
|
font: bold 28px 'Rajdhani', sans-serif;
|
||||||
|
color: #ffffff;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
main div.txt h3 {
|
||||||
|
font: bold 17px 'Rajdhani', sans-serif;
|
||||||
|
color: rgba(149, 207, 245, 0.9);
|
||||||
|
margin: 28px 0 10px;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
main div.txt p,
|
||||||
|
main div.txt li {
|
||||||
|
font: 400 15px 'Rajdhani', sans-serif;
|
||||||
|
color: rgba(255, 255, 255, 0.72);
|
||||||
|
line-height: 1.75;
|
||||||
|
}
|
||||||
|
|
||||||
|
main div.txt a {
|
||||||
|
color: #95cff5;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: color 180ms;
|
||||||
|
|
||||||
|
&:hover { color: #c5e8ff; }
|
||||||
|
}
|
||||||
|
|
||||||
|
main div.txt img {
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
main div.txt .img-container {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
/*!*
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.hero-cta {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
font: 800 28px 'Rajdhani', sans-serif;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 6px;
|
||||||
|
text-decoration: none;
|
||||||
|
color: #ffffff;
|
||||||
|
padding: 22px 100px 20px;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid rgba(246, 125, 82, 0.25);
|
||||||
|
|
||||||
|
background: linear-gradient(to bottom, #b30c06 0%, #d63d15 50%, #f67d52 100%);
|
||||||
|
|
||||||
|
box-shadow:
|
||||||
|
0 0 0 1px rgba(173, 10, 5, 0.2),
|
||||||
|
0 0 30px rgba(173, 10, 5, 0.35),
|
||||||
|
0 6px 24px rgba(0, 0, 0, 0.5),
|
||||||
|
inset 0 1px 0 rgba(255, 255, 255, 0.12);
|
||||||
|
|
||||||
|
transition: transform 220ms ease, box-shadow 220ms ease, letter-spacing 220ms ease;
|
||||||
|
animation: rise 0.8s 0.42s cubic-bezier(0.22, 1, 0.36, 1) both;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Outer glow layer (blurred duplicate, always visible)
|
||||||
|
.hero-cta::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: -4px;
|
||||||
|
border-radius: 7px;
|
||||||
|
background: linear-gradient(to bottom, #ad0a05, #f67d52);
|
||||||
|
filter: blur(18px);
|
||||||
|
opacity: 0.3;
|
||||||
|
z-index: -1;
|
||||||
|
transition: opacity 220ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-cta:hover {
|
||||||
|
transform: translateY(-4px);
|
||||||
|
letter-spacing: 8px;
|
||||||
|
box-shadow:
|
||||||
|
0 0 0 1px rgba(246, 125, 82, 0.3),
|
||||||
|
0 0 50px rgba(173, 10, 5, 0.65),
|
||||||
|
0 10px 32px rgba(0, 0, 0, 0.45),
|
||||||
|
inset 0 1px 0 rgba(255, 255, 255, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-cta:hover::before {
|
||||||
|
opacity: 0.55;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-cta:active {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Version / copyright line
|
||||||
|
.hero-meta {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
font: 400 12px 'Rajdhani', sans-serif;
|
||||||
|
color: rgba(255, 255, 255, 0.5);
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
margin-top: 58px;
|
||||||
|
animation: rise 0.8s 0.55s cubic-bezier(0.22, 1, 0.36, 1) both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-meta a {
|
||||||
|
color: rgba(149, 207, 245, 0.65);
|
||||||
|
text-decoration: none;
|
||||||
|
transition: color 180ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-meta a:hover {
|
||||||
|
color: #95cff5;
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
/*!*
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.hero-donate-text {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
font: 400 12px 'Rajdhani', sans-serif;
|
||||||
|
color: rgba(255, 255, 255, 0.5);
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
margin-top: 42px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
animation: rise 0.8s 0.5s cubic-bezier(0.22, 1, 0.36, 1) both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-donate {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
font: 500 12px 'Rajdhani', sans-serif;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
text-decoration: none;
|
||||||
|
color: rgba(255, 184, 82, 0.75);
|
||||||
|
padding: 6px 14px;
|
||||||
|
border-radius: 2px;
|
||||||
|
border: 1px solid rgba(255, 184, 82, 0.25);
|
||||||
|
|
||||||
|
background: rgba(255, 140, 30, 0.05);
|
||||||
|
|
||||||
|
box-shadow:
|
||||||
|
0 0 0 1px rgba(255, 140, 30, 0.1),
|
||||||
|
0 0 8px rgba(255, 140, 30, 0.08);
|
||||||
|
|
||||||
|
transition: transform 200ms ease, box-shadow 200ms ease, color 200ms ease, background 200ms ease;
|
||||||
|
animation: rise 0.8s 0.58s cubic-bezier(0.22, 1, 0.36, 1) both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-donate::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: -2px;
|
||||||
|
border-radius: 3px;
|
||||||
|
background: rgba(255, 140, 30, 0.15);
|
||||||
|
filter: blur(8px);
|
||||||
|
opacity: 0.1;
|
||||||
|
z-index: -1;
|
||||||
|
transition: opacity 200ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-donate:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
color: rgba(255, 200, 100, 0.9);
|
||||||
|
background: rgba(255, 140, 30, 0.1);
|
||||||
|
box-shadow:
|
||||||
|
0 0 0 1px rgba(255, 140, 30, 0.2),
|
||||||
|
0 0 12px rgba(255, 140, 30, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-donate:hover::before {
|
||||||
|
opacity: 0.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-donate:active {
|
||||||
|
transform: translateY(0px);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,363 @@
|
|||||||
|
/*!*
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.feature-block {
|
||||||
|
width: 100%;
|
||||||
|
padding: 80px 40px;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&:first-of-type {
|
||||||
|
border-top: 1px solid rgba(35, 111, 135, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
& + & {
|
||||||
|
border-top: 1px solid rgba(255, 255, 255, 0.05);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-block__inner {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 80px;
|
||||||
|
max-width: 1100px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-block--reverse .feature-block__inner {
|
||||||
|
flex-direction: row-reverse;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Visual side
|
||||||
|
.feature-block__visual {
|
||||||
|
flex: 0 0 340px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stats icons cluster
|
||||||
|
.feature-block__visual--stats {
|
||||||
|
height: 260px;
|
||||||
|
gap: 0;
|
||||||
|
|
||||||
|
i {
|
||||||
|
position: absolute;
|
||||||
|
color: rgba(35, 111, 135, 0.5);
|
||||||
|
transition: color 300ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bar chart — large, centre
|
||||||
|
i.fa-chart-bar {
|
||||||
|
font-size: 160px;
|
||||||
|
color: rgba(35, 111, 135, 0.5);
|
||||||
|
filter: drop-shadow(0 0 40px rgba(35, 111, 135, 0.5));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trophy — top right
|
||||||
|
i.fa-trophy {
|
||||||
|
font-size: 80px;
|
||||||
|
top: 0px;
|
||||||
|
right: 20px;
|
||||||
|
color: rgba(246, 125, 82, 0.7);
|
||||||
|
filter: drop-shadow(0 0 25px rgba(246, 125, 82, 0.4));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clock history — bottom left
|
||||||
|
i.fa-clock-rotate-left {
|
||||||
|
font-size: 68px;
|
||||||
|
bottom: 0px;
|
||||||
|
left: 20px;
|
||||||
|
color: rgba(149, 207, 245, 0.65);
|
||||||
|
filter: drop-shadow(0 0 20px rgba(149, 207, 245, 0.35));
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover i.fa-chart-bar { color: rgba(35, 111, 135, 0.8); filter: drop-shadow(0 0 50px rgba(35, 111, 135, 0.7)); }
|
||||||
|
&:hover i.fa-trophy { color: rgba(246, 125, 82, 0.9); filter: drop-shadow(0 0 35px rgba(246, 125, 82, 0.6)); }
|
||||||
|
&:hover i.fa-clock-rotate-left { color: rgba(149, 207, 245, 0.9); filter: drop-shadow(0 0 30px rgba(149, 207, 245, 0.5)); }
|
||||||
|
}
|
||||||
|
|
||||||
|
// MSN visual
|
||||||
|
.feature-block__visual--msn {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 20px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.msn-logo {
|
||||||
|
width: 90px;
|
||||||
|
height: 90px;
|
||||||
|
object-fit: contain;
|
||||||
|
filter: drop-shadow(0 0 18px rgba(149, 207, 245, 0.3)) brightness(1.1);
|
||||||
|
flex-shrink: 0;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.msn-screenshot {
|
||||||
|
width: 340px;
|
||||||
|
max-width: 100%;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
|
box-shadow:
|
||||||
|
0 8px 40px rgba(0, 0, 0, 0.6),
|
||||||
|
0 0 0 1px rgba(35, 111, 135, 0.12);
|
||||||
|
filter: saturate(0.85) brightness(0.9);
|
||||||
|
transition: filter 300ms ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
filter: saturate(1) brightness(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Privacy visual
|
||||||
|
.feature-block__visual--privacy {
|
||||||
|
height: 260px;
|
||||||
|
gap: 0;
|
||||||
|
|
||||||
|
i {
|
||||||
|
position: absolute;
|
||||||
|
color: rgba(35, 111, 135, 0.5);
|
||||||
|
transition: color 300ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shield — centre, large
|
||||||
|
i.fa-shield {
|
||||||
|
font-size: 140px;
|
||||||
|
color: rgba(34, 197, 94, 0.3);
|
||||||
|
filter: drop-shadow(0 0 30px rgba(34, 197, 94, 0.25));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lock — top right
|
||||||
|
i.fa-lock {
|
||||||
|
font-size: 56px;
|
||||||
|
top: 20px;
|
||||||
|
right: 35px;
|
||||||
|
color: rgba(168, 85, 247, 0.5);
|
||||||
|
filter: drop-shadow(0 0 16px rgba(168, 85, 247, 0.2));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Eye slash — bottom left
|
||||||
|
i.fa-eye-slash {
|
||||||
|
font-size: 48px;
|
||||||
|
bottom: 28px;
|
||||||
|
left: 40px;
|
||||||
|
color: rgba(59, 130, 246, 0.5);
|
||||||
|
filter: drop-shadow(0 0 12px rgba(59, 130, 246, 0.15));
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover i.fa-shield { color: rgba(34, 197, 94, 0.6); }
|
||||||
|
&:hover i.fa-lock { color: rgba(168, 85, 247, 0.75); }
|
||||||
|
&:hover i.fa-eye-slash { color: rgba(59, 130, 246, 0.7); }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Practice visual
|
||||||
|
.feature-block__visual--practice {
|
||||||
|
height: 260px;
|
||||||
|
gap: 0;
|
||||||
|
|
||||||
|
i {
|
||||||
|
position: absolute;
|
||||||
|
color: rgba(35, 111, 135, 0.5);
|
||||||
|
transition: color 300ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Laptop — centre, large
|
||||||
|
i.fa-laptop {
|
||||||
|
font-size: 140px;
|
||||||
|
color: rgba(251, 146, 60, 0.3);
|
||||||
|
filter: drop-shadow(0 0 30px rgba(251, 146, 60, 0.25));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Linux — top left
|
||||||
|
i.fa-linux {
|
||||||
|
font-size: 48px;
|
||||||
|
top: 20px;
|
||||||
|
left: 35px;
|
||||||
|
color: rgba(245, 158, 11, 0.5);
|
||||||
|
filter: drop-shadow(0 0 16px rgba(245, 158, 11, 0.2));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apple — top right
|
||||||
|
i.fa-apple {
|
||||||
|
font-size: 56px;
|
||||||
|
top: 20px;
|
||||||
|
right: 35px;
|
||||||
|
color: rgba(156, 163, 175, 0.5);
|
||||||
|
filter: drop-shadow(0 0 16px rgba(156, 163, 175, 0.2));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Windows — bottom left
|
||||||
|
i.fa-windows {
|
||||||
|
font-size: 48px;
|
||||||
|
bottom: 28px;
|
||||||
|
left: 40px;
|
||||||
|
color: rgba(59, 130, 246, 0.5);
|
||||||
|
filter: drop-shadow(0 0 12px rgba(59, 130, 246, 0.15));
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover i.fa-laptop { color: rgba(251, 146, 60, 0.6); }
|
||||||
|
&:hover i.fa-linux { color: rgba(245, 158, 11, 0.75); }
|
||||||
|
&:hover i.fa-apple { color: rgba(156, 163, 175, 0.75); }
|
||||||
|
&:hover i.fa-windows { color: rgba(59, 130, 246, 0.7); }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Text side
|
||||||
|
.feature-block__text {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-block__label {
|
||||||
|
font: 700 11px 'Rajdhani', sans-serif;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 4px;
|
||||||
|
color: rgba(149, 207, 245, 0.55);
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-block__title {
|
||||||
|
font: 800 40px 'Rajdhani', sans-serif;
|
||||||
|
color: #ffffff;
|
||||||
|
line-height: 1.1;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
margin-bottom: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-block__body {
|
||||||
|
font: 400 16px 'Rajdhani', sans-serif;
|
||||||
|
color: rgba(255, 255, 255, 0.62);
|
||||||
|
line-height: 1.8;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-block__cta {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 28px;
|
||||||
|
font: 700 14px 'Rajdhani', sans-serif;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
text-decoration: none;
|
||||||
|
color: rgba(149, 207, 245, 0.85);
|
||||||
|
border: 1px solid rgba(35, 111, 135, 0.4);
|
||||||
|
background: rgba(35, 111, 135, 0.1);
|
||||||
|
padding: 11px 24px;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: all 200ms ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(35, 111, 135, 0.25);
|
||||||
|
border-color: rgba(149, 207, 245, 0.55);
|
||||||
|
color: #fff;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 16px rgba(35, 111, 135, 0.25);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.practice-links {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
margin-top: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.practice-link {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
font: 700 13px 'Rajdhani', sans-serif;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
text-decoration: none;
|
||||||
|
color: rgba(149, 207, 245, 0.85);
|
||||||
|
border: 1px solid rgba(59, 130, 246, 0.3);
|
||||||
|
background: rgba(59, 130, 246, 0.08);
|
||||||
|
padding: 10px 18px;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: all 200ms ease;
|
||||||
|
width: fit-content;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(59, 130, 246, 0.2);
|
||||||
|
border-color: rgba(59, 130, 246, 0.6);
|
||||||
|
color: #fff;
|
||||||
|
transform: translateX(4px);
|
||||||
|
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.practice-link-icon {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 8px;
|
||||||
|
object-fit: contain;
|
||||||
|
background: rgba(255, 255, 255, 0.08);
|
||||||
|
padding: 4px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
flex-shrink: 0;
|
||||||
|
transition: all 200ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.practice-link:hover .practice-link-icon {
|
||||||
|
background: rgba(59, 130, 246, 0.15);
|
||||||
|
border-color: rgba(59, 130, 246, 0.4);
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 900px) {
|
||||||
|
.feature-block__inner,
|
||||||
|
.feature-block--reverse .feature-block__inner {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 48px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-block__visual {
|
||||||
|
flex: none;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-block__visual--stats {
|
||||||
|
height: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-block__visual--msn {
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.msn-screenshot {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-block__label,
|
||||||
|
.feature-block__cta {
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-block__title { font-size: 32px; }
|
||||||
|
|
||||||
|
.feature-block {
|
||||||
|
padding: 60px 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.practice-links {
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.practice-link {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
/*!*
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
footer {
|
||||||
|
background: #040608;
|
||||||
|
border-top: 1px solid rgba(35, 111, 135, 0.12);
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-inner {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
max-width: 1100px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 60px 60px 52px;
|
||||||
|
gap: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-brand {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-logo {
|
||||||
|
width: 72px;
|
||||||
|
height: 72px;
|
||||||
|
opacity: 0.55;
|
||||||
|
filter:
|
||||||
|
drop-shadow(0 0 12px rgba(35, 111, 135, 0.4))
|
||||||
|
brightness(1.1);
|
||||||
|
transition: opacity 250ms ease, filter 250ms ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 0.9;
|
||||||
|
filter:
|
||||||
|
drop-shadow(0 0 20px rgba(35, 111, 135, 0.65))
|
||||||
|
brightness(1.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-name {
|
||||||
|
font: 700 22px 'Rajdhani', sans-serif;
|
||||||
|
color: rgba(255, 255, 255, 0.75);
|
||||||
|
letter-spacing: 2px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-tagline {
|
||||||
|
font: 400 13px 'Rajdhani', sans-serif;
|
||||||
|
color: rgba(149, 207, 245, 0.7);
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
max-width: 240px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-nav-label {
|
||||||
|
font: 700 11px 'Rajdhani', sans-serif;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 4px;
|
||||||
|
color: rgba(255, 255, 255, 0.5);
|
||||||
|
margin-bottom: 18px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-nav ul {
|
||||||
|
list-style: none;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 6px;
|
||||||
|
min-width: 180px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-nav ul li a {
|
||||||
|
display: block;
|
||||||
|
font: 500 15px 'Rajdhani', sans-serif;
|
||||||
|
color: rgba(255, 255, 255, 0.7);
|
||||||
|
text-decoration: none;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 1.5px;
|
||||||
|
white-space: nowrap;
|
||||||
|
padding: 6px 0;
|
||||||
|
transition: color 180ms ease, letter-spacing 180ms ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #95cff5;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-copy {
|
||||||
|
border-top: 1px solid rgba(255, 255, 255, 0.05);
|
||||||
|
padding: 16px 60px;
|
||||||
|
max-width: 1100px;
|
||||||
|
margin: 0 auto;
|
||||||
|
|
||||||
|
p {
|
||||||
|
font: 400 11px 'Rajdhani', sans-serif;
|
||||||
|
color: rgba(255, 255, 255, 0.45);
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: rgba(149, 207, 245, 0.6);
|
||||||
|
text-decoration: none;
|
||||||
|
transition: color 180ms;
|
||||||
|
|
||||||
|
&:hover { color: #95cff5; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-version {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
margin: 0 7px;
|
||||||
|
padding: 3px 8px 2px;
|
||||||
|
border: 1px solid rgba(149, 207, 245, 0.24);
|
||||||
|
border-radius: 999px;
|
||||||
|
background: linear-gradient(135deg, rgba(35, 111, 135, 0.22), rgba(149, 207, 245, 0.08));
|
||||||
|
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.08), 0 0 12px rgba(35, 111, 135, 0.12);
|
||||||
|
color: rgba(180, 224, 248, 0.9);
|
||||||
|
font: 700 10px 'Rajdhani', sans-serif;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
line-height: 1;
|
||||||
|
text-transform: uppercase;
|
||||||
|
vertical-align: middle;
|
||||||
|
|
||||||
|
i {
|
||||||
|
color: #95cff5;
|
||||||
|
font-size: 9px;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
/*!*
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
header {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
// Minesweeper grid texture
|
||||||
|
background-color: #07090d;
|
||||||
|
background-image:
|
||||||
|
linear-gradient(rgba(35, 111, 135, 0.1) 1px, transparent 1px),
|
||||||
|
linear-gradient(90deg, rgba(35, 111, 135, 0.1) 1px, transparent 1px);
|
||||||
|
background-size: 46px 46px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deep radial vignette – grid fades toward the centre
|
||||||
|
header::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: radial-gradient(
|
||||||
|
ellipse 85% 75% at 50% 50%,
|
||||||
|
#07090d 10%,
|
||||||
|
transparent 75%
|
||||||
|
);
|
||||||
|
z-index: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Smoke at the bottom so header bleeds into body
|
||||||
|
header::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 160px;
|
||||||
|
background: linear-gradient(to bottom, transparent, #07090d);
|
||||||
|
z-index: 1;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
/*!*
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.hero--compact {
|
||||||
|
min-height: unset;
|
||||||
|
padding: 36px 60px 48px;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
text-align: left;
|
||||||
|
gap: 52px;
|
||||||
|
|
||||||
|
&::before, &::after { display: none; }
|
||||||
|
|
||||||
|
.hero-logo {
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
|
||||||
|
img { width: 180px; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-body {
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-sub {
|
||||||
|
font-size: 14px;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 26px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
letter-spacing: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-cta {
|
||||||
|
padding: 12px 52px 10px;
|
||||||
|
font-size: 18px;
|
||||||
|
letter-spacing: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-meta {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also shrink the bottom fade on sub-pages
|
||||||
|
header:has(.hero--compact)::after {
|
||||||
|
height: 60px;
|
||||||
|
}
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
/*!*
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.hero {
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
text-align: center;
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: 80px 40px 160px;
|
||||||
|
gap: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decorative glow blobs in opposite corners
|
||||||
|
.hero::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: -60px;
|
||||||
|
left: -60px;
|
||||||
|
width: 420px;
|
||||||
|
height: 420px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: radial-gradient(circle, rgba(173, 10, 5, 0.09) 0%, transparent 65%);
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
bottom: 100px;
|
||||||
|
right: -60px;
|
||||||
|
width: 380px;
|
||||||
|
height: 380px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: radial-gradient(circle, rgba(35, 111, 135, 0.1) 0%, transparent 65%);
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logo
|
||||||
|
.hero-logo {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 72px;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
animation: appear 0.9s cubic-bezier(0.22, 1, 0.36, 1) both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-logo img {
|
||||||
|
width: 400px;
|
||||||
|
max-width: 82vw;
|
||||||
|
filter:
|
||||||
|
drop-shadow(0 0 40px rgba(35, 111, 135, 0.35))
|
||||||
|
drop-shadow(0 6px 20px rgba(0, 0, 0, 0.8));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Body text block
|
||||||
|
.hero-body {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-sub {
|
||||||
|
font: 300 17px 'Rajdhani', sans-serif;
|
||||||
|
color: rgba(149, 207, 245, 0.9);
|
||||||
|
letter-spacing: 3px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
margin-bottom: 18px;
|
||||||
|
animation: rise 0.8s 0.15s cubic-bezier(0.22, 1, 0.36, 1) both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-sub strong {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #95cff5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero h1 {
|
||||||
|
font: 800 58px 'Rajdhani', sans-serif;
|
||||||
|
color: #ffffff;
|
||||||
|
line-height: 1.1;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
margin-bottom: 56px;
|
||||||
|
text-shadow: 0 4px 30px rgba(0, 0, 0, 0.7);
|
||||||
|
animation: rise 0.8s 0.28s cubic-bezier(0.22, 1, 0.36, 1) both;
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,32 @@
|
|||||||
|
/*!*
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
* {
|
||||||
|
outline: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
// Grid lives on html so it tiles across all pages including content pages
|
||||||
|
background-color: #07090d;
|
||||||
|
background-image:
|
||||||
|
linear-gradient(rgba(35, 111, 135, 0.1) 1px, transparent 1px),
|
||||||
|
linear-gradient(90deg, rgba(35, 111, 135, 0.1) 1px, transparent 1px);
|
||||||
|
background-size: 46px 46px;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background: transparent;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
/*!*
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
@media screen and (max-width: 900px) {
|
||||||
|
.hero h1 {
|
||||||
|
font-size: 44px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-cta {
|
||||||
|
padding: 20px 72px 18px;
|
||||||
|
font-size: 24px;
|
||||||
|
letter-spacing: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 768px) {
|
||||||
|
.hero--compact {
|
||||||
|
flex-direction: column;
|
||||||
|
text-align: center;
|
||||||
|
padding: 36px 24px 44px;
|
||||||
|
gap: 28px;
|
||||||
|
|
||||||
|
.hero-body { align-items: center; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-stats {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-charts {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-header {
|
||||||
|
flex-direction: column;
|
||||||
|
text-align: center;
|
||||||
|
padding: 28px 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-email,
|
||||||
|
.profile-role {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-game {
|
||||||
|
grid-template-columns: 26px 64px 18px 1fr 14px;
|
||||||
|
|
||||||
|
.profile-game__date { display: none; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-inner {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
|
padding: 48px 30px 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-brand {
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-tagline {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-nav-label {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-nav ul {
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-copy {
|
||||||
|
padding: 16px 30px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 550px) {
|
||||||
|
.hero {
|
||||||
|
padding: 60px 24px 140px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-logo img {
|
||||||
|
width: 260px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-logo {
|
||||||
|
margin-bottom: 52px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero h1 {
|
||||||
|
font-size: 32px;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-sub {
|
||||||
|
font-size: 14px;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-cta {
|
||||||
|
padding: 18px 48px 16px;
|
||||||
|
font-size: 20px;
|
||||||
|
letter-spacing: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
/*!*
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
main {
|
||||||
|
background: #07090d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tech-section {
|
||||||
|
padding: 48px 20px 72px;
|
||||||
|
text-align: center;
|
||||||
|
border-top: 1px solid rgba(255, 255, 255, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tech-label {
|
||||||
|
font: 600 11px 'Rajdhani', sans-serif;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 6px;
|
||||||
|
color: rgba(255, 255, 255, 0.14);
|
||||||
|
margin-bottom: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tech-link {
|
||||||
|
display: inline-block;
|
||||||
|
line-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tech-logos img {
|
||||||
|
display: inline-block;
|
||||||
|
width: 52px;
|
||||||
|
height: 52px;
|
||||||
|
object-fit: contain;
|
||||||
|
margin: 8px 24px;
|
||||||
|
// Force all logos to white, then tint with the game's blue on hover
|
||||||
|
filter: brightness(0) invert(1) opacity(0.35);
|
||||||
|
transition: filter 220ms ease, transform 220ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tech-logos img:hover {
|
||||||
|
filter:
|
||||||
|
brightness(0) invert(1)
|
||||||
|
sepia(1) saturate(3) hue-rotate(175deg) brightness(1.1)
|
||||||
|
opacity(0.9);
|
||||||
|
transform: translateY(-4px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tech-oss {
|
||||||
|
margin-top: 36px;
|
||||||
|
font: 400 15px 'Rajdhani', sans-serif;
|
||||||
|
color: rgba(255, 255, 255, 0.7);
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
max-width: 680px;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
padding: 0 24px;
|
||||||
|
line-height: 1.7;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
i {
|
||||||
|
color: rgba(220, 60, 50, 0.85);
|
||||||
|
margin-right: 6px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
/*!*
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.game-brand-bar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
position: fixed;
|
||||||
|
top: 20px;
|
||||||
|
left: 20px;
|
||||||
|
z-index: 20;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-from-game {
|
||||||
|
display: inline-block;
|
||||||
|
-ms-transform: scale(1);
|
||||||
|
-webkit-transform: scale(1);
|
||||||
|
transform: scale(1);
|
||||||
|
-webkit-transition: all 250ms cubic-bezier(.17, .67, .83, .67);
|
||||||
|
transition: all 250ms cubic-bezier(.17, .67, .83, .67);
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-from-game img {
|
||||||
|
width: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-from-game:hover {
|
||||||
|
-ms-transform: scale(1.2);
|
||||||
|
-webkit-transform: scale(1.2);
|
||||||
|
transform: scale(1.2);
|
||||||
|
-webkit-transition: all 250ms cubic-bezier(.17, .67, .83, .67);
|
||||||
|
transition: all 250ms cubic-bezier(.17, .67, .83, .67);
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-cap-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
position: fixed;
|
||||||
|
bottom: 20px;
|
||||||
|
left: 50%;
|
||||||
|
z-index: 20;
|
||||||
|
padding: 5px 10px;
|
||||||
|
border: 1px solid rgba(255, 216, 92, 0.42);
|
||||||
|
-webkit-border-radius: 999px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background:
|
||||||
|
linear-gradient(115deg, transparent 0 38%, rgba(255, 255, 255, 0.22) 48%, transparent 58% 100%),
|
||||||
|
rgba(14, 16, 18, 0.72);
|
||||||
|
background-position: 120% 50%, 0 0;
|
||||||
|
background-size: 260% 100%, auto;
|
||||||
|
-webkit-box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.32), 0 8px 22px rgba(0, 0, 0, 0.28);
|
||||||
|
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.32), 0 8px 22px rgba(0, 0, 0, 0.28);
|
||||||
|
color: #ffe58a;
|
||||||
|
font: 700 11px 'Rajdhani', sans-serif;
|
||||||
|
letter-spacing: 0.9px;
|
||||||
|
line-height: 1;
|
||||||
|
text-decoration: none;
|
||||||
|
text-transform: uppercase;
|
||||||
|
-ms-transform: translateX(-50%);
|
||||||
|
-webkit-transform: translateX(-50%);
|
||||||
|
transform: translateX(-50%);
|
||||||
|
white-space: nowrap;
|
||||||
|
-webkit-transition:
|
||||||
|
background-position 650ms ease,
|
||||||
|
border-color 200ms ease,
|
||||||
|
box-shadow 200ms ease,
|
||||||
|
color 200ms ease,
|
||||||
|
transform 200ms ease;
|
||||||
|
transition:
|
||||||
|
background-position 650ms ease,
|
||||||
|
border-color 200ms ease,
|
||||||
|
box-shadow 200ms ease,
|
||||||
|
color 200ms ease,
|
||||||
|
transform 200ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-cap-badge:hover,
|
||||||
|
.game-cap-badge:focus-visible {
|
||||||
|
background-position: -60% 50%, 0 0;
|
||||||
|
border-color: rgba(255, 229, 138, 0.78);
|
||||||
|
-webkit-box-shadow: 0 0 0 1px rgba(255, 229, 138, 0.18), 0 0 24px rgba(255, 195, 50, 0.28);
|
||||||
|
box-shadow: 0 0 0 1px rgba(255, 229, 138, 0.18), 0 0 24px rgba(255, 195, 50, 0.28);
|
||||||
|
color: #fff4bd;
|
||||||
|
text-decoration: none;
|
||||||
|
-ms-transform: translateX(-50%) translateY(-1px);
|
||||||
|
-webkit-transform: translateX(-50%) translateY(-1px);
|
||||||
|
transform: translateX(-50%) translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-cap-badge:focus,
|
||||||
|
.game-cap-badge:active,
|
||||||
|
.game-cap-badge:visited {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-cap-badge:focus-visible {
|
||||||
|
outline: 2px solid rgba(255, 229, 138, 0.75);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
/*!*
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
html {
|
||||||
|
height: 100%;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
height: 100%;
|
||||||
|
min-height: 100%;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mine-container {
|
||||||
|
background-size: cover;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clear {
|
||||||
|
clear: both
|
||||||
|
}
|
||||||
|
|
||||||
|
#mine-wrapper,
|
||||||
|
#mine-wrapper * {
|
||||||
|
-webkit-box-sizing: border-box;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mine-wrapper {
|
||||||
|
display: table;
|
||||||
|
width: 842px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mine-wrapper .game-wrapper {
|
||||||
|
background: #000;
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: flex-start;
|
||||||
|
padding: 10px;
|
||||||
|
|
||||||
|
-webkit-border-radius: 10px;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
@@ -0,0 +1,234 @@
|
|||||||
|
/*!*
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#mine-wrapper .game-wrapper .users .user-container .user-control {
|
||||||
|
background: -moz-linear-gradient(-45deg, rgba(255, 255, 255, 0.5) 0%, rgba(125, 185, 232, 0) 100%);
|
||||||
|
background: -webkit-linear-gradient(-45deg, rgba(255, 255, 255, 0.5) 0%, rgba(125, 185, 232, 0) 100%);
|
||||||
|
background: linear-gradient(135deg, rgba(255, 255, 255, 0.5) 0%, rgba(125, 185, 232, 0) 100%);
|
||||||
|
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffff', endColorstr='#007db9e8', GradientType=1);
|
||||||
|
position: relative;
|
||||||
|
padding: 5px;
|
||||||
|
|
||||||
|
-webkit-border-radius: 5px;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mine-wrapper .game-wrapper .users .user-container .user-control > img {
|
||||||
|
position: absolute;
|
||||||
|
width: 55px;
|
||||||
|
left: -5px;
|
||||||
|
bottom: 10px;
|
||||||
|
|
||||||
|
-ms-transform: rotate(-15deg);
|
||||||
|
-webkit-transform: rotate(-15deg);
|
||||||
|
transform: rotate(-15deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
#mine-wrapper .game-wrapper .users .user-container .user-control .user-control-mines {
|
||||||
|
display: inline-block;
|
||||||
|
background: #FFFFFF;
|
||||||
|
font-size: 25px;
|
||||||
|
text-align: center;
|
||||||
|
width: 45px;
|
||||||
|
height: 35px;
|
||||||
|
margin-left: 25px;
|
||||||
|
margin-top: 5px;
|
||||||
|
|
||||||
|
-webkit-border-radius: 5px;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mine-wrapper .game-wrapper .users .user-container.user-blue .user-control .user-control-mines {
|
||||||
|
color: #1a3955;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mine-wrapper .game-wrapper .users .user-container.user-red .user-control .user-control-mines {
|
||||||
|
color: #b10000;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mine-wrapper .game-wrapper .users .user-container .user-control .bomb-container {
|
||||||
|
display: inline-block;
|
||||||
|
float: right;
|
||||||
|
width: 65px;
|
||||||
|
height: 45px;
|
||||||
|
border: 1px solid #000;
|
||||||
|
|
||||||
|
-webkit-border-radius: 7px;
|
||||||
|
border-radius: 7px;
|
||||||
|
-webkit-transform: translateZ(0);
|
||||||
|
transform: translateZ(0);
|
||||||
|
-webkit-backface-visibility: hidden;
|
||||||
|
backface-visibility: hidden;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
box-shadow: 0 0 1px rgba(0, 0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#mine-wrapper .game-wrapper .users .user-container .user-control .bomb-container.buzz:hover {
|
||||||
|
-webkit-animation-name: hvr-buzz-out;
|
||||||
|
animation-name: hvr-buzz-out;
|
||||||
|
-webkit-animation-duration: 0.75s;
|
||||||
|
animation-duration: 0.75s;
|
||||||
|
-webkit-animation-timing-function: linear;
|
||||||
|
animation-timing-function: linear;
|
||||||
|
-webkit-animation-iteration-count: 1;
|
||||||
|
animation-iteration-count: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@-webkit-keyframes hvr-buzz-out {
|
||||||
|
10% { -webkit-transform: translateX(3px) rotate(2deg); transform: translateX(3px) rotate(2deg); }
|
||||||
|
20% { -webkit-transform: translateX(-3px) rotate(-2deg); transform: translateX(-3px) rotate(-2deg); }
|
||||||
|
30% { -webkit-transform: translateX(3px) rotate(2deg); transform: translateX(3px) rotate(2deg); }
|
||||||
|
40% { -webkit-transform: translateX(-3px) rotate(-2deg); transform: translateX(-3px) rotate(-2deg); }
|
||||||
|
50% { -webkit-transform: translateX(2px) rotate(1deg); transform: translateX(2px) rotate(1deg); }
|
||||||
|
60% { -webkit-transform: translateX(-2px) rotate(-1deg); transform: translateX(-2px) rotate(-1deg); }
|
||||||
|
70% { -webkit-transform: translateX(2px) rotate(1deg); transform: translateX(2px) rotate(1deg); }
|
||||||
|
80% { -webkit-transform: translateX(-2px) rotate(-1deg); transform: translateX(-2px) rotate(-1deg); }
|
||||||
|
90% { -webkit-transform: translateX(1px) rotate(0); transform: translateX(1px) rotate(0); }
|
||||||
|
100% { -webkit-transform: translateX(-1px) rotate(0); transform: translateX(-1px) rotate(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes hvr-buzz-out {
|
||||||
|
10% { -webkit-transform: translateX(3px) rotate(2deg); transform: translateX(3px) rotate(2deg); }
|
||||||
|
20% { -webkit-transform: translateX(-3px) rotate(-2deg); transform: translateX(-3px) rotate(-2deg); }
|
||||||
|
30% { -webkit-transform: translateX(3px) rotate(2deg); transform: translateX(3px) rotate(2deg); }
|
||||||
|
40% { -webkit-transform: translateX(-3px) rotate(-2deg); transform: translateX(-3px) rotate(-2deg); }
|
||||||
|
50% { -webkit-transform: translateX(2px) rotate(1deg); transform: translateX(2px) rotate(1deg); }
|
||||||
|
60% { -webkit-transform: translateX(-2px) rotate(-1deg); transform: translateX(-2px) rotate(-1deg); }
|
||||||
|
70% { -webkit-transform: translateX(2px) rotate(1deg); transform: translateX(2px) rotate(1deg); }
|
||||||
|
80% { -webkit-transform: translateX(-2px) rotate(-1deg); transform: translateX(-2px) rotate(-1deg); }
|
||||||
|
90% { -webkit-transform: translateX(1px) rotate(0); transform: translateX(1px) rotate(0); }
|
||||||
|
100% { -webkit-transform: translateX(-1px) rotate(0); transform: translateX(-1px) rotate(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
#mine-wrapper .game-wrapper .users .user-container .user-control .bomb-container .bomb {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
text-align: center;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mine-wrapper .game-wrapper .users .user-container .user-control .bomb-container .bomb img {
|
||||||
|
display: inline-block;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mine-wrapper .game-wrapper .users .user-container .user-control .bomb-container:hover .bomb img {
|
||||||
|
-webkit-animation-name: hvr-buzz;
|
||||||
|
animation-name: hvr-buzz;
|
||||||
|
-webkit-animation-duration: 0.15s;
|
||||||
|
animation-duration: 0.15s;
|
||||||
|
-webkit-animation-timing-function: linear;
|
||||||
|
animation-timing-function: linear;
|
||||||
|
-webkit-animation-iteration-count: infinite;
|
||||||
|
animation-iteration-count: infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mine-wrapper .game-wrapper .users .user-container .user-control .bomb-container .bomb {
|
||||||
|
-webkit-border-radius: 5px;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mine-wrapper .game-wrapper .users .user-container.user-blue .user-control .bomb-container .bomb {
|
||||||
|
background: rgb(131, 194, 245);
|
||||||
|
background: -moz-linear-gradient(top, rgba(131, 194, 245, 1) 0%, rgba(108, 190, 230, 1) 39%, rgba(221, 255, 252, 1) 100%);
|
||||||
|
background: -webkit-linear-gradient(top, rgba(131, 194, 245, 1) 0%, rgba(108, 190, 230, 1) 39%, rgba(221, 255, 252, 1) 100%);
|
||||||
|
background: linear-gradient(to bottom, rgba(131, 194, 245, 1) 0%, rgba(108, 190, 230, 1) 39%, rgba(221, 255, 252, 1) 100%);
|
||||||
|
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#83c2f5', endColorstr='#ddfffc', GradientType=0);
|
||||||
|
border: 3px solid #0b538e;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mine-wrapper .game-wrapper .users .user-container.user-red .user-control .bomb-container .bomb {
|
||||||
|
background: rgb(255, 175, 159);
|
||||||
|
background: -moz-linear-gradient(top, rgba(255, 175, 159, 1) 0%, rgba(231, 113, 7, 1) 54%, rgba(231, 113, 7, 1) 54%, rgba(237, 172, 16, 1) 100%);
|
||||||
|
background: -webkit-linear-gradient(top, rgba(255, 175, 159, 1) 0%, rgba(231, 113, 7, 1) 54%, rgba(231, 113, 7, 1) 54%, rgba(237, 172, 16, 1) 100%);
|
||||||
|
background: linear-gradient(to bottom, rgba(255, 175, 159, 1) 0%, rgba(231, 113, 7, 1) 54%, rgba(231, 113, 7, 1) 54%, rgba(237, 172, 16, 1) 100%);
|
||||||
|
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffaf9f', endColorstr='#edac10', GradientType=0);
|
||||||
|
border: 3px solid #c9221c;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resign button
|
||||||
|
|
||||||
|
#mine-wrapper .game-wrapper .users .resign {
|
||||||
|
background: rgba(70, 73, 66, 1);
|
||||||
|
background: -moz-linear-gradient(top, rgba(70, 73, 66, 1) 0%, rgba(140, 138, 139, 1) 69%, rgba(96, 89, 97, 1) 100%);
|
||||||
|
background: -webkit-gradient(left top, left bottom, color-stop(0%, rgba(70, 73, 66, 1)), color-stop(69%, rgba(140, 138, 139, 1)), color-stop(100%, rgba(96, 89, 97, 1)));
|
||||||
|
background: -webkit-linear-gradient(top, rgba(70, 73, 66, 1) 0%, rgba(140, 138, 139, 1) 69%, rgba(96, 89, 97, 1) 100%);
|
||||||
|
background: -o-linear-gradient(top, rgba(70, 73, 66, 1) 0%, rgba(140, 138, 139, 1) 69%, rgba(96, 89, 97, 1) 100%);
|
||||||
|
background: -ms-linear-gradient(top, rgba(70, 73, 66, 1) 0%, rgba(140, 138, 139, 1) 69%, rgba(96, 89, 97, 1) 100%);
|
||||||
|
background: linear-gradient(to bottom, rgba(70, 73, 66, 1) 0%, rgba(140, 138, 139, 1) 69%, rgba(96, 89, 97, 1) 100%);
|
||||||
|
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#464942', endColorstr='#605961', GradientType=0);
|
||||||
|
display: block;
|
||||||
|
position: relative;
|
||||||
|
width: 95%;
|
||||||
|
height: 50px;
|
||||||
|
font-family: 'Open Sans', sans-serif;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 22px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 40px;
|
||||||
|
border: 3px solid #484742;
|
||||||
|
color: #fff;
|
||||||
|
margin: 10px auto 0 auto;
|
||||||
|
|
||||||
|
outline: none;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
-webkit-border-radius: 5px;
|
||||||
|
border-radius: 5px;
|
||||||
|
-webkit-transition: all 250ms ease-in-out;
|
||||||
|
-moz-transition: all 250ms ease-in-out;
|
||||||
|
-o-transition: all 250ms ease-in-out;
|
||||||
|
transition: all 250ms ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mine-wrapper .game-wrapper .users .resign:hover {
|
||||||
|
background: rgba(70, 73, 66, 1);
|
||||||
|
color: #FFFFFF;
|
||||||
|
transition: all 250ms ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mine-wrapper .game-wrapper .users .resign.disabled {
|
||||||
|
background: rgba(70, 73, 66, 1);
|
||||||
|
color: #848484;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mine-wrapper .game-wrapper .users .resign.disabled:hover {
|
||||||
|
background: rgba(70, 73, 66, 1);
|
||||||
|
color: #848484;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mine-wrapper .game-wrapper .users .resign .resign-shine {
|
||||||
|
background: rgba(255, 255, 255, 1);
|
||||||
|
background: -moz-linear-gradient(top, rgba(255, 255, 255, 1) 0%, rgba(0, 0, 0, 0) 100%);
|
||||||
|
background: -webkit-gradient(left top, left bottom, color-stop(0%, rgba(255, 255, 255, 1)), color-stop(100%, rgba(0, 0, 0, 0)));
|
||||||
|
background: -webkit-linear-gradient(top, rgba(255, 255, 255, 1) 0%, rgba(0, 0, 0, 0) 100%);
|
||||||
|
background: -o-linear-gradient(top, rgba(255, 255, 255, 1) 0%, rgba(0, 0, 0, 0) 100%);
|
||||||
|
background: -ms-linear-gradient(top, rgba(255, 255, 255, 1) 0%, rgba(0, 0, 0, 0) 100%);
|
||||||
|
background: linear-gradient(to bottom, rgba(255, 255, 255, 1) 0%, rgba(0, 0, 0, 0) 100%);
|
||||||
|
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffff', endColorstr='#000000', GradientType=0);
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 33%;
|
||||||
|
|
||||||
|
opacity: 0.7;
|
||||||
|
|
||||||
|
-webkit-border-radius: 5px;
|
||||||
|
border-radius: 5px;
|
||||||
|
transition: all 250ms ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mine-wrapper .game-wrapper .users .resign:hover .resign-shine,
|
||||||
|
#mine-wrapper .game-wrapper .users .resign.disabled .resign-shine {
|
||||||
|
display: none;
|
||||||
|
transition: all 250ms ease-in-out;
|
||||||
|
}
|
||||||
@@ -0,0 +1,250 @@
|
|||||||
|
/*!*
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#mine-wrapper .bonus-box {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 6px;
|
||||||
|
min-width: 58px;
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 2px solid transparent;
|
||||||
|
background: #07090d;
|
||||||
|
font-family: 'Rajdhani', sans-serif;
|
||||||
|
font-weight: bold;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.25s ease;
|
||||||
|
align-self: stretch;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
filter: brightness(1.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#mine-wrapper .bonus-box.red-bonus {
|
||||||
|
background: linear-gradient(to bottom, #2a0502 0%, #4a1510 100%);
|
||||||
|
border-color: rgba(246, 125, 82, 0.4);
|
||||||
|
color: rgba(246, 125, 82, 0.85);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: rgba(246, 125, 82, 0.85);
|
||||||
|
box-shadow: 0 0 12px rgba(173, 10, 5, 0.6);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#mine-wrapper .bonus-box.blue-bonus {
|
||||||
|
background: linear-gradient(to bottom, #050f18 0%, #0f2838 100%);
|
||||||
|
border-color: rgba(149, 207, 245, 0.4);
|
||||||
|
color: rgba(149, 207, 245, 0.85);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: rgba(149, 207, 245, 0.85);
|
||||||
|
box-shadow: 0 0 12px rgba(35, 111, 135, 0.6);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#mine-wrapper .bonus-box__icon {
|
||||||
|
font-size: 13px;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mine-wrapper .bonus-box__value {
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 16px;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bsd {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
font-family: 'Rajdhani', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bsd-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 18px 22px 14px;
|
||||||
|
border-bottom: 1px solid rgba(35, 111, 135, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bsd-header-text {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bsd-label {
|
||||||
|
font-size: 11px;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
color: rgba(149, 207, 245, 0.7);
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bsd-title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 700;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
color: #fff;
|
||||||
|
|
||||||
|
.fa {
|
||||||
|
color: #f6d572;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.bsd-close {
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid rgba(35, 111, 135, 0.4);
|
||||||
|
border-radius: 6px;
|
||||||
|
color: rgba(255, 255, 255, 0.7);
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #fff;
|
||||||
|
border-color: rgba(149, 207, 245, 0.8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.bsd-body {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 14px;
|
||||||
|
padding: 18px 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bsd-column {
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 14px;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
background: rgba(255, 255, 255, 0.02);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bsd-column--red {
|
||||||
|
border-color: rgba(246, 125, 82, 0.35);
|
||||||
|
background: linear-gradient(to bottom, rgba(74, 6, 3, 0.35), rgba(107, 37, 21, 0.15));
|
||||||
|
}
|
||||||
|
|
||||||
|
.bsd-column--blue {
|
||||||
|
border-color: rgba(149, 207, 245, 0.35);
|
||||||
|
background: linear-gradient(to bottom, rgba(11, 37, 48, 0.35), rgba(22, 61, 85, 0.15));
|
||||||
|
}
|
||||||
|
|
||||||
|
.bsd-column-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bsd-column-name {
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 16px;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bsd-column-total {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #f6d572;
|
||||||
|
|
||||||
|
.fa {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.bsd-stats {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bsd-stat {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 6px 2px;
|
||||||
|
border-bottom: 1px dashed rgba(255, 255, 255, 0.06);
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.bsd-stat-text {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bsd-stat-label {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: rgba(255, 255, 255, 0.92);
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bsd-stat-desc {
|
||||||
|
font-size: 11px;
|
||||||
|
color: rgba(255, 255, 255, 0.48);
|
||||||
|
line-height: 1.25;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bsd-stat-value {
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #fff;
|
||||||
|
min-width: 24px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bsd-note {
|
||||||
|
margin: 0;
|
||||||
|
padding: 12px 22px 18px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: rgba(255, 255, 255, 0.45);
|
||||||
|
text-align: center;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 520px) {
|
||||||
|
.bsd-body {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,216 @@
|
|||||||
|
/*!*
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#mine-wrapper .grid {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 643px;
|
||||||
|
border: 1px solid #cac3e5;
|
||||||
|
|
||||||
|
cursor: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mine-wrapper .grid-container {
|
||||||
|
background: #4E4E4E;
|
||||||
|
padding: 15px 10px;
|
||||||
|
|
||||||
|
-webkit-border-radius: 5px;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mine-wrapper .grid .field-wrapper {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mine-wrapper .grid .field-wrapper > img.field-target {
|
||||||
|
position: absolute;
|
||||||
|
display: none;
|
||||||
|
width: 45px;
|
||||||
|
top: -2.5px;
|
||||||
|
left: -2.5px;
|
||||||
|
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mine-wrapper .grid .field-wrapper:hover > img.field-target {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mine-wrapper .grid .field-wrapper > img.field-bomb-target {
|
||||||
|
position: absolute;
|
||||||
|
display: block;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mine-wrapper .grid .field-wrapper > img.field-blue-last,
|
||||||
|
#mine-wrapper .grid .field-wrapper > img.field-red-last {
|
||||||
|
position: absolute;
|
||||||
|
display: none;
|
||||||
|
width: 100%;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: 99;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mine-wrapper .grid .field-wrapper > img.field-blue-last.last-clicked,
|
||||||
|
#mine-wrapper .grid .field-wrapper > img.field-red-last.last-clicked {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mine-wrapper .grid .field-wrapper .field {
|
||||||
|
background: #61defa;
|
||||||
|
background: -moz-linear-gradient(left, #61defa 0%, #119dec 100%);
|
||||||
|
background: -webkit-linear-gradient(left, #61defa 0%, #119dec 100%);
|
||||||
|
background: linear-gradient(to right, #61defa 0%, #119dec 100%);
|
||||||
|
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#61defa', endColorstr='#119dec', GradientType=1);
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border: 2px solid #51c2fe;
|
||||||
|
font-family: 'Open Sans', sans-serif;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 35px;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 35px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mine-wrapper .grid .field-wrapper .field .field-corner {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mine-wrapper .grid .field-wrapper .field.active {
|
||||||
|
background: #fde717;
|
||||||
|
background: -moz-linear-gradient(left, #fde717 0%, #f5b807 100%);
|
||||||
|
background: -webkit-linear-gradient(left, #fde717 0%, #f5b807 100%);
|
||||||
|
background: linear-gradient(to right, #fde717 0%, #f5b807 100%);
|
||||||
|
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fde717', endColorstr='#f5b807', GradientType=1);
|
||||||
|
border: 2px solid #f6d762;
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mine-wrapper .grid .field-wrapper .field.active .flag-number {
|
||||||
|
-webkit-animation: bubbleNumber 500ms cubic-bezier(.36, .07, .19, .97) both;
|
||||||
|
animation: bubbleNumber 500ms cubic-bezier(.36, .07, .19, .97) both;
|
||||||
|
-webkit-transform: scale(1);
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes bubbleNumber {
|
||||||
|
0% {
|
||||||
|
background: #61defa;
|
||||||
|
background: -moz-linear-gradient(left, #61defa 0%, #119dec 100%);
|
||||||
|
background: -webkit-linear-gradient(left, #61defa 0%, #119dec 100%);
|
||||||
|
background: linear-gradient(to right, #61defa 0%, #119dec 100%);
|
||||||
|
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#61defa', endColorstr='#119dec', GradientType=1);
|
||||||
|
|
||||||
|
-webkit-border-radius: 50%;
|
||||||
|
border-radius: 50%;
|
||||||
|
-webkit-transform: scale(1);
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
-webkit-border-radius: 50%;
|
||||||
|
border-radius: 50%;
|
||||||
|
-webkit-transform: scale(0);
|
||||||
|
transform: scale(0);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
-webkit-border-radius: 0;
|
||||||
|
border-radius: 0;
|
||||||
|
-webkit-transform: scale(1);
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#mine-wrapper .grid .field-wrapper .field.active .flag-mine {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mine-wrapper .grid .field-wrapper .field.active .flag-mine > img {
|
||||||
|
width: 75%;
|
||||||
|
margin-left: 15px;
|
||||||
|
|
||||||
|
-ms-transform: rotate(7deg);
|
||||||
|
-webkit-transform: rotate(7deg);
|
||||||
|
transform: rotate(7deg);
|
||||||
|
-webkit-animation: mineFlagLoad 500ms cubic-bezier(.36, .07, .19, .97) both;
|
||||||
|
animation: mineFlagLoad 500ms cubic-bezier(.36, .07, .19, .97) both;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes mineFlagLoad {
|
||||||
|
0% {
|
||||||
|
margin-bottom: 0;
|
||||||
|
margin-left: 15px;
|
||||||
|
-ms-transform: rotate(9deg);
|
||||||
|
-webkit-transform: rotate(9deg);
|
||||||
|
transform: rotate(9deg);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
margin-bottom: -5px;
|
||||||
|
margin-left: 7px;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
margin-bottom: 3px;
|
||||||
|
margin-left: 0;
|
||||||
|
-ms-transform: rotate(-9deg);
|
||||||
|
-webkit-transform: rotate(-9deg);
|
||||||
|
transform: rotate(-9deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#mine-wrapper .grid .field-wrapper .field.active .flag-mine .flag-mine-base {
|
||||||
|
position: absolute;
|
||||||
|
background: #000000;
|
||||||
|
width: 25px;
|
||||||
|
height: 22px;
|
||||||
|
bottom: -12px;
|
||||||
|
left: 50%;
|
||||||
|
margin-left: -10.5px;
|
||||||
|
|
||||||
|
-webkit-border-radius: 50%;
|
||||||
|
border-radius: 50%;
|
||||||
|
-webkit-animation: mineBaseLoad 500ms cubic-bezier(.36, .07, .19, .97) both;
|
||||||
|
animation: mineBaseLoad 500ms cubic-bezier(.36, .07, .19, .97) both;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes mineBaseLoad {
|
||||||
|
0% { margin-bottom: 0; }
|
||||||
|
50% { margin-bottom: -5px; }
|
||||||
|
100% { margin-bottom: 0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
#mine-wrapper .grid .field-wrapper .field.active.mine {
|
||||||
|
background: #61defa;
|
||||||
|
background: -moz-linear-gradient(left, #61defa 0%, #119dec 100%);
|
||||||
|
background: -webkit-linear-gradient(left, #61defa 0%, #119dec 100%);
|
||||||
|
background: linear-gradient(to right, #61defa 0%, #119dec 100%);
|
||||||
|
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#61defa', endColorstr='#119dec', GradientType=1);
|
||||||
|
border: 2px solid #51c2fe;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mine-wrapper .grid .field-wrapper .field.active.color-1 { color: #0000ff; }
|
||||||
|
#mine-wrapper .grid .field-wrapper .field.active.color-2 { color: #079433; }
|
||||||
|
#mine-wrapper .grid .field-wrapper .field.active.color-3 { color: #fd1400; }
|
||||||
|
#mine-wrapper .grid .field-wrapper .field.active.color-4 { color: #0c099e; }
|
||||||
|
#mine-wrapper .grid .field-wrapper .field.active.color-5 { color: #7b4c01; }
|
||||||
|
#mine-wrapper .grid .field-wrapper .field.active.color-6 { color: #008388; }
|
||||||
|
#mine-wrapper .grid .field-wrapper .field.active.color-7 { color: #000000; }
|
||||||
|
#mine-wrapper .grid .field-wrapper .field.active.color-8 { color: #ff0000; }
|
||||||
|
|
||||||
|
#mine-wrapper .grid .field-wrapper .field img {
|
||||||
|
width: 80%;
|
||||||
|
}
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
/*!*
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#mine-wrapper .game-wrapper .users .active-mines-container {
|
||||||
|
background: -moz-radial-gradient(center, ellipse cover, rgba(255, 252, 252, 1) 0%, rgba(255, 252, 252, 0.99) 1%, rgba(106, 106, 106, 0.39) 61%, rgba(106, 106, 106, 0) 100%);
|
||||||
|
background: -webkit-radial-gradient(center, ellipse cover, rgba(255, 252, 252, 1) 0%, rgba(255, 252, 252, 0.99) 1%, rgba(106, 106, 106, 0.39) 61%, rgba(106, 106, 106, 0) 100%);
|
||||||
|
background: radial-gradient(ellipse at center, rgba(255, 252, 252, 1) 0%, rgba(255, 252, 252, 0.99) 1%, rgba(106, 106, 106, 0.39) 61%, rgba(106, 106, 106, 0) 100%);
|
||||||
|
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcfc', endColorstr='#006a6a6a', GradientType=1);
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: center center;
|
||||||
|
background-size: 72% 179%;
|
||||||
|
position: relative;
|
||||||
|
height: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mine-wrapper .game-wrapper .users .active-mines-container i {
|
||||||
|
font-size: 27px;
|
||||||
|
color: #b1b1b3;
|
||||||
|
margin-top: 3px;
|
||||||
|
|
||||||
|
text-shadow: 0 0 3px #000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mine-wrapper .game-wrapper .users .active-mines-container i:first-child {
|
||||||
|
float: left;
|
||||||
|
margin-left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mine-wrapper .game-wrapper .users .active-mines-container i:last-child {
|
||||||
|
float: right;
|
||||||
|
margin-right: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mine-wrapper .game-wrapper .users .active-mines-container .active-mines {
|
||||||
|
background: -moz-linear-gradient(top, rgba(0, 0, 0, 1) 0%, rgba(135, 136, 131, 1) 100%);
|
||||||
|
background: -webkit-linear-gradient(top, rgba(0, 0, 0, 1) 0%, rgba(135, 136, 131, 1) 100%);
|
||||||
|
background: linear-gradient(to bottom, rgba(0, 0, 0, 1) 0%, rgba(135, 136, 131, 1) 100%);
|
||||||
|
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#3e3f41', endColorstr='#878883', GradientType=0);
|
||||||
|
position: absolute;
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
top: -7.5px;
|
||||||
|
left: 50%;
|
||||||
|
font-family: 'Open Sans', sans-serif;
|
||||||
|
font-size: 25px;
|
||||||
|
line-height: 39px;
|
||||||
|
text-align: center;
|
||||||
|
color: #FFFFFF;
|
||||||
|
border: 5px solid #000000;
|
||||||
|
margin-left: -25px;
|
||||||
|
|
||||||
|
z-index: 100;
|
||||||
|
|
||||||
|
-webkit-border-radius: 50%;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mine-wrapper .game-wrapper .users .active-mines-container .active-mines.found-mine {
|
||||||
|
-webkit-animation: bubbleLeftMine 750ms cubic-bezier(.36, .07, .19, .97) both;
|
||||||
|
animation: bubbleLeftMine 750ms cubic-bezier(.36, .07, .19, .97) both;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes bubbleLeftMine {
|
||||||
|
0% { -webkit-transform: scale(1); transform: scale(1); }
|
||||||
|
50% { border-color: #2e3337; -webkit-transform: scale(2); transform: scale(2); }
|
||||||
|
100% { -webkit-transform: scale(1); transform: scale(1); }
|
||||||
|
}
|
||||||
|
|
||||||
|
#mine-wrapper .game-wrapper .users .active-mines-container .active-mines .active-mines-shine {
|
||||||
|
background: -moz-linear-gradient(top, rgba(213, 214, 216, 1) 0%, rgba(106, 106, 106, 1) 100%);
|
||||||
|
background: -webkit-linear-gradient(top, rgba(213, 214, 216, 1) 0%, rgba(106, 106, 106, 1) 100%);
|
||||||
|
background: linear-gradient(to bottom, rgba(213, 214, 216, 1) 0%, rgba(106, 106, 106, 1) 100%);
|
||||||
|
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#d5d6d8', endColorstr='#6a6a6a', GradientType=0);
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 50%;
|
||||||
|
width: 30px;
|
||||||
|
height: 20px;
|
||||||
|
margin-left: -14.5px;
|
||||||
|
|
||||||
|
z-index: 101;
|
||||||
|
|
||||||
|
-webkit-border-radius: 50%;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mine-wrapper .game-wrapper .users .active-mines-container .active-mines .active-mines-nbr {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
z-index: 102;
|
||||||
|
}
|
||||||
@@ -0,0 +1,780 @@
|
|||||||
|
/*!*
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#mine-wrapper .game-wrapper .game-overlay {
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
|
||||||
|
z-index: 200;
|
||||||
|
|
||||||
|
-webkit-border-radius: 10px;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mine-wrapper .game-wrapper .game-overlay.hide {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mine-wrapper .game-wrapper .game-overlay .game-overlay-window {
|
||||||
|
background: linear-gradient(135deg, rgba(7, 9, 13, 0.98) 0%, rgba(10, 20, 35, 0.98) 100%);
|
||||||
|
border: 2px solid rgba(35, 111, 135, 0.4);
|
||||||
|
backdrop-filter: blur(12px);
|
||||||
|
font-family: 'Rajdhani', sans-serif;
|
||||||
|
color: #fff;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 680px;
|
||||||
|
padding: 40px;
|
||||||
|
border-radius: 16px;
|
||||||
|
box-shadow: 0 25px 50px rgba(0, 0, 0, 0.7), 0 0 40px rgba(35, 111, 135, 0.15);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0;
|
||||||
|
animation: slideUp 0.5s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||||
|
overflow: hidden;
|
||||||
|
max-height: 90vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideUp {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(30px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#mine-wrapper .game-wrapper .game-overlay .game-overlay-window h1 {
|
||||||
|
font-weight: 800;
|
||||||
|
font-size: 32px;
|
||||||
|
color: #fff;
|
||||||
|
margin: 0 0 50px 0;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mine-wrapper .game-wrapper .game-overlay .game-overlay-window h2 {
|
||||||
|
font-size: 14px;
|
||||||
|
color: rgba(149, 207, 245, 0.6);
|
||||||
|
margin: 0 0 30px 0;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mine-wrapper .game-wrapper .game-overlay .game-overlay-window .resign {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
width: 100%;
|
||||||
|
text-transform: none;
|
||||||
|
letter-spacing: normal;
|
||||||
|
font-size: inherit;
|
||||||
|
|
||||||
|
a {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 10px;
|
||||||
|
background: linear-gradient(to bottom, #236f87 0%, #1a5068 100%);
|
||||||
|
border: 2px solid #2e7a9a;
|
||||||
|
color: #e0f4ff;
|
||||||
|
font: 800 13px 'Rajdhani', sans-serif;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
padding: 12px 24px;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 300ms cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||||
|
width: 100%;
|
||||||
|
max-width: 260px;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 4px 12px rgba(35, 111, 135, 0.25);
|
||||||
|
text-decoration: none;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: -100%;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
|
||||||
|
transition: left 0.4s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: linear-gradient(to bottom, #2d8aa8 0%, #236f87 100%);
|
||||||
|
border-color: #5ba4d4;
|
||||||
|
color: #fff;
|
||||||
|
box-shadow: 0 8px 24px rgba(35, 111, 135, 0.4);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
left: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
background: linear-gradient(to bottom, #8a2323 0%, #681a1a 100%);
|
||||||
|
border-color: #9a2e2e;
|
||||||
|
color: #ffe0e0;
|
||||||
|
box-shadow: 0 4px 12px rgba(135, 35, 35, 0.25);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: linear-gradient(to bottom, #a82d2d 0%, #872323 100%);
|
||||||
|
border-color: #d45b5b;
|
||||||
|
color: #fff;
|
||||||
|
box-shadow: 0 8px 24px rgba(135, 35, 35, 0.4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#mine-wrapper .game-wrapper .game-overlay .game-overlay-window:has(.resign) h1 {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mine-wrapper .game-wrapper .game-overlay .game-overlay-window:has(.resign) h2 {
|
||||||
|
text-align: center;
|
||||||
|
color: inherit;
|
||||||
|
font-size: inherit;
|
||||||
|
letter-spacing: normal;
|
||||||
|
text-transform: none;
|
||||||
|
font-weight: normal;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mine-wrapper .game-wrapper .game-overlay .game-overlay-window h3 {
|
||||||
|
font-size: 16px;
|
||||||
|
color: #236f87;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mine-wrapper .game-wrapper .game-overlay .game-overlay-window .share-invite {
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mine-wrapper .game-wrapper .game-overlay .game-overlay-window .share-invite-label {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #386e8c;
|
||||||
|
margin: 0 0 8px;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.waiting-options {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr auto 1fr;
|
||||||
|
grid-template-rows: auto;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: stretch;
|
||||||
|
width: 100%;
|
||||||
|
animation: fadeInUp 0.6s ease-out 0.2s both;
|
||||||
|
|
||||||
|
&.waiting-options--invite-only {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.waiting-option {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 15px;
|
||||||
|
background: linear-gradient(135deg, rgba(35, 111, 135, 0.08) 0%, rgba(26, 80, 104, 0.08) 100%);
|
||||||
|
border: 2px solid rgba(35, 111, 135, 0.2);
|
||||||
|
border-radius: 12px;
|
||||||
|
transition: all 350ms cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
animation: scaleIn 0.5s ease-out;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: -100%;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(90deg, transparent, rgba(35, 111, 135, 0.15), transparent);
|
||||||
|
transition: left 0.5s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: rgba(35, 111, 135, 0.45);
|
||||||
|
background: linear-gradient(135deg, rgba(35, 111, 135, 0.12) 0%, rgba(26, 80, 104, 0.12) 100%);
|
||||||
|
transform: translateY(-4px);
|
||||||
|
box-shadow: 0 12px 30px rgba(35, 111, 135, 0.2);
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
left: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.waiting-option-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 14px;
|
||||||
|
font-weight: 800;
|
||||||
|
font-size: 17px;
|
||||||
|
color: #fff;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
|
||||||
|
i {
|
||||||
|
font-size: 20px;
|
||||||
|
color: #fff;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 42px;
|
||||||
|
height: 42px;
|
||||||
|
background: linear-gradient(135deg, rgba(35, 111, 135, 0.4) 0%, rgba(35, 111, 135, 0.2) 100%);
|
||||||
|
border: 2px solid rgba(35, 111, 135, 0.5);
|
||||||
|
border-radius: 8px;
|
||||||
|
transition: all 400ms cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover i {
|
||||||
|
background: linear-gradient(135deg, rgba(35, 111, 135, 0.6) 0%, rgba(35, 111, 135, 0.4) 100%);
|
||||||
|
border-color: rgba(35, 111, 135, 0.8);
|
||||||
|
transform: scale(1.15) rotate(-8deg);
|
||||||
|
box-shadow: 0 0 20px rgba(35, 111, 135, 0.4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.waiting-option-desc {
|
||||||
|
font: 600 12px 'Rajdhani', sans-serif;
|
||||||
|
color: rgba(149, 207, 245, 0.75);
|
||||||
|
margin: 0;
|
||||||
|
letter-spacing: 0.4px;
|
||||||
|
line-height: 1.4;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 3;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
.waiting-divider {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
margin: 0;
|
||||||
|
animation: slideIn 0.7s ease-out 0.4s both;
|
||||||
|
|
||||||
|
&::before,
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
width: 2px;
|
||||||
|
height: 20px;
|
||||||
|
background: linear-gradient(
|
||||||
|
to bottom,
|
||||||
|
rgba(35, 111, 135, 0.1),
|
||||||
|
rgba(35, 111, 135, 0.4),
|
||||||
|
rgba(35, 111, 135, 0.1)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
font: 700 11px 'Rajdhani', sans-serif;
|
||||||
|
color: rgba(35, 111, 135, 0.6);
|
||||||
|
letter-spacing: 2px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
padding: 0 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
flex-direction: row;
|
||||||
|
margin: 8px 0;
|
||||||
|
|
||||||
|
&::before,
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
flex: 1;
|
||||||
|
width: auto;
|
||||||
|
height: 1px;
|
||||||
|
background: linear-gradient(
|
||||||
|
to right,
|
||||||
|
rgba(35, 111, 135, 0),
|
||||||
|
rgba(35, 111, 135, 0.3),
|
||||||
|
rgba(35, 111, 135, 0)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#mine-wrapper .game-wrapper .game-overlay .game-overlay-window .share-invite {
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mine-wrapper .game-wrapper .game-overlay .game-overlay-window .share-url-box {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
background: linear-gradient(135deg, #d0e8f5 0%, #c5dff0 100%);
|
||||||
|
border: 2px solid #7ab8d8;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 0 10px;
|
||||||
|
cursor: text;
|
||||||
|
transition: all 300ms ease;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: #236f87;
|
||||||
|
box-shadow: 0 4px 12px rgba(35, 111, 135, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus-within {
|
||||||
|
border-color: #236f87;
|
||||||
|
box-shadow: 0 0 16px rgba(35, 111, 135, 0.35);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#mine-wrapper .game-wrapper .game-overlay .game-overlay-window .share-url-icon {
|
||||||
|
color: #236f87;
|
||||||
|
font-size: 13px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-right: 8px;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mine-wrapper .game-wrapper .game-overlay .game-overlay-window .share-url-input {
|
||||||
|
flex: 1;
|
||||||
|
background: transparent;
|
||||||
|
border: 0;
|
||||||
|
outline: 0;
|
||||||
|
height: 40px;
|
||||||
|
color: #1a4a6a;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
text-align: center;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
cursor: text;
|
||||||
|
min-width: 0;
|
||||||
|
|
||||||
|
&::selection {
|
||||||
|
background: rgba(35, 111, 135, 0.3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeInUp {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes scaleIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#mine-wrapper .game-wrapper .game-overlay .game-overlay-window .browse-players-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 10px;
|
||||||
|
background: linear-gradient(to bottom, #236f87 0%, #1a5068 100%);
|
||||||
|
border: 2px solid #2e7a9a;
|
||||||
|
color: #e0f4ff;
|
||||||
|
font: 700 13px 'Rajdhani', sans-serif;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
padding: 12px 24px;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 300ms cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||||
|
width: 100%;
|
||||||
|
font-weight: 800;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 4px 12px rgba(35, 111, 135, 0.25);
|
||||||
|
|
||||||
|
i {
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: -100%;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
|
||||||
|
transition: left 0.4s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: linear-gradient(to bottom, #2d8aa8 0%, #236f87 100%);
|
||||||
|
border-color: #5ba4d4;
|
||||||
|
color: #fff;
|
||||||
|
box-shadow: 0 8px 24px rgba(35, 111, 135, 0.4);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
left: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#mine-wrapper .game-wrapper .game-overlay .game-overlay-window .share-copy-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 9px;
|
||||||
|
background: linear-gradient(to bottom, #236f87 0%, #1a5068 100%);
|
||||||
|
border: 2px solid #2e7a9a;
|
||||||
|
color: #e0f4ff;
|
||||||
|
font-family: 'Rajdhani', sans-serif;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
padding: 12px 24px;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 300ms cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 4px 12px rgba(35, 111, 135, 0.25);
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: -100%;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
|
||||||
|
transition: left 0.4s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: linear-gradient(to bottom, #2d8aa8 0%, #236f87 100%);
|
||||||
|
border-color: #5ba4d4;
|
||||||
|
color: #fff;
|
||||||
|
box-shadow: 0 8px 24px rgba(35, 111, 135, 0.4);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
left: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.copied {
|
||||||
|
background: linear-gradient(to bottom, #1a6844 0%, #135233 100%);
|
||||||
|
border-color: #2a9e60;
|
||||||
|
color: #a0f0c0;
|
||||||
|
box-shadow: 0 4px 12px rgba(26, 104, 68, 0.4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#mine-wrapper .game-wrapper .game-overlay .game-overlay-window .game-overlay-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: 12px;
|
||||||
|
margin-top: 20px;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
> * {
|
||||||
|
flex: 1 1 0;
|
||||||
|
margin-top: 0 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#mine-wrapper .game-wrapper .game-overlay .game-overlay-window .game-overlay-share {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 9px;
|
||||||
|
background: linear-gradient(to bottom, #236f87 0%, #1a5068 100%);
|
||||||
|
border: 2px solid #2e7a9a;
|
||||||
|
color: #e0f4ff;
|
||||||
|
font-family: 'Rajdhani', sans-serif;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
padding: 12px 24px;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 300ms cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 20px;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 4px 12px rgba(35, 111, 135, 0.25);
|
||||||
|
z-index: 10;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: -100%;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
|
||||||
|
transition: left 0.4s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: linear-gradient(to bottom, #2d8aa8 0%, #236f87 100%);
|
||||||
|
border-color: #5ba4d4;
|
||||||
|
color: #fff;
|
||||||
|
box-shadow: 0 8px 24px rgba(35, 111, 135, 0.4);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
left: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.copied {
|
||||||
|
background: linear-gradient(to bottom, #1a6844 0%, #135233 100%);
|
||||||
|
border-color: #2a9e60;
|
||||||
|
color: #a0f0c0;
|
||||||
|
box-shadow: 0 4px 12px rgba(26, 104, 68, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
i {
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#mine-wrapper .game-wrapper .game-overlay .game-overlay-window .game-overlay-profile {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 9px;
|
||||||
|
background: linear-gradient(to bottom, #1a6844 0%, #135233 100%);
|
||||||
|
border: 2px solid #2a9e60;
|
||||||
|
color: #d0ffe0;
|
||||||
|
font-family: 'Rajdhani', sans-serif;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
padding: 12px 24px;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 300ms cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 20px;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 4px 12px rgba(42, 158, 96, 0.25);
|
||||||
|
text-decoration: none;
|
||||||
|
z-index: 10;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: -100%;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
|
||||||
|
transition: left 0.4s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: linear-gradient(to bottom, #238f5c 0%, #1a6844 100%);
|
||||||
|
border-color: #5ee89a;
|
||||||
|
color: #fff;
|
||||||
|
box-shadow: 0 8px 24px rgba(42, 158, 96, 0.4);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
left: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
i {
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CaptchaOverlay Styles
|
||||||
|
.captcha-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: rgba(7, 9, 13, 0.95);
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.captcha-content {
|
||||||
|
text-align: center;
|
||||||
|
color: #fff;
|
||||||
|
max-width: 400px;
|
||||||
|
padding: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.captcha-icon {
|
||||||
|
font-size: 64px;
|
||||||
|
color: #236f87;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.captcha-title {
|
||||||
|
font: 800 32px 'Rajdhani', sans-serif;
|
||||||
|
margin: 0 0 16px;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.captcha-description {
|
||||||
|
color: rgba(149, 207, 245, 0.7);
|
||||||
|
font: 400 16px 'Rajdhani', sans-serif;
|
||||||
|
margin: 0 0 32px;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.captcha-button {
|
||||||
|
background: linear-gradient(#236f87 0%, #1a5068 100%);
|
||||||
|
border: 2px solid #2e7a9a;
|
||||||
|
border-radius: 8px;
|
||||||
|
color: #e0f4ff;
|
||||||
|
cursor: pointer;
|
||||||
|
font: 800 18px 'Rajdhani', sans-serif;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
padding: 16px 40px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
opacity: 1;
|
||||||
|
|
||||||
|
i {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
opacity: 0.7;
|
||||||
|
cursor: wait;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover:not(:disabled) {
|
||||||
|
background: linear-gradient(#2d8aa8 0%, #236f87 100%);
|
||||||
|
border-color: #5ba4d4;
|
||||||
|
color: #fff;
|
||||||
|
box-shadow: 0 8px 24px rgba(35, 111, 135, 0.4);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active:not(:disabled) {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.captcha-button--error {
|
||||||
|
background: linear-gradient(#8a2323 0%, #681a1a 100%);
|
||||||
|
border-color: #9a2e2e;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: linear-gradient(#a82d2d 0%, #872323 100%);
|
||||||
|
border-color: #d45b5b;
|
||||||
|
box-shadow: 0 8px 24px rgba(135, 35, 35, 0.4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.captcha-button--loading {
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
/*!*
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
@media screen and (max-width: 900px) {
|
||||||
|
#mine-wrapper .game-wrapper .users {
|
||||||
|
visibility: hidden;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mine-wrapper {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mine-wrapper .game-wrapper {
|
||||||
|
width: 100%;
|
||||||
|
flex-direction: column-reverse;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mine-wrapper .grid-container {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mine-wrapper .grid {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mine-wrapper .grid .field-wrapper {
|
||||||
|
width: 6.25%;
|
||||||
|
aspect-ratio: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mine-wrapper .grid .field-wrapper > img.field-target {
|
||||||
|
width: 105%;
|
||||||
|
top: -2.5%;
|
||||||
|
left: -2.5%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mine-wrapper .grid .field-wrapper .field {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
/*!*
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#mine-wrapper .game-timer-container {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mine-wrapper .game-timer {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-width: 115px;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 8px 18px;
|
||||||
|
font-family: 'Rajdhani', sans-serif;
|
||||||
|
font-weight: bold;
|
||||||
|
border: 2px solid transparent;
|
||||||
|
transition: all 0.4s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Red – waiting
|
||||||
|
#mine-wrapper .game-timer.red-timer {
|
||||||
|
background: linear-gradient(to bottom, #4a0603 0%, #6b2515 100%);
|
||||||
|
border-color: #7a1e10;
|
||||||
|
color: rgba(246, 125, 82, 0.55);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Red – active (thinking)
|
||||||
|
#mine-wrapper .game-timer.red-timer.active {
|
||||||
|
background: linear-gradient(to bottom, #ad0a05 0%, #f67d52 100%);
|
||||||
|
border-color: #ff9b6b;
|
||||||
|
color: #fff;
|
||||||
|
box-shadow: 0 0 16px rgba(173, 10, 5, 0.75), 0 0 5px rgba(246, 125, 82, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Blue – waiting
|
||||||
|
#mine-wrapper .game-timer.blue-timer {
|
||||||
|
background: linear-gradient(to bottom, #0b2530 0%, #163d55 100%);
|
||||||
|
border-color: #173650;
|
||||||
|
color: rgba(149, 207, 245, 0.55);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Blue – active (thinking)
|
||||||
|
#mine-wrapper .game-timer.blue-timer.active {
|
||||||
|
background: linear-gradient(to bottom, #236f87 0%, #95cff5 100%);
|
||||||
|
border-color: #b8e5ff;
|
||||||
|
color: #fff;
|
||||||
|
box-shadow: 0 0 16px rgba(35, 111, 135, 0.75), 0 0 5px rgba(149, 207, 245, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
#mine-wrapper .game-timer .timer-avatar {
|
||||||
|
width: 26px;
|
||||||
|
height: 26px;
|
||||||
|
border-radius: 50%;
|
||||||
|
overflow: hidden;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: bold;
|
||||||
|
font-family: 'Rajdhani', sans-serif;
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
#mine-wrapper .game-timer .timer-avatar__img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mine-wrapper .game-timer.red-timer .timer-avatar__initials {
|
||||||
|
color: rgba(246, 125, 82, 0.85);
|
||||||
|
}
|
||||||
|
|
||||||
|
#mine-wrapper .game-timer.blue-timer .timer-avatar__initials {
|
||||||
|
color: rgba(149, 207, 245, 0.85);
|
||||||
|
}
|
||||||
|
|
||||||
|
#mine-wrapper .game-timer .timer-icon {
|
||||||
|
font-size: 15px;
|
||||||
|
opacity: 0.7;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mine-wrapper .game-timer.active .timer-icon {
|
||||||
|
opacity: 1;
|
||||||
|
animation: timer-icon-pulse 1.6s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes timer-icon-pulse {
|
||||||
|
0%, 100% { transform: scale(1); opacity: 0.85; }
|
||||||
|
50% { transform: scale(1.2); opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
#mine-wrapper .game-timer .timer-display {
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 20px;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
}
|
||||||
@@ -0,0 +1,171 @@
|
|||||||
|
/*!*
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#mine-wrapper .game-wrapper .users {
|
||||||
|
width: 180px;
|
||||||
|
padding: 0 10px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mine-wrapper .game-wrapper .users .user-container {
|
||||||
|
background: #FFFFFF;
|
||||||
|
height: 40%;
|
||||||
|
font-family: 'Open Sans', sans-serif;
|
||||||
|
padding: 5px;
|
||||||
|
margin: 5px;
|
||||||
|
|
||||||
|
z-index: 99;
|
||||||
|
|
||||||
|
-webkit-border-radius: 10px;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mine-wrapper .game-wrapper .users .user-container.user-blue {
|
||||||
|
background: rgb(35, 111, 135);
|
||||||
|
background: -moz-linear-gradient(top, rgba(35, 111, 135, 1) 0%, rgba(149, 207, 245, 1) 100%);
|
||||||
|
background: -webkit-linear-gradient(top, rgba(35, 111, 135, 1) 0%, rgba(149, 207, 245, 1) 100%);
|
||||||
|
background: linear-gradient(to bottom, rgba(35, 111, 135, 1) 0%, rgba(149, 207, 245, 1) 100%);
|
||||||
|
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#236f87', endColorstr='#95cff5', GradientType=0);
|
||||||
|
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mine-wrapper .game-wrapper .users .user-container.user-red {
|
||||||
|
background: rgb(173, 10, 5);
|
||||||
|
background: -moz-linear-gradient(top, rgba(173, 10, 5, 1) 0%, rgba(246, 125, 82, 1) 100%);
|
||||||
|
background: -webkit-linear-gradient(top, rgba(173, 10, 5, 1) 0%, rgba(246, 125, 82, 1) 100%);
|
||||||
|
background: linear-gradient(to bottom, rgba(173, 10, 5, 1) 0%, rgba(246, 125, 82, 1) 100%);
|
||||||
|
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ad0a05', endColorstr='#f67d52', GradientType=0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#mine-wrapper .game-wrapper .users .user-container .user-header {
|
||||||
|
background: -moz-linear-gradient(top, rgba(255, 255, 255, 0.7) 39%, rgba(255, 255, 255, 0.21) 87%, rgba(0, 0, 0, 0) 100%);
|
||||||
|
background: -webkit-linear-gradient(top, rgba(255, 255, 255, 0.7) 39%, rgba(255, 255, 255, 0.21) 87%, rgba(0, 0, 0, 0) 100%);
|
||||||
|
background: linear-gradient(to bottom, rgba(255, 255, 255, 0.7) 39%, rgba(255, 255, 255, 0.21) 87%, rgba(0, 0, 0, 0) 100%);
|
||||||
|
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffff', endColorstr='#00000000', GradientType=0);
|
||||||
|
position: relative;
|
||||||
|
font: bolder 25px 'Changa One', cursive;
|
||||||
|
letter-spacing: 5px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
text-align: center;
|
||||||
|
padding: 6px 5px 20px 5px;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
|
||||||
|
-webkit-border-radius: 5px;
|
||||||
|
border-radius: 5px;
|
||||||
|
-webkit-text-shadow: 1px 1px 0 #FFF;
|
||||||
|
text-shadow: 1px 1px 0 #FFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mine-wrapper .game-wrapper .users .user-container.user-blue .user-header {
|
||||||
|
color: #236f87;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mine-wrapper .game-wrapper .users .user-container.user-red .user-header {
|
||||||
|
color: #AD0A05;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mine-wrapper .game-wrapper .users .user-container .user-header > img {
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
bottom: 0;
|
||||||
|
width: 40%;
|
||||||
|
margin-left: -20%;
|
||||||
|
margin-bottom: -25%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mine-wrapper .game-wrapper .users .user-container .user-header > img.user-cursor {
|
||||||
|
display: block;
|
||||||
|
width: 30%;
|
||||||
|
top: 20px;
|
||||||
|
left: 10px;
|
||||||
|
margin-left: 0;
|
||||||
|
|
||||||
|
-webkit-animation: cursorJumping 1.2s cubic-bezier(.36, .07, .19, .97) infinite;
|
||||||
|
animation: cursorJumping 1.2s cubic-bezier(.36, .07, .19, .97) infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mine-wrapper .game-wrapper .users .user-container .user-header > img.user-cursor::after {
|
||||||
|
content: '';
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
background: #1A6844;
|
||||||
|
animation: animate .5s linear infinite;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes cursorJumping {
|
||||||
|
0% { top: 15px; }
|
||||||
|
50% { top: 25px; }
|
||||||
|
100% { top: 15px; }
|
||||||
|
}
|
||||||
|
|
||||||
|
#mine-wrapper .game-wrapper .users .user-container .user-name {
|
||||||
|
min-height: 30px;
|
||||||
|
font-weight: normal;
|
||||||
|
text-align: center;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
padding: 3px 5px;
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
overflow: hidden;
|
||||||
|
word-break: break-word;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mine-wrapper .game-wrapper .users .user-container.user-blue .user-name {
|
||||||
|
border-top: 1px dashed #0b3776;
|
||||||
|
border-bottom: 1px dashed #0b3776;
|
||||||
|
color: #0b3776;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mine-wrapper .game-wrapper .users .user-container.user-red .user-name {
|
||||||
|
color: #fdf612;
|
||||||
|
border-top: 1px dashed #fdf612;
|
||||||
|
border-bottom: 1px dashed #fdf612;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mine-wrapper .game-wrapper .users .user-container .user-caret {
|
||||||
|
height: 30px;
|
||||||
|
font-size: 30px;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mine-wrapper .game-wrapper .users .user-container.user-blue .user-caret > i {
|
||||||
|
color: #0b3776;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mine-wrapper .game-wrapper .users .user-container.user-red .user-caret > i {
|
||||||
|
color: #fdf612;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mine-wrapper .game-wrapper .users .user-container .user-desc {
|
||||||
|
height: 65px;
|
||||||
|
font-size: 14px;
|
||||||
|
text-align: center;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 3;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
word-break: break-word;
|
||||||
|
padding: 0 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mine-wrapper .game-wrapper .users .user-container.user-blue .user-desc {
|
||||||
|
color: #0b3776;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mine-wrapper .game-wrapper .users .user-container.user-red .user-desc {
|
||||||
|
color: #fdf612;
|
||||||
|
}
|
||||||
@@ -0,0 +1,356 @@
|
|||||||
|
/*!*
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.opd-paper {
|
||||||
|
background: #07090d !important;
|
||||||
|
background-image: linear-gradient(rgba(35, 111, 135, 0.08) 1px, transparent 1px),
|
||||||
|
linear-gradient(90deg, rgba(35, 111, 135, 0.08) 1px, transparent 1px) !important;
|
||||||
|
background-size: 46px 46px !important;
|
||||||
|
border: 1px solid rgba(35, 111, 135, 0.4) !important;
|
||||||
|
border-radius: 12px !important;
|
||||||
|
box-shadow: 0 0 80px rgba(35, 111, 135, 0.15),
|
||||||
|
0 32px 80px rgba(0, 0, 0, 0.9) !important;
|
||||||
|
width: 500px;
|
||||||
|
max-width: 94vw !important;
|
||||||
|
overflow: hidden !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opd {
|
||||||
|
padding: 28px 28px 22px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opd-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 22px;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opd-header-text {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opd-header-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opd-label {
|
||||||
|
display: block;
|
||||||
|
font: 700 11px 'Rajdhani', sans-serif;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 4px;
|
||||||
|
color: rgba(149, 207, 245, 0.55);
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opd-title {
|
||||||
|
font: 800 28px 'Rajdhani', sans-serif;
|
||||||
|
color: #fff;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 11px;
|
||||||
|
|
||||||
|
i {
|
||||||
|
color: rgba(35, 111, 135, 0.9);
|
||||||
|
font-size: 22px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.opd-refresh,
|
||||||
|
.opd-close {
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid rgba(35, 111, 135, 0.3);
|
||||||
|
border-radius: 6px;
|
||||||
|
color: rgba(149, 207, 245, 0.55);
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 200ms ease;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
&:hover:not(:disabled) {
|
||||||
|
border-color: rgba(149, 207, 245, 0.5);
|
||||||
|
color: #fff;
|
||||||
|
background: rgba(35, 111, 135, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
opacity: 0.4;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.opd-refresh--spin i {
|
||||||
|
animation: opd-spin 0.7s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes opd-spin {
|
||||||
|
from {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.opd-search-wrap {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
background: rgba(35, 111, 135, 0.07);
|
||||||
|
border: 1px solid rgba(35, 111, 135, 0.28);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 0 14px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
transition: border-color 200ms ease, background 200ms ease;
|
||||||
|
|
||||||
|
&:focus-within {
|
||||||
|
border-color: rgba(35, 111, 135, 0.65);
|
||||||
|
background: rgba(35, 111, 135, 0.12);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.opd-search-icon {
|
||||||
|
color: rgba(149, 207, 245, 0.38);
|
||||||
|
font-size: 13px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opd-search {
|
||||||
|
flex: 1;
|
||||||
|
background: transparent;
|
||||||
|
border: 0;
|
||||||
|
outline: 0;
|
||||||
|
height: 44px;
|
||||||
|
font: 400 14px 'Rajdhani', sans-serif;
|
||||||
|
letter-spacing: 0.3px;
|
||||||
|
color: #fff;
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: rgba(149, 207, 245, 0.32);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.opd-search-clear {
|
||||||
|
background: transparent;
|
||||||
|
border: 0;
|
||||||
|
color: rgba(149, 207, 245, 0.4);
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0 0 0 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
transition: color 150ms ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: rgba(149, 207, 245, 0.8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.opd-list {
|
||||||
|
min-height: 110px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opd-empty {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 30px 0 22px;
|
||||||
|
gap: 12px;
|
||||||
|
|
||||||
|
i {
|
||||||
|
font-size: 34px;
|
||||||
|
color: rgba(35, 111, 135, 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
font: 400 14px 'Rajdhani', sans-serif;
|
||||||
|
color: rgba(255, 255, 255, 0.38);
|
||||||
|
letter-spacing: 0.4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.opd-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 14px;
|
||||||
|
padding: 11px 12px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
transition: all 180ms ease;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(35, 111, 135, 0.1);
|
||||||
|
border-color: rgba(35, 111, 135, 0.28);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.opd-avatar {
|
||||||
|
width: 42px;
|
||||||
|
height: 42px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: linear-gradient(135deg, rgba(35, 111, 135, 0.55) 0%, rgba(35, 111, 135, 0.28) 100%);
|
||||||
|
border: 1px solid rgba(35, 111, 135, 0.5);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font: 700 13px 'Rajdhani', sans-serif;
|
||||||
|
color: rgba(149, 207, 245, 0.9);
|
||||||
|
letter-spacing: 1px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opd-info {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opd-name {
|
||||||
|
font: 700 15px 'Rajdhani', sans-serif;
|
||||||
|
color: #fff;
|
||||||
|
letter-spacing: 0.3px;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opd-since {
|
||||||
|
font: 400 12px 'Rajdhani', sans-serif;
|
||||||
|
color: rgba(149, 207, 245, 0.48);
|
||||||
|
|
||||||
|
i {
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.opd-join {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
background: linear-gradient(to bottom, rgba(35, 111, 135, 0.75) 0%, rgba(26, 80, 104, 0.9) 100%);
|
||||||
|
border: 1px solid rgba(35, 111, 135, 0.55);
|
||||||
|
color: rgba(149, 207, 245, 0.9);
|
||||||
|
font: 700 12px 'Rajdhani', sans-serif;
|
||||||
|
letter-spacing: 1.5px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
text-decoration: none;
|
||||||
|
padding: 7px 16px;
|
||||||
|
border-radius: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 200ms ease;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
&:hover:not(:disabled) {
|
||||||
|
background: linear-gradient(to bottom, rgba(45, 138, 168, 0.9) 0%, rgba(35, 111, 135, 0.95) 100%);
|
||||||
|
border-color: rgba(149, 207, 245, 0.5);
|
||||||
|
color: #fff;
|
||||||
|
box-shadow: 0 0 14px rgba(35, 111, 135, 0.5);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
cursor: default;
|
||||||
|
opacity: 0.55;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.opd-join--waiting {
|
||||||
|
background: linear-gradient(to bottom, rgba(26, 80, 104, 0.6) 0%, rgba(15, 50, 70, 0.7) 100%);
|
||||||
|
border-color: rgba(35, 111, 135, 0.3);
|
||||||
|
color: rgba(149, 207, 245, 0.6);
|
||||||
|
opacity: 1;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.opd-declined {
|
||||||
|
font: 600 12px 'Rajdhani', sans-serif;
|
||||||
|
color: rgba(255, 120, 120, 0.85);
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid rgba(180, 60, 60, 0.3);
|
||||||
|
background: rgba(180, 60, 60, 0.08);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 7px;
|
||||||
|
|
||||||
|
i {
|
||||||
|
font-size: 14px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.opd-note {
|
||||||
|
font: 400 11px 'Rajdhani', sans-serif;
|
||||||
|
color: rgba(149, 207, 245, 0.32);
|
||||||
|
text-align: center;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
margin-top: 14px;
|
||||||
|
padding-top: 14px;
|
||||||
|
border-top: 1px solid rgba(35, 111, 135, 0.14);
|
||||||
|
}
|
||||||
|
|
||||||
|
.opd-header-actions {
|
||||||
|
.opd-refresh[disabled] {
|
||||||
|
opacity: 0.4;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opd-close[disabled] {
|
||||||
|
opacity: 0.4;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.opd-waiting {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 14px 16px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
background: rgba(35, 111, 135, 0.07);
|
||||||
|
border: 1px solid rgba(35, 111, 135, 0.28);
|
||||||
|
border-radius: 8px;
|
||||||
|
color: #95cff5;
|
||||||
|
|
||||||
|
i {
|
||||||
|
font-size: 16px;
|
||||||
|
animation: opd-hourglass 1s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
font: 600 14px 'Rajdhani', sans-serif;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes opd-hourglass {
|
||||||
|
0%, 100% { transform: rotate(0deg); }
|
||||||
|
50% { transform: rotate(180deg); }
|
||||||
|
}
|
||||||
@@ -0,0 +1,301 @@
|
|||||||
|
/*!*
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
@use "sass:color";
|
||||||
|
|
||||||
|
.twofa-status {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 10px 14px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font: 600 13px 'Rajdhani', sans-serif;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
|
||||||
|
&--enabled {
|
||||||
|
background: rgba(42, 158, 96, 0.12);
|
||||||
|
border: 1px solid rgba(42, 158, 96, 0.3);
|
||||||
|
color: #5ee89a;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--disabled {
|
||||||
|
background: rgba(255, 255, 255, 0.03);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
|
color: rgba(255, 255, 255, 0.4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.twofa-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 20px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
&__form {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.twofa-backup-meta {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
&__count {
|
||||||
|
font: 500 13px 'Rajdhani', sans-serif;
|
||||||
|
color: rgba(149, 207, 245, 0.55);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.twofa-backup-reveal {
|
||||||
|
background: rgba(246, 125, 82, 0.07);
|
||||||
|
border: 1px solid rgba(246, 125, 82, 0.25);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 18px 20px;
|
||||||
|
|
||||||
|
&__warning {
|
||||||
|
font: 600 12px 'Rajdhani', sans-serif;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 1.5px;
|
||||||
|
color: #f6a060;
|
||||||
|
margin: 0 0 14px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.twofa-backup-code {
|
||||||
|
display: block;
|
||||||
|
text-align: center;
|
||||||
|
padding: 7px 10px;
|
||||||
|
background: rgba(0, 0, 0, 0.35);
|
||||||
|
border: 1px solid rgba(246, 125, 82, 0.2);
|
||||||
|
border-radius: 4px;
|
||||||
|
font: 700 13px 'Courier New', monospace;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
color: #e0c890;
|
||||||
|
user-select: all;
|
||||||
|
}
|
||||||
|
|
||||||
|
$primary: #236f87;
|
||||||
|
$primary-dark: #1a5a70;
|
||||||
|
$danger: #c0392b;
|
||||||
|
$warning: #d68910;
|
||||||
|
$success: #388e3c;
|
||||||
|
$text: #e0e0e0;
|
||||||
|
$text-muted: #9e9e9e;
|
||||||
|
$border: rgba(35, 111, 135, 0.3);
|
||||||
|
$bg-card: #0a0e14;
|
||||||
|
$bg-hover: rgba(35, 111, 135, 0.15);
|
||||||
|
|
||||||
|
.passkey-manager {
|
||||||
|
&__actions {
|
||||||
|
margin: 20px 0;
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 15px;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.passkey-item {
|
||||||
|
border: 1px solid $border;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 15px;
|
||||||
|
background: $bg-card;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: $bg-hover;
|
||||||
|
border-color: rgba(35, 111, 135, 0.5);
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__info {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__name {
|
||||||
|
margin: 0 0 5px 0;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: $text;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__meta {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 13px;
|
||||||
|
color: $text-muted;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__badges {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
justify-content: flex-end;
|
||||||
|
padding-top: 12px;
|
||||||
|
border-top: 1px solid $border;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
|
||||||
|
&--info {
|
||||||
|
background: rgba(25, 118, 210, 0.2);
|
||||||
|
color: #64b5f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--success {
|
||||||
|
background: rgba(56, 142, 60, 0.2);
|
||||||
|
color: #81c784;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
text-decoration: none;
|
||||||
|
|
||||||
|
&--primary {
|
||||||
|
background: $primary;
|
||||||
|
color: white;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: $primary-dark;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&--secondary {
|
||||||
|
background: #546e7a;
|
||||||
|
color: white;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #455a64;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&--warning {
|
||||||
|
background: $warning;
|
||||||
|
color: white;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: color.adjust($warning, $lightness: -10%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&--danger {
|
||||||
|
background: $danger;
|
||||||
|
color: white;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: color.adjust($danger, $lightness: -10%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&--sm {
|
||||||
|
padding: 6px 12px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
text-align: center;
|
||||||
|
padding: 40px 20px;
|
||||||
|
border: 1px dashed $border;
|
||||||
|
border-radius: 8px;
|
||||||
|
|
||||||
|
&__icon {
|
||||||
|
font-size: 48px;
|
||||||
|
color: #455a64;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__text {
|
||||||
|
font-size: 16px;
|
||||||
|
color: $text;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__subtext {
|
||||||
|
font-size: 13px;
|
||||||
|
color: $text-muted;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.registration-status {
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-top: 15px;
|
||||||
|
font-size: 14px;
|
||||||
|
|
||||||
|
&--success {
|
||||||
|
background: rgba(56, 142, 60, 0.15);
|
||||||
|
color: #81c784;
|
||||||
|
border: 1px solid rgba(56, 142, 60, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
&--error {
|
||||||
|
background: rgba(192, 57, 43, 0.15);
|
||||||
|
color: #e57373;
|
||||||
|
border: 1px solid rgba(192, 57, 43, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
&--loading {
|
||||||
|
background: rgba(25, 118, 210, 0.15);
|
||||||
|
color: #64b5f6;
|
||||||
|
border: 1px solid rgba(25, 118, 210, 0.3);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
/*!*
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2019 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
@use 'homepage/reset';
|
||||||
|
@use 'homepage/animations';
|
||||||
|
@use 'homepage/header';
|
||||||
|
@use 'homepage/hero';
|
||||||
|
@use 'homepage/hero-compact';
|
||||||
|
@use 'homepage/cta';
|
||||||
|
@use 'homepage/donate';
|
||||||
|
@use 'homepage/auth-bar';
|
||||||
|
@use 'homepage/auth';
|
||||||
|
@use 'homepage/content';
|
||||||
|
@use 'homepage/features';
|
||||||
|
@use 'homepage/tech';
|
||||||
|
@use 'homepage/footer';
|
||||||
|
@use 'homepage/profile';
|
||||||
|
@use 'homepage/battle-dialog';
|
||||||
|
@use 'homepage/responsive';
|
||||||
@@ -0,0 +1,403 @@
|
|||||||
|
/*!*
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2019 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
@import 'fonts-config';
|
||||||
|
@import 'fontawesome-config';
|
||||||
|
@import "style";
|
||||||
|
@import "style.homepage";
|
||||||
|
|
||||||
|
::-webkit-input-placeholder {
|
||||||
|
color: #888982;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-moz-placeholder {
|
||||||
|
color: #888982;
|
||||||
|
}
|
||||||
|
|
||||||
|
:-ms-input-placeholder {
|
||||||
|
color: #888982;
|
||||||
|
}
|
||||||
|
|
||||||
|
:-moz-placeholder {
|
||||||
|
color: #888982;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
outline: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
*,
|
||||||
|
*:after,
|
||||||
|
*::before {
|
||||||
|
-webkit-box-sizing: border-box;
|
||||||
|
-moz-box-sizing: border-box;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ac-custom {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
header section .form-check {
|
||||||
|
display: table;
|
||||||
|
position: relative;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
header section h1 {
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
header section .input-submit button,
|
||||||
|
header section .input-submit button:hover,
|
||||||
|
header section .form-input,
|
||||||
|
header section .form-input:focus,
|
||||||
|
header section .form-input:hover {
|
||||||
|
-webkit-transition: all 250ms ease-in-out;
|
||||||
|
-moz-transition: all 250ms ease-in-out;
|
||||||
|
-o-transition: all 250ms ease-in-out;
|
||||||
|
transition: all 250ms ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
header section .input-submit button {
|
||||||
|
background: #83aed9;
|
||||||
|
display: table;
|
||||||
|
font: bold 32px 'Rajdhani', sans-serif;
|
||||||
|
text-transform: uppercase;
|
||||||
|
text-decoration: none;
|
||||||
|
width: 500px;
|
||||||
|
border: 1px solid #658fb8;
|
||||||
|
color: #FFFFFF;
|
||||||
|
padding: 25px 150px;
|
||||||
|
margin-top: 20px;
|
||||||
|
|
||||||
|
-webkit-box-shadow: 0 3px 10px rgba(0, 0, 0, 0.3);
|
||||||
|
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.3);
|
||||||
|
-webkit-border-radius: 3px;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
header section .input-submit button:hover {
|
||||||
|
background: #86b5e1;
|
||||||
|
|
||||||
|
-webkit-box-shadow: 0 7px 15px rgba(0, 0, 0, 0.2);
|
||||||
|
box-shadow: 0 7px 15px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
header section .form-input {
|
||||||
|
display: block;
|
||||||
|
width: 500px;
|
||||||
|
font: bold 22px 'Rajdhani', sans-serif;
|
||||||
|
border: 1px solid #dddddd;
|
||||||
|
color: #000000;
|
||||||
|
padding: 15px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
|
||||||
|
-webkit-border-radius: 3px;
|
||||||
|
border-radius: 3px;
|
||||||
|
-webkit-box-shadow: 0 3px 5px rgba(0, 0, 0, 0.15);
|
||||||
|
box-shadow: 0 3px 5px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
header section .form-input:focus,
|
||||||
|
header section .form-input:hover {
|
||||||
|
-webkit-box-shadow: 0 7px 10px rgba(0, 0, 0, 0.1);
|
||||||
|
box-shadow: 0 7px 10px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
header section .failure {
|
||||||
|
background: #f00;
|
||||||
|
position: relative;
|
||||||
|
border: 3px solid #fff;
|
||||||
|
font: bold 16px 'Rajdhani', sans-serif;
|
||||||
|
color: #FFFFFF;
|
||||||
|
padding: 10px;
|
||||||
|
margin: 30px 0 10px 0;
|
||||||
|
|
||||||
|
-webkit-border-radius: 5px;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
header section .failure:after,
|
||||||
|
header section .failure:before {
|
||||||
|
content: " ";
|
||||||
|
position: absolute;
|
||||||
|
bottom: 100%;
|
||||||
|
left: 50px;
|
||||||
|
height: 0;
|
||||||
|
width: 0;
|
||||||
|
border: solid transparent;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
header section .failure:after {
|
||||||
|
border-color: rgba(0, 0, 0, 0);
|
||||||
|
border-bottom-color: #f00;
|
||||||
|
border-width: 20px;
|
||||||
|
margin-left: -20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
header section .failure:before {
|
||||||
|
border-color: rgba(255, 255, 255, 0);
|
||||||
|
border-bottom-color: #ffffff;
|
||||||
|
border-width: 26px;
|
||||||
|
margin-left: -26px;
|
||||||
|
}
|
||||||
|
|
||||||
|
header section .failure ul {
|
||||||
|
display: inline-block;
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0 10px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
header section h3.or {
|
||||||
|
font: bold 16px 'Rajdhani', sans-serif;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: #a1a1a1;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
header section #id_welcome {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
margin-bottom: 115px;
|
||||||
|
}
|
||||||
|
|
||||||
|
header section #id_welcome > div {
|
||||||
|
padding-right: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
header section #id_welcome img {
|
||||||
|
width: 100px;
|
||||||
|
border: 5px solid #414040;
|
||||||
|
|
||||||
|
-webkit-border-radius: 50%;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
header section .buttons,
|
||||||
|
header section form {
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
header section div.buttons > a.fb-login,
|
||||||
|
header section div.buttons > a.slack-login {
|
||||||
|
position: relative;
|
||||||
|
display: block;
|
||||||
|
width: 500px;
|
||||||
|
height: 93px;
|
||||||
|
padding: 25px 0 25px 150px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
|
||||||
|
overflow: hidden;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
-webkit-border-radius: 3px;
|
||||||
|
border-radius: 3px;
|
||||||
|
-webkit-box-shadow: 0 3px 5px rgba(0, 0, 0, 0.15);
|
||||||
|
box-shadow: 0 3px 5px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
header section div.buttons > a.fb-login:hover,
|
||||||
|
header section div.buttons > a.slack-login:hover {
|
||||||
|
text-decoration: none;
|
||||||
|
|
||||||
|
-webkit-box-shadow: 0 7px 10px rgba(0, 0, 0, 0.1);
|
||||||
|
box-shadow: 0 7px 10px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
header section div.buttons > a.fb-login i,
|
||||||
|
header section div.buttons > a.slack-login i {
|
||||||
|
position: absolute;
|
||||||
|
font-size: 130px;
|
||||||
|
top: 0;
|
||||||
|
left: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
header section div.buttons > a.fb-login {
|
||||||
|
background: #5975b1;
|
||||||
|
border: 1px solid #50649f;
|
||||||
|
}
|
||||||
|
|
||||||
|
header section div.buttons > a.fb-login:hover {
|
||||||
|
background: #42598c;
|
||||||
|
color: #FFFFFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
header section div.buttons > a.slack-login {
|
||||||
|
background: #FFFFFF;
|
||||||
|
border: 1px solid #5c3a58;
|
||||||
|
color: #5c3a58;
|
||||||
|
}
|
||||||
|
|
||||||
|
header section div.buttons > a.slack-login:hover {
|
||||||
|
background: #e6e6e6;
|
||||||
|
color: #5c3a58;
|
||||||
|
}
|
||||||
|
|
||||||
|
header section .failure-main {
|
||||||
|
background: #f00;
|
||||||
|
max-width: 500px;
|
||||||
|
border: 3px solid #fff;
|
||||||
|
font: bold 16px 'Rajdhani', sans-serif;
|
||||||
|
color: #FFFFFF;
|
||||||
|
padding: 10px;
|
||||||
|
margin: 0 0 20px 0;
|
||||||
|
|
||||||
|
-webkit-border-radius: 5px;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
main div.txt {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 1000px;
|
||||||
|
font-family: 'Rajdhani', sans-serif;
|
||||||
|
color: #414040;
|
||||||
|
margin: 50px auto 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
main div.txt h2 {
|
||||||
|
margin: 0 0 50px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
main div.txt p {
|
||||||
|
font: normal 16px 'Rajdhani', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
main div.txt li {
|
||||||
|
font: normal 16px 'Rajdhani', sans-serif;
|
||||||
|
padding-left: 10px;
|
||||||
|
margin-left: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
main .technologies {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
main .technologies img {
|
||||||
|
display: inline-block;
|
||||||
|
width: 90%;
|
||||||
|
max-width: 100px;
|
||||||
|
margin: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
main .technologies h1 {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer {
|
||||||
|
background: #414040;
|
||||||
|
width: 100%;
|
||||||
|
min-height: 50px;
|
||||||
|
margin-top: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer nav {
|
||||||
|
display: block;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer nav ul {
|
||||||
|
display: inline-block;
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer nav ul li {
|
||||||
|
display: inline-block;
|
||||||
|
font: bold 16px 'Rajdhani', sans-serif;
|
||||||
|
color: #FFFFFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer nav ul li:nth-child(even) {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer nav ul li a {
|
||||||
|
text-align: center;
|
||||||
|
line-height: 50px;
|
||||||
|
color: #FFFFFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer nav ul li a:hover {
|
||||||
|
color: #FFFFFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 1100px) {
|
||||||
|
header section #id_welcome {
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
header section .form-input,
|
||||||
|
header section .form-check {
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
header section .input-submit button {
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
header section > div {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
header section div.buttons > a.fb-login,
|
||||||
|
header section div.buttons > a.slack-login {
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
main div.txt {
|
||||||
|
padding: 0 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 550px) {
|
||||||
|
header section #id_welcome {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
header section {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
header section .form-input {
|
||||||
|
width: 100%;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
header section .form-check {
|
||||||
|
margin: 20px auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
header section .input-submit button {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
header section div.buttons > a.fb-login,
|
||||||
|
header section div.buttons > a.slack-login {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
header section div.buttons > a.fb-login span,
|
||||||
|
header section div.buttons > a.slack-login span {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer nav ul li {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
/*!*
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2019 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
@import "style";
|
||||||
|
@import 'fonts-config';
|
||||||
|
@import 'fontawesome-config';
|
||||||
|
|
||||||
|
@import 'mineseeker/base';
|
||||||
|
@import 'mineseeker/overlay';
|
||||||
|
@import 'mineseeker/users';
|
||||||
|
@import 'mineseeker/bomb';
|
||||||
|
@import 'mineseeker/mine-counter';
|
||||||
|
@import 'mineseeker/grid';
|
||||||
|
@import 'mineseeker/back-button';
|
||||||
|
@import 'mineseeker/timer';
|
||||||
|
@import 'mineseeker/bonus-box';
|
||||||
|
@import 'mineseeker/responsive';
|
||||||
|
@import 'mineseeker/waiting-dialog';
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
/*!*
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2019 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.mine-beta {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
width: 150px;
|
||||||
|
|
||||||
|
z-index: 4;
|
||||||
|
|
||||||
|
transform: rotate(90deg);
|
||||||
|
}
|
||||||
Binary file not shown.
@@ -0,0 +1,170 @@
|
|||||||
|
<!---
|
||||||
|
- This file is part of the SplendidBear Websites' projects.
|
||||||
|
-
|
||||||
|
- Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
-
|
||||||
|
- For the full copyright and license information, please view the LICENSE
|
||||||
|
- file that was distributed with this source code.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<svg viewBox="0 0 800 500" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<defs>
|
||||||
|
<style>
|
||||||
|
.bg-dark { fill: #07090d; }
|
||||||
|
.bg-grid { fill: url(#gridPattern); }
|
||||||
|
.border-accent { stroke: rgba(35, 111, 135, 0.4); stroke-width: 2; fill: none; }
|
||||||
|
.shadow { filter: drop-shadow(0 8px 20px rgba(0, 0, 0, 0.4)); }
|
||||||
|
.title-text { font: bold 28px 'Rajdhani', sans-serif; fill: #fff; letter-spacing: 0.5px; }
|
||||||
|
.header-text { font: bold 16px 'Rajdhani', sans-serif; fill: #236f87; letter-spacing: 0.5px; }
|
||||||
|
.desc-text { font: 13px 'Rajdhani', sans-serif; fill: rgba(149, 207, 245, 0.7); letter-spacing: 0.3px; }
|
||||||
|
.divider-line { stroke: rgba(35, 111, 135, 0.25); stroke-width: 1.5; }
|
||||||
|
.divider-text { font: bold 12px 'Rajdhani', sans-serif; fill: rgba(35, 111, 135, 0.5); letter-spacing: 1px; text-transform: uppercase; }
|
||||||
|
.icon-color { fill: rgba(35, 111, 135, 0.9); }
|
||||||
|
.button-gradient { fill: url(#buttonGradient); }
|
||||||
|
.button-text { font: bold 13px 'Rajdhani', sans-serif; fill: #e0f4ff; letter-spacing: 1.5px; text-transform: uppercase; }
|
||||||
|
.glow { filter: drop-shadow(0 0 12px rgba(35, 111, 135, 0.3)); }
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<!-- Grid Pattern Background -->
|
||||||
|
<pattern id="gridPattern" x="0" y="0" width="46" height="46" patternUnits="userSpaceOnUse">
|
||||||
|
<path d="M 46 0 L 0 0 0 46" fill="none" stroke="rgba(35, 111, 135, 0.08)" stroke-width="1"/>
|
||||||
|
</pattern>
|
||||||
|
|
||||||
|
<!-- Button Gradient -->
|
||||||
|
<linearGradient id="buttonGradient" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||||
|
<stop offset="0%" style="stop-color:#236f87;stop-opacity:1" />
|
||||||
|
<stop offset="100%" style="stop-color:#1a5068;stop-opacity:1" />
|
||||||
|
</linearGradient>
|
||||||
|
|
||||||
|
<!-- Icon Definitions -->
|
||||||
|
<g id="icon-link">
|
||||||
|
<path d="M 12 8 L 20 8 Q 22 8 22 10 L 22 16 Q 22 18 20 18 L 16 18 M 28 12 L 32 12 Q 34 12 34 14 L 34 20 Q 34 22 32 22 L 28 22 M 22 12 L 28 12"
|
||||||
|
stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<g id="icon-users">
|
||||||
|
<circle cx="16" cy="10" r="3.5" fill="currentColor"/>
|
||||||
|
<path d="M 12 15 Q 12 13 16 13 Q 20 13 20 15 L 20 18 L 12 18 Z" fill="currentColor"/>
|
||||||
|
<circle cx="30" cy="10" r="3.5" fill="currentColor"/>
|
||||||
|
<path d="M 26 15 Q 26 13 30 13 Q 34 13 34 15 L 34 18 L 26 18 Z" fill="currentColor"/>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<g id="icon-search">
|
||||||
|
<circle cx="18" cy="18" r="8" stroke="currentColor" stroke-width="2" fill="none"/>
|
||||||
|
<line x1="26" y1="26" x2="32" y2="32" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<g id="icon-clipboard">
|
||||||
|
<rect x="12" y="8" width="16" height="24" rx="2" stroke="currentColor" stroke-width="2" fill="none"/>
|
||||||
|
<rect x="16" y="6" width="8" height="3" fill="currentColor"/>
|
||||||
|
<line x1="16" y1="14" x2="28" y2="14" stroke="currentColor" stroke-width="1.5"/>
|
||||||
|
<line x1="16" y1="18" x2="28" y2="18" stroke="currentColor" stroke-width="1.5"/>
|
||||||
|
<line x1="16" y1="22" x2="24" y2="22" stroke="currentColor" stroke-width="1.5"/>
|
||||||
|
</g>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<!-- Main Background -->
|
||||||
|
<rect class="bg-dark" width="800" height="500"/>
|
||||||
|
<rect class="bg-grid" width="800" height="500"/>
|
||||||
|
|
||||||
|
<!-- Dialog Container -->
|
||||||
|
<g class="shadow">
|
||||||
|
<rect class="bg-dark" x="60" y="40" width="680" height="420" rx="12" class="shadow"/>
|
||||||
|
<rect class="border-accent" x="60" y="40" width="680" height="420" rx="12"/>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- Dialog Header -->
|
||||||
|
<g>
|
||||||
|
<text x="100" y="85" class="title-text">We are waiting...</text>
|
||||||
|
<g transform="translate(720, 60)">
|
||||||
|
<!-- Close Button -->
|
||||||
|
<circle cx="0" cy="0" r="16" stroke="rgba(35, 111, 135, 0.3)" stroke-width="1" fill="none"/>
|
||||||
|
<line x1="-6" y1="-6" x2="6" y2="6" stroke="rgba(149, 207, 245, 0.55)" stroke-width="2" stroke-linecap="round"/>
|
||||||
|
<line x1="6" y1="-6" x2="-6" y2="6" stroke="rgba(149, 207, 245, 0.55)" stroke-width="2" stroke-linecap="round"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- Option 1: Invite a Friend -->
|
||||||
|
<g>
|
||||||
|
<!-- Option Container -->
|
||||||
|
<rect x="80" y="120" width="280" height="280" rx="8" fill="rgba(35, 111, 135, 0.05)" stroke="rgba(35, 111, 135, 0.15)" stroke-width="1"/>
|
||||||
|
|
||||||
|
<!-- Icon -->
|
||||||
|
<g transform="translate(110, 145)">
|
||||||
|
<circle cx="0" cy="0" r="22" fill="rgba(35, 111, 135, 0.15)"/>
|
||||||
|
<g use="#icon-link" class="icon-color" transform="scale(1.8)"/>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- Title -->
|
||||||
|
<text x="160" y="165" class="header-text">Invite a Friend</text>
|
||||||
|
|
||||||
|
<!-- Description -->
|
||||||
|
<text x="100" y="195" class="desc-text">Share this link with your opponent</text>
|
||||||
|
|
||||||
|
<!-- URL Box -->
|
||||||
|
<rect x="100" y="210" width="240" height="40" rx="6" fill="#d0e8f5" stroke="#7ab8d8" stroke-width="1"/>
|
||||||
|
<text x="118" y="237" style="font: 12px 'Courier New', monospace; fill: #1a4a6a; letter-spacing: 0.3px;">play.mineseeker.com/game-id</text>
|
||||||
|
|
||||||
|
<!-- Copy Button -->
|
||||||
|
<g class="glow">
|
||||||
|
<rect x="100" y="265" width="240" height="40" rx="5" class="button-gradient" stroke="#2e7a9a" stroke-width="1"/>
|
||||||
|
<g transform="translate(120, 285)">
|
||||||
|
<rect x="0" y="0" width="6" height="6" fill="#e0f4ff"/>
|
||||||
|
<rect x="2" y="2" width="4" height="4" fill="none" stroke="#e0f4ff" stroke-width="0.5"/>
|
||||||
|
</g>
|
||||||
|
<text x="140" y="287" class="button-text">Copy Link</text>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- Divider OR -->
|
||||||
|
<g>
|
||||||
|
<line x1="400" y1="200" x2="400" y2="320" class="divider-line"/>
|
||||||
|
<circle cx="400" cy="260" r="18" fill="#07090d" stroke="rgba(35, 111, 135, 0.25)" stroke-width="1"/>
|
||||||
|
<text x="400" y="267" class="divider-text" text-anchor="middle">OR</text>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- Option 2: Challenge a Player -->
|
||||||
|
<g>
|
||||||
|
<!-- Option Container -->
|
||||||
|
<rect x="440" y="120" width="280" height="280" rx="8" fill="rgba(35, 111, 135, 0.05)" stroke="rgba(35, 111, 135, 0.15)" stroke-width="1"/>
|
||||||
|
|
||||||
|
<!-- Icon -->
|
||||||
|
<g transform="translate(470, 145)">
|
||||||
|
<circle cx="0" cy="0" r="22" fill="rgba(35, 111, 135, 0.15)"/>
|
||||||
|
<g use="#icon-users" class="icon-color" transform="scale(1.5)"/>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- Title -->
|
||||||
|
<text x="510" y="165" class="header-text">Challenge a Player</text>
|
||||||
|
|
||||||
|
<!-- Description -->
|
||||||
|
<text x="460" y="195" class="desc-text">Browse online players and challenge</text>
|
||||||
|
|
||||||
|
<!-- Browse Button -->
|
||||||
|
<g class="glow">
|
||||||
|
<rect x="460" y="265" width="240" height="40" rx="5" class="button-gradient" stroke="#2e7a9a" stroke-width="1"/>
|
||||||
|
<g transform="translate(480, 285)">
|
||||||
|
<!-- Search icon simplified -->
|
||||||
|
<circle cx="4" cy="4" r="3" fill="none" stroke="#e0f4ff" stroke-width="1"/>
|
||||||
|
<line x1="7" y1="7" x2="9" y2="9" stroke="#e0f4ff" stroke-width="1" stroke-linecap="round"/>
|
||||||
|
</g>
|
||||||
|
<text x="510" y="287" class="button-text">Browse Players</text>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- Highlight: Players Online -->
|
||||||
|
<g>
|
||||||
|
<text x="460" y="230" class="desc-text" style="font-weight: bold;">5 players waiting</text>
|
||||||
|
<g transform="translate(680, 220)">
|
||||||
|
<circle r="6" fill="#236f87"/>
|
||||||
|
<circle cx="-8" cy="3" r="4" fill="rgba(35, 111, 135, 0.6)"/>
|
||||||
|
<circle cx="8" cy="3" r="4" fill="rgba(35, 111, 135, 0.6)"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- Bottom Info -->
|
||||||
|
<g>
|
||||||
|
<text x="100" y="435" style="font: 11px 'Rajdhani', sans-serif; fill: rgba(149, 207, 245, 0.35); letter-spacing: 0.5px;">Choose either option to start playing or find an opponent</text>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
After Width: | Height: | Size: 7.5 KiB |
@@ -0,0 +1,22 @@
|
|||||||
|
/**
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { createRoot } from 'react-dom/client';
|
||||||
|
import MineSeeker from './mine-seeker/MineSeeker';
|
||||||
|
|
||||||
|
const wrapper = document.getElementById('mine-wrapper');
|
||||||
|
|
||||||
|
createRoot(wrapper).render(
|
||||||
|
<MineSeeker
|
||||||
|
env={wrapper.dataset.env}
|
||||||
|
gameId={wrapper.dataset.gameId}
|
||||||
|
opponentName={wrapper.dataset.opponentName || ''}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
/**
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import 'cap-widget';
|
||||||
|
|
||||||
|
const FORMS_SELECTOR = '.auth-form';
|
||||||
|
const SUBMIT_SELECTOR = 'button[type="submit"], input[type="submit"]';
|
||||||
|
|
||||||
|
const createInvisibleCap = apiEndpoint => {
|
||||||
|
const host = document.createElement('cap-widget');
|
||||||
|
host.style.display = 'none';
|
||||||
|
|
||||||
|
const cap = new window.Cap({ apiEndpoint }, host);
|
||||||
|
|
||||||
|
return { cap, host };
|
||||||
|
};
|
||||||
|
|
||||||
|
const lockSubmit = (form, locked) => {
|
||||||
|
const submit = form.querySelector(SUBMIT_SELECTOR);
|
||||||
|
if (!submit) return;
|
||||||
|
submit.disabled = locked;
|
||||||
|
};
|
||||||
|
|
||||||
|
const bindInvisibleCap = (form, apiEndpoint) => {
|
||||||
|
const { cap, host } = createInvisibleCap(apiEndpoint);
|
||||||
|
form.appendChild(host);
|
||||||
|
|
||||||
|
let solving = null;
|
||||||
|
|
||||||
|
const solveToken = async () => {
|
||||||
|
if (!solving) {
|
||||||
|
solving = cap.solve()
|
||||||
|
.finally(() => {
|
||||||
|
solving = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await solving;
|
||||||
|
return result?.token ?? cap.token ?? '';
|
||||||
|
};
|
||||||
|
|
||||||
|
lockSubmit(form, true);
|
||||||
|
|
||||||
|
solveToken()
|
||||||
|
.catch(() => '')
|
||||||
|
.finally(() => lockSubmit(form, false));
|
||||||
|
|
||||||
|
form.addEventListener('submit', async e => {
|
||||||
|
if (cap.token) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
e.preventDefault();
|
||||||
|
lockSubmit(form, true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const token = await solveToken();
|
||||||
|
if (!token) {
|
||||||
|
lockSubmit(form, false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
form.requestSubmit();
|
||||||
|
} catch (_) {
|
||||||
|
lockSubmit(form, false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const initInvisibleCap = () => {
|
||||||
|
const endpointSource = document.querySelector('[data-cap-api-endpoint]')
|
||||||
|
|| document.getElementById('mine-wrapper');
|
||||||
|
const apiEndpoint = endpointSource?.dataset.capApiEndpoint;
|
||||||
|
|
||||||
|
if ('function' !== typeof window.Cap || !apiEndpoint) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.querySelectorAll(FORMS_SELECTOR).forEach(form => bindInvisibleCap(form, apiEndpoint));
|
||||||
|
};
|
||||||
|
|
||||||
|
initInvisibleCap();
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
/**
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { Fragment, useMemo, useRef } from 'react';
|
||||||
|
import { string } from 'prop-types';
|
||||||
|
import { useProfileDataProvider } from '@mine-hooks/useGameDataProvider';
|
||||||
|
|
||||||
|
export const AvatarUpload = ({ uploadUrl, initialThumbUrl, initials }) => {
|
||||||
|
const inputRef = useRef(null);
|
||||||
|
const [thumbUrl, setThumbUrl] = React.useState(initialThumbUrl || null);
|
||||||
|
const { uploadAvatarMutation: { isPending, error, mutate } } = useProfileDataProvider();
|
||||||
|
|
||||||
|
const errorMessage = useMemo(() => error?.message ?? null, [error]);
|
||||||
|
|
||||||
|
const handleChange = e => {
|
||||||
|
const file = e.target.files?.[0];
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
mutate({ uploadUrl, file }, {
|
||||||
|
onSuccess: data => {
|
||||||
|
setThumbUrl(data.thumbUrl);
|
||||||
|
|
||||||
|
const navImg = document.querySelector('.hero-auth-avatar:not(.hero-auth-avatar--initials)');
|
||||||
|
const navInitials = document.querySelector('.hero-auth-avatar.hero-auth-avatar--initials');
|
||||||
|
|
||||||
|
if (navImg) {
|
||||||
|
navImg.src = data.thumbUrl;
|
||||||
|
} else if (navInitials) {
|
||||||
|
const img = document.createElement('img');
|
||||||
|
img.src = data.thumbUrl;
|
||||||
|
img.alt = navInitials.textContent.trim();
|
||||||
|
img.className = 'hero-auth-avatar';
|
||||||
|
navInitials.replaceWith(img);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<div
|
||||||
|
className={`profile-avatar${isPending ? ' profile-avatar--loading' : ''}`}
|
||||||
|
title="Click to change profile picture"
|
||||||
|
onClick={() => inputRef.current?.click()}
|
||||||
|
>
|
||||||
|
{thumbUrl
|
||||||
|
? <img src={thumbUrl} alt={initials} className="profile-avatar__img" />
|
||||||
|
: <span className="profile-avatar__initials">{initials}</span>
|
||||||
|
}
|
||||||
|
<div className="profile-avatar__overlay">
|
||||||
|
<i className="fa fa-camera" />
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
ref={inputRef}
|
||||||
|
type="file"
|
||||||
|
accept="image/jpeg,image/png,image/gif,image/webp"
|
||||||
|
style={{ display: 'none' }}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{errorMessage && <div className="profile-avatar__error">{errorMessage}</div>}
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
AvatarUpload.propTypes = {
|
||||||
|
uploadUrl: string.isRequired,
|
||||||
|
initialThumbUrl: string,
|
||||||
|
initials: string.isRequired,
|
||||||
|
};
|
||||||
@@ -0,0 +1,223 @@
|
|||||||
|
/**
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { array } from 'prop-types';
|
||||||
|
import { formatDuration } from '@global-utils/format';
|
||||||
|
import Dialog from '@mui/material/Dialog';
|
||||||
|
import { createTheme, styled, ThemeProvider } from '@mui/material/styles';
|
||||||
|
import { Avatar } from './battle-dialog/Avatar';
|
||||||
|
import { BonusPoints } from './battle-dialog/BonusPoints';
|
||||||
|
import { StatRow } from './battle-dialog/StatRow';
|
||||||
|
|
||||||
|
const darkTheme = createTheme({ palette: { mode: 'dark' } });
|
||||||
|
|
||||||
|
const RESULT_META = {
|
||||||
|
win: {
|
||||||
|
label: 'Victory',
|
||||||
|
color: '#5ee89a',
|
||||||
|
bg: 'rgba(42,158,96,0.15)',
|
||||||
|
border: 'rgba(42,158,96,0.4)',
|
||||||
|
icon: 'fa-trophy',
|
||||||
|
},
|
||||||
|
loss: {
|
||||||
|
label: 'Defeated',
|
||||||
|
color: '#f67d52',
|
||||||
|
bg: 'rgba(173,10,5,0.15)',
|
||||||
|
border: 'rgba(173,10,5,0.4)',
|
||||||
|
icon: 'fa-flag',
|
||||||
|
},
|
||||||
|
draw: {
|
||||||
|
label: 'Draw',
|
||||||
|
color: '#95cff5',
|
||||||
|
bg: 'rgba(149,207,245,0.1)',
|
||||||
|
border: 'rgba(149,207,245,0.3)',
|
||||||
|
icon: 'fa-minus',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const BattleDialog = ({ games }) => {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [game, setGame] = useState(null);
|
||||||
|
const [copied, setCopied] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handler = e => {
|
||||||
|
const row = e.target.closest('[data-game-index]');
|
||||||
|
if (!row) return;
|
||||||
|
const idx = parseInt(row.dataset.gameIndex, 10);
|
||||||
|
if (!isNaN(idx) && games[idx]) {
|
||||||
|
setGame(games[idx]);
|
||||||
|
setOpen(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
document.addEventListener('click', handler);
|
||||||
|
return () => document.removeEventListener('click', handler);
|
||||||
|
}, [games]);
|
||||||
|
|
||||||
|
if (!game) {
|
||||||
|
return <ThemeProvider theme={darkTheme}><StyledDialog open={false} /></ThemeProvider>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const meta = RESULT_META[game.result] ?? RESULT_META.draw;
|
||||||
|
const resign = game.resign;
|
||||||
|
const maxPoints = Math.max(game.redPoints ?? 0, game.bluePoints ?? 0);
|
||||||
|
const endReason = resign
|
||||||
|
? `${resign.charAt(0).toUpperCase() + resign.slice(1)} resigned`
|
||||||
|
: 26 <= maxPoints ? 'Points' : 'Abandoned';
|
||||||
|
const bothRegistered = game.bothRegistered;
|
||||||
|
const shareUrl = `${window.location.origin}/battle/${game.uuid}`;
|
||||||
|
const canContinue = bothRegistered && !resign && 26 > maxPoints;
|
||||||
|
const canShare = !canContinue;
|
||||||
|
const playUrl = `${window.location.origin}/play/${game.uuid}`;
|
||||||
|
|
||||||
|
const duration = formatDuration(game.created, game.date);
|
||||||
|
const pointDiff = Math.abs((game.redPoints ?? 0) - (game.bluePoints ?? 0));
|
||||||
|
const winnerColor = (game.redPoints ?? 0) > (game.bluePoints ?? 0) ? '#f67d52'
|
||||||
|
: (game.bluePoints ?? 0) > (game.redPoints ?? 0) ? '#95cff5'
|
||||||
|
: 'rgba(255,255,255,0.45)';
|
||||||
|
|
||||||
|
const handleShare = () => {
|
||||||
|
navigator.clipboard.writeText(shareUrl).then(() => {
|
||||||
|
setCopied(true);
|
||||||
|
setTimeout(() => setCopied(false), 2200);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ThemeProvider theme={darkTheme}>
|
||||||
|
<StyledDialog open={open} onClose={() => setOpen(false)}>
|
||||||
|
<div className="bd">
|
||||||
|
<div className="bd-header">
|
||||||
|
<div className="bd-header-left">
|
||||||
|
<span className="bd-label">Battle Report</span>
|
||||||
|
<h2 className="bd-title">
|
||||||
|
<i className="fa fa-crosshairs" /> Match Details
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div className="bd-header-actions">
|
||||||
|
{canContinue ? (
|
||||||
|
<a
|
||||||
|
className="bd-continue"
|
||||||
|
href={playUrl}
|
||||||
|
aria-label="Continue the game"
|
||||||
|
title="Continue the game"
|
||||||
|
>
|
||||||
|
<i className="fa fa-play" />
|
||||||
|
Continue
|
||||||
|
</a>
|
||||||
|
) : canShare ? (
|
||||||
|
<button
|
||||||
|
className={`bd-share${copied ? ' bd-share--copied' : ''}`}
|
||||||
|
onClick={handleShare}
|
||||||
|
aria-label="Copy share link"
|
||||||
|
title="Copy share link"
|
||||||
|
>
|
||||||
|
<i className={`fa ${copied ? 'fa-check' : 'fa-share-alt'}`} />
|
||||||
|
{copied ? 'Copied!' : 'Share'}
|
||||||
|
</button>
|
||||||
|
) : null}
|
||||||
|
<button className="bd-close" onClick={() => setOpen(false)} aria-label="Close">
|
||||||
|
<i className="fa fa-times" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="bd-vs-panel">
|
||||||
|
<Avatar
|
||||||
|
name={game.redName} color="red" avatarUrl={game.redAvatar}
|
||||||
|
bonusPoints={game.redBonusPoints > game.blueBonusPoints ? game.redBonusPoints : 0}
|
||||||
|
/>
|
||||||
|
<div className="bd-vs-center">
|
||||||
|
<div className="bd-vs-score">
|
||||||
|
<span className="bd-vs-score__red">{game.redPoints ?? '—'}</span>
|
||||||
|
<span className="bd-vs-score__sep">:</span>
|
||||||
|
<span className="bd-vs-score__blue">{game.bluePoints ?? '—'}</span>
|
||||||
|
</div>
|
||||||
|
<div className="bd-vs-score bd-bonus-score">
|
||||||
|
<span className="bd-bonus-score__red">
|
||||||
|
<i className="fa fa-star" /> {(game.redBonusPoints ?? 0).toFixed(1)}
|
||||||
|
</span>
|
||||||
|
<span className="bd-vs-score__sep">:</span>
|
||||||
|
<span className="bd-bonus-score__blue">
|
||||||
|
{(game.blueBonusPoints ?? 0).toFixed(1)} <i className="fa fa-star" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="bd-vs-label">VS</div>
|
||||||
|
<div
|
||||||
|
className="bd-result-badge"
|
||||||
|
style={{ '--bd-result-bg': meta.bg, '--bd-result-border': meta.border, '--bd-result-color': meta.color }}
|
||||||
|
>
|
||||||
|
<i className={`fa ${meta.icon}`} /> {meta.label}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Avatar
|
||||||
|
name={game.blueName} color="blue" avatarUrl={game.blueAvatar}
|
||||||
|
bonusPoints={game.blueBonusPoints > game.redBonusPoints ? game.blueBonusPoints : 0}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="bd-stats">
|
||||||
|
<StatRow icon="fa-calendar" label="Date" value={game.date ?? '—'} />
|
||||||
|
{game.created && game.date && game.created !== game.date && (
|
||||||
|
<StatRow icon="fa-clock" label="Started" value={game.created} />
|
||||||
|
)}
|
||||||
|
{duration && (
|
||||||
|
<StatRow icon="fa-hourglass-half" label="Match duration" value={duration} />
|
||||||
|
)}
|
||||||
|
<StatRow icon="fa-flag-checkered" label="End reason" value={endReason} />
|
||||||
|
{0 < pointDiff && (
|
||||||
|
<StatRow
|
||||||
|
icon="fa-balance-scale" label="Winning margin"
|
||||||
|
value={`${pointDiff} mine${1 === pointDiff ? '' : 's'}`} valueColor={winnerColor}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<StatRow
|
||||||
|
icon="fa-bomb" label="Red used bomb"
|
||||||
|
value={game.redExplodedBomb ? 'Yes' : 'No'}
|
||||||
|
valueColor={game.redExplodedBomb ? '#f67d52' : 'rgba(255,255,255,0.45)'}
|
||||||
|
/>
|
||||||
|
<StatRow
|
||||||
|
icon="fa-bomb" label="Blue used bomb"
|
||||||
|
value={game.blueExplodedBomb ? 'Yes' : 'No'}
|
||||||
|
valueColor={game.blueExplodedBomb ? '#95cff5' : 'rgba(255,255,255,0.45)'}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<BonusPoints
|
||||||
|
game={game}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</StyledDialog>
|
||||||
|
</ThemeProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
BattleDialog.propTypes = {
|
||||||
|
games: array.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
const StyledDialog = styled(Dialog)({
|
||||||
|
'& .MuiDialog-paper': {
|
||||||
|
background: '#07090d',
|
||||||
|
backgroundImage: `
|
||||||
|
linear-gradient(rgba(35, 111, 135, 0.08) 1px, transparent 1px),
|
||||||
|
linear-gradient(90deg, rgba(35, 111, 135, 0.08) 1px, transparent 1px)
|
||||||
|
`,
|
||||||
|
backgroundSize: '46px 46px',
|
||||||
|
border: '1px solid rgba(35, 111, 135, 0.4)',
|
||||||
|
borderRadius: '12px',
|
||||||
|
boxShadow: '0 0 80px rgba(35, 111, 135, 0.15), 0 32px 80px rgba(0,0,0,0.9)',
|
||||||
|
width: '580px',
|
||||||
|
maxWidth: '94vw',
|
||||||
|
overflow: 'hidden',
|
||||||
|
color: '#fff',
|
||||||
|
},
|
||||||
|
'& .MuiBackdrop-root': {
|
||||||
|
background: 'rgba(2, 4, 8, 0.88)',
|
||||||
|
backdropFilter: 'blur(4px)',
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
/**
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { useState, useCallback } from 'react';
|
||||||
|
import { shape, string } from 'prop-types';
|
||||||
|
|
||||||
|
const base64ToArrayBuffer = base64 => {
|
||||||
|
const binary = atob(base64.replace(/([-_])/g, m => ('-' === m ? '+' : '/')));
|
||||||
|
const bytes = new Uint8Array(binary.length);
|
||||||
|
for (let i = 0; i < binary.length; i++) {
|
||||||
|
bytes[i] = binary.charCodeAt(i);
|
||||||
|
}
|
||||||
|
return bytes.buffer;
|
||||||
|
};
|
||||||
|
|
||||||
|
const arrayBufferToBase64url = buffer =>
|
||||||
|
btoa(String.fromCharCode(...new Uint8Array(buffer)))
|
||||||
|
.replace(/([+\/=])/g, m => ('+' === m ? '-' : '/' === m ? '_' : ''));
|
||||||
|
|
||||||
|
const credentialToJSON = credential => ({
|
||||||
|
id: credential.id,
|
||||||
|
rawId: arrayBufferToBase64url(credential.rawId),
|
||||||
|
type: credential.type,
|
||||||
|
response: {
|
||||||
|
authenticatorData: arrayBufferToBase64url(credential.response.authenticatorData),
|
||||||
|
clientDataJSON: arrayBufferToBase64url(credential.response.clientDataJSON),
|
||||||
|
signature: arrayBufferToBase64url(credential.response.signature),
|
||||||
|
userHandle: credential.response.userHandle
|
||||||
|
? arrayBufferToBase64url(credential.response.userHandle)
|
||||||
|
: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const PasskeyLogin = ({ apiRoutes }) => {
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState('');
|
||||||
|
|
||||||
|
const handleLogin = useCallback(async () => {
|
||||||
|
setLoading(true);
|
||||||
|
setError('');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const beginResponse = await fetch(apiRoutes.authenticationBegin, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!beginResponse.ok) throw new Error('Failed to get authentication options');
|
||||||
|
const options = await beginResponse.json();
|
||||||
|
|
||||||
|
const publicKey = {
|
||||||
|
...options,
|
||||||
|
challenge: base64ToArrayBuffer(options.challenge),
|
||||||
|
allowCredentials: (options.allowCredentials ?? []).map(cred => ({
|
||||||
|
...cred,
|
||||||
|
id: base64ToArrayBuffer(cred.id),
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
|
||||||
|
const credential = await navigator.credentials.get({ publicKey });
|
||||||
|
if (!credential) throw new Error('Authentication was cancelled');
|
||||||
|
|
||||||
|
const completeResponse = await fetch(apiRoutes.authenticationComplete, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ credential: credentialToJSON(credential) }),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!completeResponse.ok) {
|
||||||
|
const body = await completeResponse.json();
|
||||||
|
throw new Error(body.error || 'Authentication failed');
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await completeResponse.json();
|
||||||
|
if (result.success) {
|
||||||
|
window.location.href = result.redirect || '/';
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setError(err.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(false);
|
||||||
|
}, [apiRoutes]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="auth-passkey-btn"
|
||||||
|
onClick={handleLogin}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
<i className={loading ? 'fa fa-spinner fa-spin' : 'fa fa-key'} />
|
||||||
|
{loading ? 'Waiting for passkey…' : 'Sign In with Passkey'}
|
||||||
|
</button>
|
||||||
|
{error && (
|
||||||
|
<p className="auth-error" style={{ marginTop: '10px' }}>
|
||||||
|
<i className="fa fa-exclamation-triangle" /> {error}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PasskeyLogin;
|
||||||
|
|
||||||
|
PasskeyLogin.propTypes = {
|
||||||
|
apiRoutes: shape({
|
||||||
|
authenticationBegin: string.isRequired,
|
||||||
|
authenticationComplete: string.isRequired,
|
||||||
|
}).isRequired,
|
||||||
|
};
|
||||||
@@ -0,0 +1,428 @@
|
|||||||
|
/**
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { Fragment, useCallback, useEffect, useState } from 'react';
|
||||||
|
import Dialog from '@mui/material/Dialog';
|
||||||
|
import { styled } from '@mui/material/styles';
|
||||||
|
import DialogTitle from '@mui/material/DialogTitle';
|
||||||
|
import DialogContent from '@mui/material/DialogContent';
|
||||||
|
import DialogActions from '@mui/material/DialogActions';
|
||||||
|
import Button from '@mui/material/Button';
|
||||||
|
import TextField from '@mui/material/TextField';
|
||||||
|
import { arrayOf, shape, string, bool } from 'prop-types';
|
||||||
|
|
||||||
|
const StyledDialog = styled(Dialog)({
|
||||||
|
'& .MuiDialog-paper': {
|
||||||
|
background: '#0a0e14',
|
||||||
|
color: '#e0e0e0',
|
||||||
|
},
|
||||||
|
'& .MuiTextField-root .MuiInputLabel-root': {
|
||||||
|
color: '#9e9e9e',
|
||||||
|
},
|
||||||
|
'& .MuiTextField-root .MuiOutlinedInput-root': {
|
||||||
|
'& fieldset': {
|
||||||
|
borderColor: 'rgba(35, 111, 135, 0.5)',
|
||||||
|
},
|
||||||
|
'&:hover fieldset': {
|
||||||
|
borderColor: 'rgba(35, 111, 135, 0.8)',
|
||||||
|
},
|
||||||
|
'&.Mui-focused fieldset': {
|
||||||
|
borderColor: '#236f87',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'& .MuiTextField-root .MuiOutlinedInput-input': {
|
||||||
|
color: '#e0e0e0',
|
||||||
|
},
|
||||||
|
'& .MuiFormHelperText-root': {
|
||||||
|
width: '500px',
|
||||||
|
maxWidth: '94vw',
|
||||||
|
overflow: 'hidden',
|
||||||
|
color: '#e0e0e0',
|
||||||
|
},
|
||||||
|
'& .MuiBackdrop-root': {
|
||||||
|
background: 'rgba(2, 4, 8, 0.88)',
|
||||||
|
backdropFilter: 'blur(4px)',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const base64ToArrayBuffer = base64 => {
|
||||||
|
const binary = atob(base64.replace(/([-_])/g, m => ('-' === m ? '+' : '/')));
|
||||||
|
const bytes = new Uint8Array(binary.length);
|
||||||
|
for (let i = 0; i < binary.length; i++) {
|
||||||
|
bytes[i] = binary.charCodeAt(i);
|
||||||
|
}
|
||||||
|
return bytes.buffer;
|
||||||
|
};
|
||||||
|
|
||||||
|
const credentialToJSON = credential => {
|
||||||
|
const attestationObject = credential.response.attestationObject;
|
||||||
|
const clientDataJSON = credential.response.clientDataJSON;
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: credential.id,
|
||||||
|
rawId: btoa(String.fromCharCode(...new Uint8Array(credential.rawId))).replace(/([+\/=])/g, m => ('+' === m ? '-' : '/' === m ? '_' : '')),
|
||||||
|
type: credential.type,
|
||||||
|
response: {
|
||||||
|
attestationObject: btoa(String.fromCharCode(...new Uint8Array(attestationObject))).replace(/([+\/=])/g, m => ('+' === m ? '-' : '/' === m ? '_' : '')),
|
||||||
|
clientDataJSON: btoa(String.fromCharCode(...new Uint8Array(clientDataJSON))).replace(/([+\/=])/g, m => ('+' === m ? '-' : '/' === m ? '_' : '')),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const PasskeyManager = ({ credentials, apiRoutes }) => {
|
||||||
|
const [addModalOpen, setAddModalOpen] = useState(false);
|
||||||
|
const [renameModalOpen, setRenameModalOpen] = useState(false);
|
||||||
|
const [deleteModalOpen, setDeleteModalOpen] = useState(false);
|
||||||
|
const [selectedCredential, setSelectedCredential] = useState(null);
|
||||||
|
const [passkeyName, setPasskeyName] = useState('');
|
||||||
|
const [renameName, setRenameName] = useState('');
|
||||||
|
const [status, setStatus] = useState({ type: '', message: '' });
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [credentialsList, setCredentialsList] = useState(credentials);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setCredentialsList(credentials);
|
||||||
|
}, [credentials]);
|
||||||
|
|
||||||
|
const showStatus = useCallback((message, type) => {
|
||||||
|
setStatus({ message, type });
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const closeAddModal = useCallback(() => {
|
||||||
|
setAddModalOpen(false);
|
||||||
|
setPasskeyName('');
|
||||||
|
setStatus({ type: '', message: '' });
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const openAddModal = useCallback(() => {
|
||||||
|
setPasskeyName('');
|
||||||
|
setStatus({ type: '', message: '' });
|
||||||
|
setAddModalOpen(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleAddPasskey = useCallback(async () => {
|
||||||
|
if (!passkeyName.trim()) {
|
||||||
|
showStatus('Please enter a passkey name', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
showStatus('Starting registration...', 'loading');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const optionsResponse = await fetch(apiRoutes.registrationBegin, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ credentialName: passkeyName.trim() }),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!optionsResponse.ok) throw new Error('Failed to get registration options');
|
||||||
|
const options = await optionsResponse.json();
|
||||||
|
|
||||||
|
showStatus('Please touch your security key or use biometric authentication...', 'loading');
|
||||||
|
|
||||||
|
const publicKey = {
|
||||||
|
...options,
|
||||||
|
challenge: base64ToArrayBuffer(options.challenge),
|
||||||
|
user: {
|
||||||
|
...options.user,
|
||||||
|
id: base64ToArrayBuffer(options.user.id),
|
||||||
|
},
|
||||||
|
attestation: 'direct',
|
||||||
|
};
|
||||||
|
|
||||||
|
const credential = await navigator.credentials.create({ publicKey });
|
||||||
|
|
||||||
|
if (!credential) {
|
||||||
|
showStatus('Registration was cancelled', 'error');
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
showStatus('Verifying credential...', 'loading');
|
||||||
|
|
||||||
|
const completeResponse = await fetch(apiRoutes.registrationComplete, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
credential: credentialToJSON(credential),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!completeResponse.ok) throw new Error('Failed to complete registration');
|
||||||
|
|
||||||
|
showStatus('Passkey registered successfully!', 'success');
|
||||||
|
setTimeout(() => window.location.reload(), 1500);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Registration error:', error);
|
||||||
|
showStatus('Error: ' + error.message, 'error');
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(false);
|
||||||
|
}, [passkeyName, apiRoutes, showStatus]);
|
||||||
|
|
||||||
|
const openRenameModal = useCallback(credential => {
|
||||||
|
setSelectedCredential(credential);
|
||||||
|
setRenameName(credential.credentialName);
|
||||||
|
setStatus({ type: '', message: '' });
|
||||||
|
setRenameModalOpen(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const closeRenameModal = useCallback(() => {
|
||||||
|
setRenameModalOpen(false);
|
||||||
|
setSelectedCredential(null);
|
||||||
|
setRenameName('');
|
||||||
|
setStatus({ type: '', message: '' });
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleRename = useCallback(async () => {
|
||||||
|
if (!renameName.trim() || !selectedCredential) {
|
||||||
|
showStatus('Please enter a new name', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${apiRoutes.credentials}/${selectedCredential.id}/rename`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ name: renameName.trim() }),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) throw new Error('Failed to rename passkey');
|
||||||
|
|
||||||
|
window.location.reload();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Rename error:', error);
|
||||||
|
showStatus('Error: ' + error.message, 'error');
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(false);
|
||||||
|
}, [renameName, selectedCredential, apiRoutes, showStatus]);
|
||||||
|
|
||||||
|
const openDeleteModal = useCallback(credential => {
|
||||||
|
setSelectedCredential(credential);
|
||||||
|
setDeleteModalOpen(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const closeDeleteModal = useCallback(() => {
|
||||||
|
setDeleteModalOpen(false);
|
||||||
|
setSelectedCredential(null);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleDelete = useCallback(async () => {
|
||||||
|
if (!selectedCredential) return;
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${apiRoutes.credentials}/${selectedCredential.id}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
window.location.reload();
|
||||||
|
} else {
|
||||||
|
showStatus('Failed to delete passkey', 'error');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Delete error:', error);
|
||||||
|
showStatus('Error: ' + error.message, 'error');
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(false);
|
||||||
|
setDeleteModalOpen(false);
|
||||||
|
}, [selectedCredential, apiRoutes, showStatus]);
|
||||||
|
|
||||||
|
const statusClass = 'error' === status.type
|
||||||
|
? 'registration-status--error'
|
||||||
|
: 'success' === status.type
|
||||||
|
? 'registration-status--success'
|
||||||
|
: 'loading' === status.type
|
||||||
|
? 'registration-status--loading'
|
||||||
|
: '';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="passkey-manager">
|
||||||
|
<div className="passkey-manager__actions">
|
||||||
|
<button className="btn btn--primary" onClick={openAddModal} type="button">
|
||||||
|
<i className="fa fa-plus" /> Add Passkey
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="passkey-manager__list">
|
||||||
|
{0 < credentialsList.length ? (
|
||||||
|
credentialsList.map(credential => (
|
||||||
|
<div key={credential.id} className="passkey-item" data-credential-id={credential.id}>
|
||||||
|
<div className="passkey-item__header">
|
||||||
|
<div className="passkey-item__info">
|
||||||
|
<h3 className="passkey-item__name">{credential.credentialName}</h3>
|
||||||
|
<p className="passkey-item__meta">
|
||||||
|
Created: {credential.createdAt}
|
||||||
|
{credential.lastUsedAt && (
|
||||||
|
<>
|
||||||
|
<br />Last used: {credential.lastUsedAt}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="passkey-item__badges">
|
||||||
|
{credential.isBackupEligible && (
|
||||||
|
<span className="badge badge--info" title="This passkey can be backed up">
|
||||||
|
<i className="fa fa-cloud" /> Backup Eligible
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{credential.isBackupAuthenticated && (
|
||||||
|
<span className="badge badge--success" title="This passkey is backed up">
|
||||||
|
<i className="fa fa-check" /> Backed Up
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="passkey-item__actions">
|
||||||
|
<button
|
||||||
|
className="btn btn--sm btn--warning"
|
||||||
|
onClick={() => openRenameModal(credential)}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<i className="fa fa-edit" /> Rename
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="btn btn--sm btn--danger"
|
||||||
|
onClick={() => openDeleteModal(credential)}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<i className="fa fa-trash" /> Delete
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<div className="empty-state">
|
||||||
|
<i className="fa fa-key empty-state__icon" />
|
||||||
|
<p className="empty-state__text">No passkeys registered yet.</p>
|
||||||
|
<p className="empty-state__subtext">
|
||||||
|
Add your first passkey to get started with secure, passwordless authentication.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<StyledDialog open={addModalOpen} onClose={closeAddModal}>
|
||||||
|
<DialogTitle>Add New Passkey</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<TextField
|
||||||
|
autoFocus
|
||||||
|
fullWidth
|
||||||
|
id="passkeyName"
|
||||||
|
label="Passkey Name"
|
||||||
|
placeholder="e.g., My Laptop, iPhone"
|
||||||
|
value={passkeyName}
|
||||||
|
onChange={e => setPasskeyName(e.target.value)}
|
||||||
|
onKeyUp={e => 'Enter' === e.key && handleAddPasskey()}
|
||||||
|
margin="dense"
|
||||||
|
variant="outlined"
|
||||||
|
helperText="Give this passkey a descriptive name to help you remember it."
|
||||||
|
/>
|
||||||
|
{status.message && (
|
||||||
|
<div className={`registration-status ${statusClass}`}>
|
||||||
|
{status.message}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={closeAddModal} disabled={loading}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleAddPasskey} variant="contained" disabled={loading}>
|
||||||
|
Continue
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</StyledDialog>
|
||||||
|
|
||||||
|
<StyledDialog open={renameModalOpen} onClose={closeRenameModal}>
|
||||||
|
<DialogTitle>Rename Passkey</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<TextField
|
||||||
|
autoFocus
|
||||||
|
fullWidth
|
||||||
|
id="renamePasskeyName"
|
||||||
|
label="New Name"
|
||||||
|
value={renameName}
|
||||||
|
onChange={e => setRenameName(e.target.value)}
|
||||||
|
onKeyUp={e => 'Enter' === e.key && handleRename()}
|
||||||
|
margin="dense"
|
||||||
|
variant="outlined"
|
||||||
|
/>
|
||||||
|
{status.message && (
|
||||||
|
<div className={`registration-status ${statusClass}`}>
|
||||||
|
{status.message}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={closeRenameModal} disabled={loading}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleRename} variant="contained" disabled={loading}>
|
||||||
|
Rename
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</StyledDialog>
|
||||||
|
|
||||||
|
<StyledDialog open={deleteModalOpen} onClose={closeDeleteModal}>
|
||||||
|
<DialogTitle>Delete Passkey</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<p>
|
||||||
|
Are you sure you want to delete the passkey
|
||||||
|
{selectedCredential && (
|
||||||
|
<Fragment>
|
||||||
|
<strong>{selectedCredential.credentialName}</strong>?
|
||||||
|
This action cannot be undone.
|
||||||
|
</Fragment>
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
{status.message && (
|
||||||
|
<div className={`registration-status ${statusClass}`}>
|
||||||
|
{status.message}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={closeDeleteModal} disabled={loading}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleDelete} variant="contained" color="error" disabled={loading}>
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</StyledDialog>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PasskeyManager;
|
||||||
|
|
||||||
|
PasskeyManager.propTypes = {
|
||||||
|
credentials: arrayOf(shape({
|
||||||
|
id: string.isRequired,
|
||||||
|
credentialName: string.isRequired,
|
||||||
|
createdAt: string,
|
||||||
|
lastUsedAt: string,
|
||||||
|
isBackupEligible: bool,
|
||||||
|
isBackupAuthenticated: bool,
|
||||||
|
})).isRequired,
|
||||||
|
apiRoutes: shape({
|
||||||
|
credentials: string.isRequired,
|
||||||
|
registrationBegin: string.isRequired,
|
||||||
|
registrationComplete: string.isRequired,
|
||||||
|
}).isRequired,
|
||||||
|
};
|
||||||
@@ -0,0 +1,165 @@
|
|||||||
|
/**
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { BarChart } from '@mui/x-charts/BarChart';
|
||||||
|
import { LineChart } from '@mui/x-charts/LineChart';
|
||||||
|
import { PieChart } from '@mui/x-charts/PieChart';
|
||||||
|
import { createTheme, ThemeProvider } from '@mui/material/styles';
|
||||||
|
import { shape, arrayOf, number, string } from 'prop-types';
|
||||||
|
|
||||||
|
const darkTheme = createTheme({
|
||||||
|
palette: {
|
||||||
|
mode: 'dark',
|
||||||
|
background: { paper: 'transparent' },
|
||||||
|
},
|
||||||
|
typography: {
|
||||||
|
fontFamily: '\'Rajdhani\', sans-serif',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const WIN_COLOR = '#5ee89a';
|
||||||
|
const LOSS_COLOR = '#f67d52';
|
||||||
|
const DRAW_COLOR = '#95cff5';
|
||||||
|
const MINES_COLOR = '#f67d52';
|
||||||
|
const BONUS_COLOR = '#ffd700';
|
||||||
|
|
||||||
|
const axisStyle = {
|
||||||
|
tickLabelStyle: { fill: 'rgba(255,255,255,0.4)', fontSize: 11, fontFamily: '\'Rajdhani\', sans-serif' },
|
||||||
|
labelStyle: { fill: 'rgba(255,255,255,0.4)', fontSize: 11, fontFamily: '\'Rajdhani\', sans-serif' },
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function ProfileCharts({ chartData }) {
|
||||||
|
const { months, wins, losses, draws, pieWins, pieLosses, pieDraws, recentGames } = chartData;
|
||||||
|
const total = pieWins + pieLosses + pieDraws;
|
||||||
|
|
||||||
|
const hasBars = wins.some(v => 0 < v) || losses.some(v => 0 < v) || draws.some(v => 0 < v);
|
||||||
|
const hasRecent = recentGames
|
||||||
|
&& (recentGames.mines?.some(v => 0 < v) || recentGames.bonus?.some(v => 0 < v));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ThemeProvider theme={darkTheme}>
|
||||||
|
<div className="profile-charts">
|
||||||
|
{0 < total && (
|
||||||
|
<div className="profile-chart-block">
|
||||||
|
<h2 className="profile-section__title">
|
||||||
|
<i className="fa fa-pie-chart" /> Result Breakdown
|
||||||
|
</h2>
|
||||||
|
<div className="profile-chart-inner">
|
||||||
|
<PieChart
|
||||||
|
series={[{
|
||||||
|
data: [
|
||||||
|
{ id: 0, value: pieWins, label: `Wins ${pieWins}`, color: WIN_COLOR },
|
||||||
|
{ id: 1, value: pieLosses, label: `Losses ${pieLosses}`, color: LOSS_COLOR },
|
||||||
|
{ id: 2, value: pieDraws, label: `Draws ${pieDraws}`, color: DRAW_COLOR },
|
||||||
|
],
|
||||||
|
innerRadius: 52,
|
||||||
|
paddingAngle: 3,
|
||||||
|
cornerRadius: 4,
|
||||||
|
highlightScope: { fade: 'global', highlight: 'item' },
|
||||||
|
}]}
|
||||||
|
slotProps={{
|
||||||
|
legend: {
|
||||||
|
labelStyle: {
|
||||||
|
fill: 'rgba(255,255,255,0.55)',
|
||||||
|
fontSize: 13,
|
||||||
|
fontFamily: '\'Rajdhani\', sans-serif',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
width={320}
|
||||||
|
height={200}
|
||||||
|
margin={{ top: 10, bottom: 10, left: 10, right: 120 }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{hasBars && (
|
||||||
|
<div className="profile-chart-block">
|
||||||
|
<h2 className="profile-section__title">
|
||||||
|
<i className="fa fa-bar-chart" /> Activity (6 months)
|
||||||
|
</h2>
|
||||||
|
<div className="profile-chart-inner">
|
||||||
|
<BarChart
|
||||||
|
xAxis={[{ scaleType: 'band', data: months, ...axisStyle }]}
|
||||||
|
yAxis={[{ ...axisStyle }]}
|
||||||
|
series={[
|
||||||
|
{ data: wins, label: 'Wins', color: WIN_COLOR, stack: 'total' },
|
||||||
|
{ data: losses, label: 'Losses', color: LOSS_COLOR, stack: 'total' },
|
||||||
|
{ data: draws, label: 'Draws', color: DRAW_COLOR, stack: 'total' },
|
||||||
|
]}
|
||||||
|
slotProps={{
|
||||||
|
legend: {
|
||||||
|
labelStyle: {
|
||||||
|
fill: 'rgba(255,255,255,0.55)',
|
||||||
|
fontSize: 13,
|
||||||
|
fontFamily: '\'Rajdhani\', sans-serif',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
borderRadius={3}
|
||||||
|
width={460}
|
||||||
|
height={200}
|
||||||
|
margin={{ top: 10, bottom: 30, left: 30, right: 120 }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{hasRecent && (
|
||||||
|
<div className="profile-chart-block profile-chart-block--wide">
|
||||||
|
<h2 className="profile-section__title">
|
||||||
|
<i className="fa fa-line-chart" /> Last {recentGames.labels.length} games — mines & bonus
|
||||||
|
</h2>
|
||||||
|
<div className="profile-chart-inner">
|
||||||
|
<LineChart
|
||||||
|
xAxis={[{ scaleType: 'band', data: recentGames.labels, ...axisStyle }]}
|
||||||
|
yAxis={[{ ...axisStyle }]}
|
||||||
|
series={[
|
||||||
|
{ data: recentGames.mines, label: 'Mines hit', color: MINES_COLOR },
|
||||||
|
{ data: recentGames.bonus, label: 'Bonus points', color: BONUS_COLOR },
|
||||||
|
]}
|
||||||
|
slotProps={{
|
||||||
|
legend: {
|
||||||
|
labelStyle: {
|
||||||
|
fill: 'rgba(255,255,255,0.55)',
|
||||||
|
fontSize: 13,
|
||||||
|
fontFamily: '\'Rajdhani\', sans-serif',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
borderRadius={3}
|
||||||
|
height={220}
|
||||||
|
margin={{ top: 10, bottom: 30, left: 40, right: 140 }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</ThemeProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ProfileCharts.propTypes = {
|
||||||
|
chartData: shape({
|
||||||
|
months: arrayOf(string).isRequired,
|
||||||
|
wins: arrayOf(number).isRequired,
|
||||||
|
losses: arrayOf(number).isRequired,
|
||||||
|
draws: arrayOf(number).isRequired,
|
||||||
|
pieWins: number.isRequired,
|
||||||
|
pieLosses: number.isRequired,
|
||||||
|
pieDraws: number.isRequired,
|
||||||
|
recentGames: shape({
|
||||||
|
labels: arrayOf(string).isRequired,
|
||||||
|
mines: arrayOf(number).isRequired,
|
||||||
|
bonus: arrayOf(number).isRequired,
|
||||||
|
}).isRequired,
|
||||||
|
}).isRequired,
|
||||||
|
};
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
/**
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { useMemo } from 'react';
|
||||||
|
import { string } from 'prop-types';
|
||||||
|
|
||||||
|
export const Avatar = ({ name, color, avatarUrl, bonusPoints = 0 }) => {
|
||||||
|
const isRed = 'red' === color;
|
||||||
|
const initials = useMemo(() => (name || '?').slice(0, 2).toUpperCase(), [name]);
|
||||||
|
|
||||||
|
const cssVars = isRed ? {
|
||||||
|
'--bd-avatar-gradient': 'linear-gradient(135deg, rgba(173,10,5,0.6) 0%, rgba(246,125,82,0.4) 100%)',
|
||||||
|
'--bd-avatar-glow': '0 0 0 3px rgba(173,10,5,0.2), 0 0 28px rgba(173,10,5,0.35)',
|
||||||
|
'--bd-avatar-border': 'rgba(173,10,5,0.5)',
|
||||||
|
'--bd-avatar-color': '#f67d52',
|
||||||
|
} : {
|
||||||
|
'--bd-avatar-gradient': 'linear-gradient(135deg, rgba(35,111,135,0.6) 0%, rgba(41,128,185,0.4) 100%)',
|
||||||
|
'--bd-avatar-glow': '0 0 0 3px rgba(35,111,135,0.2), 0 0 28px rgba(35,111,135,0.35)',
|
||||||
|
'--bd-avatar-border': 'rgba(35,111,135,0.5)',
|
||||||
|
'--bd-avatar-color': '#95cff5',
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bd-avatar-wrap" style={cssVars}>
|
||||||
|
<div className="bd-avatar-ring-wrap">
|
||||||
|
<div className="bd-avatar-ring">
|
||||||
|
{avatarUrl
|
||||||
|
? <img src={avatarUrl} alt={name} className="bd-avatar-img" />
|
||||||
|
: initials}
|
||||||
|
</div>
|
||||||
|
{0 < bonusPoints && (
|
||||||
|
<div className="bd-avatar-bonus">
|
||||||
|
<i className="fa fa-star" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<span className="bd-avatar-name">{name}</span>
|
||||||
|
<span className="bd-avatar-side">{isRed ? 'Red' : 'Blue'}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
Avatar.propTypes = {
|
||||||
|
name: string,
|
||||||
|
color: string,
|
||||||
|
avatarUrl: string,
|
||||||
|
bonusPoints: string,
|
||||||
|
};
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
/**
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import { StatRow } from './StatRow';
|
||||||
|
import { object } from 'prop-types';
|
||||||
|
|
||||||
|
export const BonusPoints = ({ game }) => {
|
||||||
|
const hasBonuspoints = useMemo(
|
||||||
|
() => 0 < game?.redBonusPoints
|
||||||
|
|| 0 < game?.blueBonusPoints
|
||||||
|
|| game?.redBonusStats?.blindHits
|
||||||
|
|| game?.blueBonusStats?.blindHits,
|
||||||
|
[
|
||||||
|
game?.blueBonusPoints,
|
||||||
|
game?.blueBonusStats?.blindHits,
|
||||||
|
game?.redBonusPoints,
|
||||||
|
game?.redBonusStats?.blindHits,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
const hasRedNoBonuses = useMemo(
|
||||||
|
() => !game.redBonusStats?.blindHits
|
||||||
|
&& !game.redBonusStats?.chainBest
|
||||||
|
&& !game.redBonusStats?.edgeMines
|
||||||
|
&& !game.redBonusStats?.lastMineHits
|
||||||
|
&& !game.redBonusStats?.biggestReveal,
|
||||||
|
[
|
||||||
|
game.redBonusStats?.biggestReveal,
|
||||||
|
game.redBonusStats?.blindHits,
|
||||||
|
game.redBonusStats?.chainBest,
|
||||||
|
game.redBonusStats?.edgeMines,
|
||||||
|
game.redBonusStats?.lastMineHits,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
const hasBlueNoBonuses = useMemo(
|
||||||
|
() => !game.blueBonusStats?.blindHits
|
||||||
|
&& !game.blueBonusStats?.chainBest
|
||||||
|
&& !game.blueBonusStats?.edgeMines
|
||||||
|
&& !game.blueBonusStats?.lastMineHits
|
||||||
|
&& !game.blueBonusStats?.biggestReveal,
|
||||||
|
[
|
||||||
|
game.blueBonusStats?.biggestReveal,
|
||||||
|
game.blueBonusStats?.blindHits,
|
||||||
|
game.blueBonusStats?.chainBest,
|
||||||
|
game.blueBonusStats?.edgeMines,
|
||||||
|
game.blueBonusStats?.lastMineHits,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!hasBonuspoints) return '';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bd-bonus">
|
||||||
|
<div className="bd-bonus__grid">
|
||||||
|
<div className="bd-bonus__column bd-bonus__column--red">
|
||||||
|
<span className="bd-bonus__heading">
|
||||||
|
<i className="fa fa-star" /> Red Bonus Statistics
|
||||||
|
</span>
|
||||||
|
<div className="bd-bonus__rows">
|
||||||
|
<StatRow icon="fa-star" label="Total Bonus Points" value={(game.redBonusPoints ?? 0).toFixed(1)} valueColor="#ffd700" />
|
||||||
|
{0 < game.redBonusStats?.blindHits && (
|
||||||
|
<StatRow icon="fa-bullseye" label="Blind hits" value={game.redBonusStats.blindHits} />
|
||||||
|
)}
|
||||||
|
{0 < game.redBonusStats?.chainBest && (
|
||||||
|
<StatRow icon="fa-link" label="Best chain" value={game.redBonusStats.chainBest} />
|
||||||
|
)}
|
||||||
|
{0 < game.redBonusStats?.edgeMines && (
|
||||||
|
<StatRow icon="fa-border-all" label="Edge mines" value={game.redBonusStats.edgeMines} />
|
||||||
|
)}
|
||||||
|
{0 < game.redBonusStats?.lastMineHits && (
|
||||||
|
<StatRow icon="fa-hourglass-end" label="Endgame mines" value={game.redBonusStats.lastMineHits} />
|
||||||
|
)}
|
||||||
|
{0 < game.redBonusStats?.biggestReveal && (
|
||||||
|
<StatRow icon="fa-expand" label="Biggest reveal" value={game.redBonusStats.biggestReveal} />
|
||||||
|
)}
|
||||||
|
{hasRedNoBonuses && (
|
||||||
|
<StatRow icon="fa-minus-circle" label="Status" value="No bonuses" valueColor="rgba(255,255,255,0.3)" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="bd-bonus__column bd-bonus__column--blue">
|
||||||
|
<span className="bd-bonus__heading">
|
||||||
|
<i className="fa fa-star" /> Blue Bonus Statistics
|
||||||
|
</span>
|
||||||
|
<div className="bd-bonus__rows">
|
||||||
|
<StatRow icon="fa-star" label="Total Bonus Points" value={(game.blueBonusPoints ?? 0).toFixed(1)} valueColor="#ffd700" />
|
||||||
|
{0 < game.blueBonusStats?.blindHits && (
|
||||||
|
<StatRow icon="fa-bullseye" label="Blind hits" value={game.blueBonusStats.blindHits} />
|
||||||
|
)}
|
||||||
|
{0 < game.blueBonusStats?.chainBest && (
|
||||||
|
<StatRow icon="fa-link" label="Best chain" value={game.blueBonusStats.chainBest} />
|
||||||
|
)}
|
||||||
|
{0 < game.blueBonusStats?.edgeMines && (
|
||||||
|
<StatRow icon="fa-border-all" label="Edge mines" value={game.blueBonusStats.edgeMines} />
|
||||||
|
)}
|
||||||
|
{0 < game.blueBonusStats?.lastMineHits && (
|
||||||
|
<StatRow icon="fa-hourglass-end" label="Endgame mines" value={game.blueBonusStats.lastMineHits} />
|
||||||
|
)}
|
||||||
|
{0 < game.blueBonusStats?.biggestReveal && (
|
||||||
|
<StatRow icon="fa-expand" label="Biggest reveal" value={game.blueBonusStats.biggestReveal} />
|
||||||
|
)}
|
||||||
|
{hasBlueNoBonuses && (
|
||||||
|
<StatRow icon="fa-minus-circle" label="Status" value="No bonuses" valueColor="rgba(255,255,255,0.3)" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
BonusPoints.propTypes = {
|
||||||
|
game: object.isRequired,
|
||||||
|
};
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
/**
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { node, string } from 'prop-types';
|
||||||
|
|
||||||
|
export const StatRow = ({ icon, label, value, valueColor }) => (
|
||||||
|
<div className="bd-stat-row">
|
||||||
|
<i className={`fa ${icon} bd-stat-row__icon`} />
|
||||||
|
<span className="bd-stat-row__label">{label}</span>
|
||||||
|
<span
|
||||||
|
className="bd-stat-row__value"
|
||||||
|
style={valueColor ? { '--bd-stat-value-color': valueColor } : undefined}
|
||||||
|
>
|
||||||
|
{value}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
StatRow.propTypes = {
|
||||||
|
icon: string.isRequired,
|
||||||
|
label: string.isRequired,
|
||||||
|
value: node.isRequired,
|
||||||
|
valueColor: string,
|
||||||
|
};
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
/**
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export { AvatarUpload } from './AvatarUpload';
|
||||||
|
export { BattleDialog } from './BattleDialog';
|
||||||
|
export { default as PasskeyLogin } from './PasskeyLogin';
|
||||||
|
export { default as PasskeyManager } from './PasskeyManager';
|
||||||
|
export { default as ProfileCharts } from './ProfileCharts';
|
||||||
|
export { BonusPoints } from './battle-dialog/BonusPoints';
|
||||||
|
export { Avatar } from './battle-dialog/Avatar';
|
||||||
|
export { StatRow } from './battle-dialog/StatRow';
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
/**
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { useRef } from 'react';
|
||||||
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||||
|
import { GameProvider } from '@mine-contexts';
|
||||||
|
import { GameBoard } from '@mine-components';
|
||||||
|
import { string } from 'prop-types';
|
||||||
|
|
||||||
|
const queryClient = new QueryClient();
|
||||||
|
|
||||||
|
const MineSeeker = ({ env, gameId, opponentName = '' }) => {
|
||||||
|
const isEnvDev = 'dev' === env;
|
||||||
|
const gameAssoc = useRef('' !== gameId ? gameId : crypto.randomUUID()).current;
|
||||||
|
const gameInherited = '' !== gameId;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<QueryClientProvider client={queryClient}>
|
||||||
|
<GameProvider>
|
||||||
|
<GameBoard
|
||||||
|
gameAssoc={gameAssoc}
|
||||||
|
gameInherited={gameInherited}
|
||||||
|
opponentName={opponentName}
|
||||||
|
isEnvDev={isEnvDev}
|
||||||
|
/>
|
||||||
|
</GameProvider>
|
||||||
|
</QueryClientProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MineSeeker;
|
||||||
|
|
||||||
|
MineSeeker.propTypes = {
|
||||||
|
env: string.isRequired,
|
||||||
|
gameId: string,
|
||||||
|
opponentName: string,
|
||||||
|
};
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
/**
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { func, number, string } from 'prop-types';
|
||||||
|
|
||||||
|
const BonusBox = ({ color, points, onClick, title }) => (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`bonus-box ${color}-bonus`}
|
||||||
|
onClick={onClick}
|
||||||
|
title={title || 'View bonus statistics'}
|
||||||
|
aria-label={`${color} bonus points: ${points}`}
|
||||||
|
>
|
||||||
|
<i className="fa fa-star bonus-box__icon" />
|
||||||
|
<span className="bonus-box__value">{points}</span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default BonusBox;
|
||||||
|
|
||||||
|
BonusBox.propTypes = {
|
||||||
|
color: string.isRequired,
|
||||||
|
points: number.isRequired,
|
||||||
|
onClick: func.isRequired,
|
||||||
|
title: string,
|
||||||
|
};
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
/**
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import Dialog from '@mui/material/Dialog';
|
||||||
|
import { styled } from '@mui/material/styles';
|
||||||
|
import { PlayerColumn } from '@mine-components';
|
||||||
|
import { bool, func, shape, string, number, object } from 'prop-types';
|
||||||
|
|
||||||
|
const BonusStatsDialog = ({ open, onClose, red, blue }) => (
|
||||||
|
<StyledDialog open={open} onClose={onClose}>
|
||||||
|
<div className="bsd">
|
||||||
|
<div className="bsd-header">
|
||||||
|
<div className="bsd-header-text">
|
||||||
|
<span className="bsd-label">Scoring</span>
|
||||||
|
<h2 className="bsd-title">
|
||||||
|
<i className="fa fa-star" />
|
||||||
|
Bonus Statistics
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<button className="bsd-close" onClick={onClose} aria-label="Close">
|
||||||
|
<i className="fa fa-times" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="bsd-body">
|
||||||
|
<PlayerColumn color="red" player={red} />
|
||||||
|
<PlayerColumn color="blue" player={blue} />
|
||||||
|
</div>
|
||||||
|
<p className="bsd-note">
|
||||||
|
Bonus points are awarded alongside the main score for skillful play.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</StyledDialog>
|
||||||
|
);
|
||||||
|
|
||||||
|
const StyledDialog = styled(Dialog)({
|
||||||
|
'& .MuiDialog-paper': {
|
||||||
|
background: '#07090d',
|
||||||
|
backgroundImage: `
|
||||||
|
linear-gradient(rgba(35, 111, 135, 0.08) 1px, transparent 1px),
|
||||||
|
linear-gradient(90deg, rgba(35, 111, 135, 0.08) 1px, transparent 1px)
|
||||||
|
`,
|
||||||
|
backgroundSize: '46px 46px',
|
||||||
|
border: '1px solid rgba(35, 111, 135, 0.4)',
|
||||||
|
borderRadius: '12px',
|
||||||
|
boxShadow: '0 0 80px rgba(35, 111, 135, 0.15), 0 32px 80px rgba(0, 0, 0, 0.9)',
|
||||||
|
width: '560px',
|
||||||
|
maxWidth: '94vw',
|
||||||
|
overflow: 'hidden',
|
||||||
|
color: '#fff',
|
||||||
|
},
|
||||||
|
'& .MuiBackdrop-root': {
|
||||||
|
background: 'rgba(2, 4, 8, 0.88)',
|
||||||
|
backdropFilter: 'blur(4px)',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default BonusStatsDialog;
|
||||||
|
|
||||||
|
BonusStatsDialog.propTypes = {
|
||||||
|
open: bool.isRequired,
|
||||||
|
onClose: func.isRequired,
|
||||||
|
red: shape({
|
||||||
|
name: string,
|
||||||
|
bonusPoints: number,
|
||||||
|
bonusStats: object,
|
||||||
|
}).isRequired,
|
||||||
|
blue: shape({
|
||||||
|
name: string,
|
||||||
|
bonusPoints: number,
|
||||||
|
bonusStats: object,
|
||||||
|
}).isRequired,
|
||||||
|
};
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
/**
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import 'cap-widget';
|
||||||
|
import React, { Fragment, useCallback, useEffect, useRef, useState } from 'react';
|
||||||
|
import { func, node, string } from 'prop-types';
|
||||||
|
|
||||||
|
const CAPTCHA_STORAGE_KEY = 'mineseeker_captcha_verified';
|
||||||
|
const CAPTCHA_TOKEN_KEY = 'mineseeker_captcha_token';
|
||||||
|
|
||||||
|
const CaptchaOverlay = ({ apiEndpoint, onVerified, children }) => {
|
||||||
|
const [verified, setVerified] = useState(false);
|
||||||
|
const capRef = useRef(null);
|
||||||
|
|
||||||
|
const handleToken = useCallback(token => {
|
||||||
|
const wrapper = document.getElementById('mine-wrapper');
|
||||||
|
if (wrapper) {
|
||||||
|
wrapper.dataset.captchaToken = token;
|
||||||
|
}
|
||||||
|
sessionStorage.setItem(CAPTCHA_TOKEN_KEY, token);
|
||||||
|
sessionStorage.setItem(CAPTCHA_STORAGE_KEY, Date.now().toString());
|
||||||
|
setVerified(true);
|
||||||
|
onVerified?.();
|
||||||
|
}, [onVerified]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const storedToken = sessionStorage.getItem(CAPTCHA_TOKEN_KEY);
|
||||||
|
const storedTime = sessionStorage.getItem(CAPTCHA_STORAGE_KEY);
|
||||||
|
|
||||||
|
if (storedToken && storedTime) {
|
||||||
|
const elapsed = (Date.now() - parseInt(storedTime)) / 1000;
|
||||||
|
if (110 > elapsed) {
|
||||||
|
const wrapper = document.getElementById('mine-wrapper');
|
||||||
|
if (wrapper) {
|
||||||
|
wrapper.dataset.captchaToken = storedToken;
|
||||||
|
}
|
||||||
|
setVerified(true);
|
||||||
|
onVerified?.();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [onVerified]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const widget = document.createElement('cap-widget');
|
||||||
|
widget.style.display = 'none';
|
||||||
|
capRef.current = widget;
|
||||||
|
document.body.appendChild(widget);
|
||||||
|
|
||||||
|
const cap = new window.Cap({ apiEndpoint }, widget);
|
||||||
|
let cancelled = false;
|
||||||
|
|
||||||
|
const run = async () => {
|
||||||
|
try {
|
||||||
|
const result = await cap.solve();
|
||||||
|
if (!cancelled && result?.token) {
|
||||||
|
handleToken(result.token);
|
||||||
|
}
|
||||||
|
} catch (_) {
|
||||||
|
if (!cancelled) {
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!cancelled) {
|
||||||
|
run();
|
||||||
|
}
|
||||||
|
}, 1200);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
run();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
widget.remove();
|
||||||
|
capRef.current = null;
|
||||||
|
};
|
||||||
|
}, [apiEndpoint, handleToken]);
|
||||||
|
|
||||||
|
if (verified) {
|
||||||
|
return <Fragment>{children}</Fragment>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="captcha-overlay">
|
||||||
|
<div className="captcha-content">
|
||||||
|
<div className="captcha-icon">
|
||||||
|
<i className="fa fa-shield-halved" />
|
||||||
|
</div>
|
||||||
|
<h1 className="captcha-title">Ready to Play?</h1>
|
||||||
|
<p className="captcha-description">
|
||||||
|
Verifying your session...
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CaptchaOverlay;
|
||||||
|
|
||||||
|
CaptchaOverlay.propTypes = {
|
||||||
|
apiEndpoint: string.isRequired,
|
||||||
|
onVerified: func,
|
||||||
|
children: node,
|
||||||
|
};
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
/**
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
import { Fragment, useEffect, useState } from 'react';
|
||||||
|
import { func, number } from 'prop-types';
|
||||||
|
|
||||||
|
const ChallengeCountdown = ({ onAccept, onDecline, seconds = 30 }) => {
|
||||||
|
const [countdown, setCountdown] = useState(seconds);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
setCountdown(prev => {
|
||||||
|
if (1 >= prev) {
|
||||||
|
clearInterval(interval);
|
||||||
|
onDecline();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return prev - 1;
|
||||||
|
});
|
||||||
|
}, 1000);
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, [onDecline]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<p style={{ textAlign: 'center', marginBottom: 20, color: '#95cff5' }}>
|
||||||
|
You have {countdown} second{1 === countdown ? '' : 's'} to answer to the challenge!
|
||||||
|
</p>
|
||||||
|
<div className="resign">
|
||||||
|
<a onClick={onAccept}>Accept</a>
|
||||||
|
<a onClick={onDecline}>Decline</a>
|
||||||
|
</div>
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ChallengeCountdown;
|
||||||
|
|
||||||
|
ChallengeCountdown.propTypes = {
|
||||||
|
onAccept: func.isRequired,
|
||||||
|
onDecline: func.isRequired,
|
||||||
|
seconds: number,
|
||||||
|
};
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
/**
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { useGame } from '@mine-contexts';
|
||||||
|
import { useServerCommunication } from '@mine-hooks';
|
||||||
|
import CaptchaOverlay from './CaptchaOverlay';
|
||||||
|
import GridControl from './grid/GridControl';
|
||||||
|
import { bool, string } from 'prop-types';
|
||||||
|
|
||||||
|
export const GameBoard = ({ gameAssoc, gameInherited, opponentName = '', isEnvDev }) => {
|
||||||
|
const { gridReady } = useGame();
|
||||||
|
const { onClick, resign } = useServerCommunication(gameAssoc, gameInherited, opponentName, isEnvDev);
|
||||||
|
const [captchaVerified, setCaptchaVerified] = useState(false);
|
||||||
|
|
||||||
|
const apiEndpoint = document.getElementById('mine-wrapper')?.dataset.capApiEndpoint;
|
||||||
|
|
||||||
|
if (!gridReady) {
|
||||||
|
return (
|
||||||
|
<div className="game-overlay">
|
||||||
|
<div className="game-overlay-window"><h1>Loading…</h1></div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!captchaVerified && apiEndpoint) {
|
||||||
|
return (
|
||||||
|
<CaptchaOverlay apiEndpoint={apiEndpoint} onVerified={() => setCaptchaVerified(true)} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<GridControl
|
||||||
|
gameAssoc={gameAssoc}
|
||||||
|
onClick={onClick}
|
||||||
|
resign={resign}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
GameBoard.propTypes = {
|
||||||
|
gameAssoc: string.isRequired,
|
||||||
|
gameInherited: bool.isRequired,
|
||||||
|
opponentName: string,
|
||||||
|
isEnvDev: bool,
|
||||||
|
};
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
/**
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
|
import { BonusBox, BonusStatsDialog, Avatar } from '@mine-components';
|
||||||
|
import { formatTime } from '@global-utils/format';
|
||||||
|
import { useGame } from '@mine-contexts';
|
||||||
|
|
||||||
|
const GameTimer = () => {
|
||||||
|
const { overlay, connectionLost, endRef, activePlayer, red, blue } = useGame();
|
||||||
|
const [redTime, setRedTime] = useState(0);
|
||||||
|
const [blueTime, setBlueTime] = useState(0);
|
||||||
|
const [isRunning, setIsRunning] = useState(false);
|
||||||
|
const [bonusDialogOpen, setBonusDialogOpen] = useState(false);
|
||||||
|
const timerIntervalRef = useRef(null);
|
||||||
|
const gameStartedRef = useRef(false);
|
||||||
|
|
||||||
|
const redStartTimeRef = useRef(null);
|
||||||
|
const blueStartTimeRef = useRef(null);
|
||||||
|
const lastActivePlayerRef = useRef(null);
|
||||||
|
const pausedRedTimeRef = useRef(0);
|
||||||
|
const pausedBlueTimeRef = useRef(0);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!overlay && !gameStartedRef.current) {
|
||||||
|
gameStartedRef.current = true;
|
||||||
|
setIsRunning(true);
|
||||||
|
setRedTime(0);
|
||||||
|
setBlueTime(0);
|
||||||
|
redStartTimeRef.current = Date.now();
|
||||||
|
blueStartTimeRef.current = Date.now();
|
||||||
|
pausedRedTimeRef.current = 0;
|
||||||
|
pausedBlueTimeRef.current = 0;
|
||||||
|
lastActivePlayerRef.current = activePlayer;
|
||||||
|
}
|
||||||
|
}, [activePlayer, overlay]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (endRef.current) setIsRunning(false);
|
||||||
|
}, [endRef]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (connectionLost) setIsRunning(false);
|
||||||
|
}, [connectionLost]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isRunning) return;
|
||||||
|
|
||||||
|
if (lastActivePlayerRef.current !== activePlayer) {
|
||||||
|
const startRef = lastActivePlayerRef.current ? blueStartTimeRef.current : redStartTimeRef.current;
|
||||||
|
if (startRef) {
|
||||||
|
const elapsed = Math.floor((Date.now() - startRef) / 1000);
|
||||||
|
if (lastActivePlayerRef.current) {
|
||||||
|
pausedBlueTimeRef.current += elapsed;
|
||||||
|
} else {
|
||||||
|
pausedRedTimeRef.current += elapsed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activePlayer) {
|
||||||
|
blueStartTimeRef.current = Date.now();
|
||||||
|
} else {
|
||||||
|
redStartTimeRef.current = Date.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
lastActivePlayerRef.current = activePlayer;
|
||||||
|
}
|
||||||
|
}, [activePlayer, isRunning]);
|
||||||
|
|
||||||
|
const syncTimes = useCallback(() => {
|
||||||
|
let currentRedTime = pausedRedTimeRef.current;
|
||||||
|
let currentBlueTime = pausedBlueTimeRef.current;
|
||||||
|
|
||||||
|
if (!activePlayer && redStartTimeRef.current) {
|
||||||
|
currentRedTime += Math.floor((Date.now() - redStartTimeRef.current) / 1000);
|
||||||
|
} else if (activePlayer && blueStartTimeRef.current) {
|
||||||
|
currentBlueTime += Math.floor((Date.now() - blueStartTimeRef.current) / 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
setRedTime(currentRedTime);
|
||||||
|
setBlueTime(currentBlueTime);
|
||||||
|
}, [activePlayer]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isRunning) {
|
||||||
|
if (timerIntervalRef.current) clearInterval(timerIntervalRef.current);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
timerIntervalRef.current = setInterval(syncTimes, 100);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (timerIntervalRef.current) clearInterval(timerIntervalRef.current);
|
||||||
|
};
|
||||||
|
}, [isRunning, activePlayer, syncTimes]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleFocus = () => {
|
||||||
|
if (isRunning) syncTimes();
|
||||||
|
};
|
||||||
|
window.addEventListener('focus', handleFocus);
|
||||||
|
return () => window.removeEventListener('focus', handleFocus);
|
||||||
|
}, [isRunning, activePlayer, syncTimes]);
|
||||||
|
|
||||||
|
useEffect(() => () => {
|
||||||
|
if (timerIntervalRef.current) clearInterval(timerIntervalRef.current);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="game-timer-container">
|
||||||
|
<BonusBox color="red" points={red.bonusPoints ?? 0} onClick={() => setBonusDialogOpen(true)} />
|
||||||
|
<div className={`game-timer red-timer ${!activePlayer ? 'active' : ''}`}>
|
||||||
|
<Avatar player={red} />
|
||||||
|
<i className={`fa ${!activePlayer ? 'fa-hourglass-half' : 'fa-hourglass-start'} timer-icon`} />
|
||||||
|
<span className="timer-display">{formatTime(redTime)}</span>
|
||||||
|
</div>
|
||||||
|
<div className={`game-timer blue-timer ${activePlayer ? 'active' : ''}`}>
|
||||||
|
<Avatar player={blue} />
|
||||||
|
<i className={`fa ${activePlayer ? 'fa-hourglass-half' : 'fa-hourglass-start'} timer-icon`} />
|
||||||
|
<span className="timer-display">{formatTime(blueTime)}</span>
|
||||||
|
</div>
|
||||||
|
<BonusBox color="blue" points={blue.bonusPoints ?? 0} onClick={() => setBonusDialogOpen(true)} />
|
||||||
|
<BonusStatsDialog open={bonusDialogOpen} onClose={() => setBonusDialogOpen(false)} red={red} blue={blue} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default GameTimer;
|
||||||
@@ -0,0 +1,294 @@
|
|||||||
|
/**
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
|
import { formatSince } from '@global-utils/format';
|
||||||
|
import Dialog from '@mui/material/Dialog';
|
||||||
|
import { styled } from '@mui/material/styles';
|
||||||
|
import { useLobbyDataProvider } from '@mine-hooks';
|
||||||
|
import { bool, func, string } from 'prop-types';
|
||||||
|
|
||||||
|
const OnlinePlayersDialog = ({ open, onClose, currentGameAssoc, isEnvDev = false }) => {
|
||||||
|
const [players, setPlayers] = useState([]);
|
||||||
|
const [search, setSearch] = useState('');
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [refreshKey, setRefreshKey] = useState(0);
|
||||||
|
const [snapshotLoaded, setSnapshotLoaded] = useState(false);
|
||||||
|
const [challengingGameAssoc, setChallengingGameAssoc] = useState(null);
|
||||||
|
const [declinedMsg, setDeclinedMsg] = useState('');
|
||||||
|
const [waitingCountdown, setWaitingCountdown] = useState(0);
|
||||||
|
const declinedTimerRef = useRef(null);
|
||||||
|
const { waitingPlayersQuery, challengeMutation } = useLobbyDataProvider();
|
||||||
|
|
||||||
|
const addPlayer = useCallback(entry => {
|
||||||
|
setPlayers(prev =>
|
||||||
|
prev.some(p => p.gameAssoc === entry.gameAssoc)
|
||||||
|
? prev
|
||||||
|
: [...prev, entry],
|
||||||
|
);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const removePlayer = useCallback(gameAssoc => {
|
||||||
|
setPlayers(prev => prev.filter(p => p.gameAssoc !== gameAssoc));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open) return;
|
||||||
|
setLoading(true);
|
||||||
|
setSnapshotLoaded(false);
|
||||||
|
|
||||||
|
waitingPlayersQuery.refetch().then(result => {
|
||||||
|
if (result.data) {
|
||||||
|
// Filter out current user's game from the snapshot
|
||||||
|
const filtered = result.data.filter(p => p.gameAssoc !== currentGameAssoc);
|
||||||
|
setPlayers(filtered);
|
||||||
|
}
|
||||||
|
setSnapshotLoaded(true);
|
||||||
|
setLoading(false);
|
||||||
|
}).catch(() => {
|
||||||
|
setPlayers([]);
|
||||||
|
setSnapshotLoaded(true);
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [open, refreshKey, currentGameAssoc]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open || !snapshotLoaded) return;
|
||||||
|
setSearch('');
|
||||||
|
|
||||||
|
const wrapper = document.getElementById('mine-wrapper');
|
||||||
|
const hubUrl = wrapper?.dataset?.mercureHubUrl;
|
||||||
|
const jwt = wrapper?.dataset?.mercureSubscriberJwt;
|
||||||
|
if (!hubUrl) return;
|
||||||
|
|
||||||
|
const url = new URL(hubUrl, window.location.origin);
|
||||||
|
url.searchParams.append('topic', 'mineseeker/lobby');
|
||||||
|
if (jwt) url.searchParams.append('authorization', jwt);
|
||||||
|
|
||||||
|
const es = new EventSource(url.toString());
|
||||||
|
es.onmessage = e => {
|
||||||
|
const { action, gameAssoc, name, since } = JSON.parse(e.data);
|
||||||
|
// Don't add the current user's game to the list
|
||||||
|
if (gameAssoc === currentGameAssoc) return;
|
||||||
|
if ('join' === action) addPlayer({ gameAssoc, name, since });
|
||||||
|
if ('leave' === action) removePlayer(gameAssoc);
|
||||||
|
};
|
||||||
|
|
||||||
|
return () => es.close();
|
||||||
|
}, [open, snapshotLoaded, addPlayer, removePlayer, currentGameAssoc]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (challengeMutation.isError) {
|
||||||
|
setChallengingGameAssoc(null);
|
||||||
|
setWaitingCountdown(0);
|
||||||
|
}
|
||||||
|
}, [challengeMutation.isError]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handler = () => {
|
||||||
|
setChallengingGameAssoc(null);
|
||||||
|
clearTimeout(declinedTimerRef.current);
|
||||||
|
setDeclinedMsg('Challenge was not accepted.');
|
||||||
|
setWaitingCountdown(0);
|
||||||
|
declinedTimerRef.current = setTimeout(() => setDeclinedMsg(''), 3500);
|
||||||
|
};
|
||||||
|
window.addEventListener('challenge-declined', handler);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('challenge-declined', handler);
|
||||||
|
clearTimeout(declinedTimerRef.current);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!waitingCountdown) return;
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
setWaitingCountdown(prev => {
|
||||||
|
if (1 >= prev) return 0;
|
||||||
|
return prev - 1;
|
||||||
|
});
|
||||||
|
}, 1000);
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, [waitingCountdown]);
|
||||||
|
|
||||||
|
const handleChallenge = player => {
|
||||||
|
if (challengingGameAssoc) return;
|
||||||
|
setChallengingGameAssoc(player.gameAssoc);
|
||||||
|
setDeclinedMsg('');
|
||||||
|
setWaitingCountdown(30);
|
||||||
|
|
||||||
|
challengeMutation.mutate(
|
||||||
|
{ targetGameAssoc: player.gameAssoc, challengerGameAssoc: currentGameAssoc }
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const visible = players
|
||||||
|
.filter(p => p.gameAssoc !== currentGameAssoc)
|
||||||
|
.filter(p => p.name.toLowerCase().includes(search.toLowerCase()));
|
||||||
|
|
||||||
|
const shown = visible.slice(0, 5);
|
||||||
|
const hasMore = 5 < visible.length;
|
||||||
|
|
||||||
|
// Debug: log if currentGameAssoc is undefined or if current user appears
|
||||||
|
if (isEnvDev && 0 < players.length) {
|
||||||
|
const userInList = players.find(p => p.gameAssoc === currentGameAssoc);
|
||||||
|
|
||||||
|
if (userInList) {
|
||||||
|
console.warn('[OnlinePlayersDialog] Current user appears in players list:', { currentGameAssoc, userInList });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledDialog
|
||||||
|
open={open}
|
||||||
|
onClose={0 < waitingCountdown ? undefined : onClose}
|
||||||
|
>
|
||||||
|
<div className="opd">
|
||||||
|
<div className="opd-header">
|
||||||
|
<div className="opd-header-text">
|
||||||
|
<span className="opd-label">Multiplayer</span>
|
||||||
|
<h2 className="opd-title">
|
||||||
|
<i className="fa fa-users" />
|
||||||
|
Online Players
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div className="opd-header-actions">
|
||||||
|
<button
|
||||||
|
className={`opd-refresh${loading ? ' opd-refresh--spin' : ''}`}
|
||||||
|
onClick={() => { if (0 === waitingCountdown) setRefreshKey(k => k + 1); }}
|
||||||
|
disabled={loading || 0 < waitingCountdown}
|
||||||
|
aria-label="Refresh"
|
||||||
|
title="Refresh list"
|
||||||
|
>
|
||||||
|
<i className="fa fa-refresh" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="opd-close"
|
||||||
|
onClick={() => { if (0 === waitingCountdown) onClose(); }}
|
||||||
|
disabled={0 < waitingCountdown}
|
||||||
|
aria-label="Close"
|
||||||
|
>
|
||||||
|
<i className="fa fa-times" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{0 < waitingCountdown ? (
|
||||||
|
<div className="opd-waiting">
|
||||||
|
<i className="fa fa-hourglass-start" />
|
||||||
|
<p>Waiting {waitingCountdown} second{1 === waitingCountdown ? '' : 's'} for opponent's answer...</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="opd-search-wrap">
|
||||||
|
<i className="fa fa-search opd-search-icon" />
|
||||||
|
<input
|
||||||
|
className="opd-search"
|
||||||
|
placeholder="Search by username…"
|
||||||
|
value={search}
|
||||||
|
onChange={e => setSearch(e.target.value)}
|
||||||
|
/>
|
||||||
|
{search && (
|
||||||
|
<button className="opd-search-clear" onClick={() => setSearch('')}>
|
||||||
|
<i className="fa fa-times" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="opd-list">
|
||||||
|
{loading && (
|
||||||
|
<div className="opd-empty">
|
||||||
|
<i className="fa fa-spinner fa-spin" />
|
||||||
|
<p>Loading players…</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!loading && 0 === shown.length && (
|
||||||
|
<div className="opd-empty">
|
||||||
|
<i className="fa fa-hourglass-half" />
|
||||||
|
<p>
|
||||||
|
{search
|
||||||
|
? 'No players match your search.'
|
||||||
|
: 'No other players are waiting right now.'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{declinedMsg && (
|
||||||
|
<div className="opd-declined">
|
||||||
|
<i className="fa fa-times-circle" />
|
||||||
|
{' '}{declinedMsg}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!loading && shown.map(player => {
|
||||||
|
const isWaiting = challengingGameAssoc === player.gameAssoc;
|
||||||
|
return (
|
||||||
|
<div key={player.gameAssoc} className="opd-row">
|
||||||
|
<div className="opd-avatar">
|
||||||
|
{player.name.slice(0, 2).toUpperCase()}
|
||||||
|
</div>
|
||||||
|
<div className="opd-info">
|
||||||
|
<span className="opd-name">{player.name}</span>
|
||||||
|
<span className="opd-since">
|
||||||
|
<i className="fa fa-clock" />
|
||||||
|
{' '}Waiting {formatSince(player.since)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
className={`opd-join${isWaiting ? ' opd-join--waiting' : ''}`}
|
||||||
|
onClick={() => handleChallenge(player)}
|
||||||
|
disabled={!!challengingGameAssoc}
|
||||||
|
>
|
||||||
|
<i className={`fa ${isWaiting ? 'fa-spinner fa-spin' : 'fa-play'}`} />
|
||||||
|
{isWaiting ? 'Waiting...' : 'Join'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!loading && hasMore && (
|
||||||
|
<p className="opd-note">
|
||||||
|
Showing 5 of {visible.length} waiting players
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</StyledDialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const StyledDialog = styled(Dialog)({
|
||||||
|
'& .MuiDialog-paper': {
|
||||||
|
background: '#07090d',
|
||||||
|
backgroundImage: `
|
||||||
|
linear-gradient(rgba(35, 111, 135, 0.08) 1px, transparent 1px),
|
||||||
|
linear-gradient(90deg, rgba(35, 111, 135, 0.08) 1px, transparent 1px)
|
||||||
|
`,
|
||||||
|
backgroundSize: '46px 46px',
|
||||||
|
border: '1px solid rgba(35, 111, 135, 0.4)',
|
||||||
|
borderRadius: '12px',
|
||||||
|
boxShadow: '0 0 80px rgba(35, 111, 135, 0.15), 0 32px 80px rgba(0, 0, 0, 0.9)',
|
||||||
|
width: '500px',
|
||||||
|
maxWidth: '94vw',
|
||||||
|
overflow: 'hidden',
|
||||||
|
color: '#fff',
|
||||||
|
},
|
||||||
|
'& .MuiBackdrop-root': {
|
||||||
|
background: 'rgba(2, 4, 8, 0.88)',
|
||||||
|
backdropFilter: 'blur(4px)',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default OnlinePlayersDialog;
|
||||||
|
|
||||||
|
OnlinePlayersDialog.propTypes = {
|
||||||
|
open: bool.isRequired,
|
||||||
|
onClose: func.isRequired,
|
||||||
|
currentGameAssoc: string,
|
||||||
|
isEnvDev: bool,
|
||||||
|
};
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
/**
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
import { Fragment, useState } from 'react';
|
||||||
|
import { OnlinePlayersDialog } from '@mine-components';
|
||||||
|
import { bool, string } from 'prop-types';
|
||||||
|
|
||||||
|
const WaitingOverlayContent = ({ shareUrl, currentGameAssoc, opponentName = '', inviteOnly = false }) => {
|
||||||
|
const [dialogOpen, setDialogOpen] = useState(false);
|
||||||
|
const inviteHeader = inviteOnly && opponentName
|
||||||
|
? `Invite ${opponentName}`
|
||||||
|
: 'Invite a Friend';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<div className={`waiting-options${inviteOnly ? ' waiting-options--invite-only' : ''}`}>
|
||||||
|
<div className="waiting-option">
|
||||||
|
<div className="waiting-option-header">
|
||||||
|
<i className="fa fa-link" />
|
||||||
|
<span>{inviteHeader}</span>
|
||||||
|
</div>
|
||||||
|
<p className="waiting-option-desc">Share this link with your opponent</p>
|
||||||
|
<ShareLinkBox
|
||||||
|
url={shareUrl}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{!inviteOnly && (
|
||||||
|
<Fragment>
|
||||||
|
<div className="waiting-divider">
|
||||||
|
<span>OR</span>
|
||||||
|
</div>
|
||||||
|
<div className="waiting-option">
|
||||||
|
<div className="waiting-option-header">
|
||||||
|
<i className="fa fa-users" />
|
||||||
|
<span>Challenge a Player</span>
|
||||||
|
</div>
|
||||||
|
<p className="waiting-option-desc">Browse online players and challenge them</p>
|
||||||
|
<button
|
||||||
|
className="browse-players-btn"
|
||||||
|
onClick={() => setDialogOpen(true)}
|
||||||
|
>
|
||||||
|
<i className="fa fa-search" />
|
||||||
|
Browse Players
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</Fragment>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!inviteOnly && (
|
||||||
|
<OnlinePlayersDialog
|
||||||
|
open={dialogOpen}
|
||||||
|
onClose={() => setDialogOpen(false)}
|
||||||
|
currentGameAssoc={currentGameAssoc}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ShareLinkBox = ({ url }) => {
|
||||||
|
const [copied, setCopied] = useState(false);
|
||||||
|
|
||||||
|
const handleCopy = () => {
|
||||||
|
navigator.clipboard.writeText(url)
|
||||||
|
.then(() => {
|
||||||
|
setCopied(true);
|
||||||
|
setTimeout(() => setCopied(false), 2500);
|
||||||
|
})
|
||||||
|
.catch(() => null);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="share-invite">
|
||||||
|
<div className="share-url-box" onClick={e => e.currentTarget.querySelector('input').select()}>
|
||||||
|
<i className="fa fa-link share-url-icon" />
|
||||||
|
<input
|
||||||
|
className="share-url-input"
|
||||||
|
readOnly
|
||||||
|
value={url}
|
||||||
|
onClick={e => e.target.select()}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button className={`share-copy-btn${copied ? ' copied' : ''}`} onClick={handleCopy}>
|
||||||
|
<i className={`fa ${copied ? 'fa-check' : 'fa-clipboard'}`} />
|
||||||
|
{copied ? 'Copied!' : 'Copy Link'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default WaitingOverlayContent;
|
||||||
|
|
||||||
|
WaitingOverlayContent.propTypes = {
|
||||||
|
shareUrl: string.isRequired,
|
||||||
|
currentGameAssoc: string,
|
||||||
|
opponentName: string,
|
||||||
|
inviteOnly: bool,
|
||||||
|
};
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
/**
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { Fragment, useState } from 'react';
|
||||||
|
import { useGame } from '@mine-contexts';
|
||||||
|
import GridField from './GridField';
|
||||||
|
import UserControl from '../user/UserControl';
|
||||||
|
import GameTimer from '../GameTimer';
|
||||||
|
import { BOMB_SYMBOLS, bombRadius } from '@mine-utils';
|
||||||
|
import { func, string } from 'prop-types';
|
||||||
|
|
||||||
|
const GridControl = ({ gameAssoc, onClick, resign }) => {
|
||||||
|
const {
|
||||||
|
overlay, overlayTitle, overlaySubTitle,
|
||||||
|
webPlayer, activePlayer, bombSelected,
|
||||||
|
cells, setCells, endRef,
|
||||||
|
} = useGame();
|
||||||
|
|
||||||
|
const [copied, setCopied] = useState(false);
|
||||||
|
const shareUrl = gameAssoc ? `${window.location.origin}/play/${gameAssoc}` : null;
|
||||||
|
const endShareUrl = gameAssoc ? `${window.location.origin}/battle/${gameAssoc}` : null;
|
||||||
|
const isAuthenticated = '1' === document.getElementById('mine-wrapper')?.dataset.isAuthenticated;
|
||||||
|
|
||||||
|
const handleShare = () => {
|
||||||
|
const url = endRef.current ? endShareUrl : shareUrl;
|
||||||
|
if (!url) return;
|
||||||
|
navigator.clipboard.writeText(url).then(() => {
|
||||||
|
setCopied(true);
|
||||||
|
setTimeout(() => setCopied(false), 2200);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleHover = (row, col) => {
|
||||||
|
if (!bombSelected) return;
|
||||||
|
const activeColor = activePlayer ? 'blue' : 'red';
|
||||||
|
if (activeColor !== webPlayer) return;
|
||||||
|
|
||||||
|
setCells(prev => {
|
||||||
|
const next = prev.map(r => r.map(c =>
|
||||||
|
null !== c.bombTargetArea ? { ...c, bombTargetArea: null } : c,
|
||||||
|
));
|
||||||
|
bombRadius(row, col, prev.length, prev[0]?.length ?? 0).forEach(([r, c], i) => {
|
||||||
|
if (!next[r]?.[c]) return;
|
||||||
|
|
||||||
|
next[r] = [...next[r]];
|
||||||
|
next[r][c] = { ...next[r][c], bombTargetArea: BOMB_SYMBOLS[i] };
|
||||||
|
});
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<GameTimer />
|
||||||
|
<div className="game-wrapper">
|
||||||
|
<div className={`game-overlay${overlay ? '' : ' hide'}`}>
|
||||||
|
<div className="game-overlay-window">
|
||||||
|
<h1>{overlayTitle}</h1>
|
||||||
|
{'string' === typeof overlaySubTitle && (
|
||||||
|
<h2>{overlaySubTitle}</h2>
|
||||||
|
)}
|
||||||
|
{'string' !== typeof overlaySubTitle && (
|
||||||
|
<Fragment>{overlaySubTitle}</Fragment>
|
||||||
|
)}
|
||||||
|
{gameAssoc && endRef.current && (
|
||||||
|
<div className="game-overlay-actions">
|
||||||
|
<button
|
||||||
|
className={`game-overlay-share${copied ? ' copied' : ''}`}
|
||||||
|
onClick={handleShare}
|
||||||
|
title="Copy share link"
|
||||||
|
aria-label="Copy share link"
|
||||||
|
>
|
||||||
|
<i className={`fa ${copied ? 'fa-check' : 'fa-share-alt'}`} />
|
||||||
|
{copied ? 'Copied!' : 'Share Battle'}
|
||||||
|
</button>
|
||||||
|
<a
|
||||||
|
className="game-overlay-profile"
|
||||||
|
href={isAuthenticated ? '/profile' : '/'}
|
||||||
|
title={isAuthenticated ? 'Go to your profile' : 'Go to homepage'}
|
||||||
|
aria-label={isAuthenticated ? 'Go to your profile' : 'Go to homepage'}
|
||||||
|
>
|
||||||
|
<i className={`fa ${isAuthenticated ? 'fa-user' : 'fa-house'}`} />
|
||||||
|
{isAuthenticated ? 'My Profile' : 'Homepage'}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<UserControl
|
||||||
|
resign={resign}
|
||||||
|
/>
|
||||||
|
<div className="grid-container">
|
||||||
|
<div className="grid">
|
||||||
|
{cells.flatMap((row, r) =>
|
||||||
|
row.map((cell, c) => (
|
||||||
|
<GridField
|
||||||
|
key={`${r}_${c}`}
|
||||||
|
cell={cell}
|
||||||
|
onClick={() => onClick([r, c])}
|
||||||
|
onMouseEnter={() => handleHover(r, c)}
|
||||||
|
/>
|
||||||
|
)),
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default GridControl;
|
||||||
|
|
||||||
|
GridControl.propTypes = {
|
||||||
|
gameAssoc: string,
|
||||||
|
onClick: func.isRequired,
|
||||||
|
resign: func.isRequired,
|
||||||
|
};
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
/**
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { memo, useMemo } from 'react';
|
||||||
|
import { IMAGES } from '@mine-utils';
|
||||||
|
import { func, shape, bool, number, string } from 'prop-types';
|
||||||
|
|
||||||
|
const bombSrc = area => {
|
||||||
|
if (null === area) return null;
|
||||||
|
const vert = ['left', 'center', 'right'][area[0]] ?? null;
|
||||||
|
const hor = ['top', 'middle', 'bottom'][area[1]] ?? null;
|
||||||
|
if (null === vert || null === hor) return IMAGES.bombEmpty;
|
||||||
|
return IMAGES.bombPos(hor, vert);
|
||||||
|
};
|
||||||
|
|
||||||
|
const GridField = memo(function GridField({ cell, onClick, onMouseEnter }) {
|
||||||
|
const { currentImage, currentObj, active, lastClickedRed, lastClickedBlue, bombTargetArea } = cell;
|
||||||
|
|
||||||
|
const fieldClass = 'field'
|
||||||
|
+ (active ? ' active' : '')
|
||||||
|
+ (active && 'm' === currentObj ? ' mine' : '')
|
||||||
|
+ ' color-' + currentObj;
|
||||||
|
const bombSourceString = useMemo(() => bombSrc(bombTargetArea), [bombTargetArea]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="field-wrapper"
|
||||||
|
onClick={onClick}
|
||||||
|
onMouseEnter={onMouseEnter}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
className="field-target"
|
||||||
|
src={IMAGES.target}
|
||||||
|
alt="Field of target"
|
||||||
|
/>
|
||||||
|
{bombSourceString && (
|
||||||
|
<img
|
||||||
|
className="field-bomb-target"
|
||||||
|
src={bombSourceString}
|
||||||
|
alt="Field of bomb target"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{(lastClickedRed || lastClickedBlue) && (
|
||||||
|
<img
|
||||||
|
className={`field-${lastClickedRed ? 'red' : 'blue'}-last last-clicked`}
|
||||||
|
src={IMAGES.last(lastClickedRed ? 'red' : 'blue')}
|
||||||
|
alt="Last clicked area"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<div className={fieldClass}>
|
||||||
|
<div
|
||||||
|
style={{ background: "url('/images/bg-corner-outbg.png') no-repeat top left / 100% 100%" }}
|
||||||
|
className="field-corner"
|
||||||
|
>
|
||||||
|
{isNaN(currentImage) && (
|
||||||
|
<div className="flag-mine">
|
||||||
|
<img src={currentImage} alt="" />
|
||||||
|
<div className="flag-mine-base" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!isNaN(currentImage) && 0 !== currentImage && (
|
||||||
|
<div className="flag-number">
|
||||||
|
{currentImage}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default GridField;
|
||||||
|
|
||||||
|
GridField.propTypes = {
|
||||||
|
cell: shape({
|
||||||
|
currentImage: string,
|
||||||
|
currentObj: string,
|
||||||
|
active: bool,
|
||||||
|
lastClickedRed: bool,
|
||||||
|
lastClickedBlue: bool,
|
||||||
|
bombTargetArea: number,
|
||||||
|
}).isRequired,
|
||||||
|
onClick: func.isRequired,
|
||||||
|
onMouseEnter: func.isRequired,
|
||||||
|
};
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
/**
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export { GameBoard } from './GameBoard';
|
||||||
|
export { default as OnlinePlayersDialog } from './OnlinePlayersDialog';
|
||||||
|
export { default as WaitingOverlayContent } from './WaitingOverlayContent';
|
||||||
|
export { default as ChallengeCountdown } from './ChallengeCountdown';
|
||||||
|
export { default as GameTimer } from './GameTimer';
|
||||||
|
export { default as GridControl } from './grid/GridControl';
|
||||||
|
export { default as GridField } from './grid/GridField';
|
||||||
|
export { default as User } from './user/User';
|
||||||
|
export { default as UserControl } from './user/UserControl';
|
||||||
|
export { default as BonusBox } from './BonusBox';
|
||||||
|
export { default as BonusStatsDialog } from './BonusStatsDialog';
|
||||||
|
export { Avatar } from './timer/Avatar';
|
||||||
|
export { PlayerColumn } from './profile/PlayerColumn';
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
/**
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { object, string } from 'prop-types';
|
||||||
|
import { BONUS_LABELS } from '@mine-utils';
|
||||||
|
|
||||||
|
const formatPlayerName = name => {
|
||||||
|
if (name && name.startsWith('anon_')) {
|
||||||
|
return 'Anonymous';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name && 10 < name.length) {
|
||||||
|
return name.substring(0, 7) + '...';
|
||||||
|
}
|
||||||
|
|
||||||
|
return name || 'Unknown';
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PlayerColumn = ({ color, player }) => (
|
||||||
|
<div className={`bsd-column bsd-column--${color}`}>
|
||||||
|
<div className="bsd-column-header">
|
||||||
|
<span className="bsd-column-name">{formatPlayerName(player.name)}</span>
|
||||||
|
<span className="bsd-column-total">
|
||||||
|
<i className="fa fa-star" />
|
||||||
|
{player.bonusPoints}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<ul className="bsd-stats">
|
||||||
|
{Object.entries(BONUS_LABELS).map(([key, { label, desc }]) => (
|
||||||
|
<li key={key} className="bsd-stat">
|
||||||
|
<div className="bsd-stat-text">
|
||||||
|
<span className="bsd-stat-label">{label}</span>
|
||||||
|
<span className="bsd-stat-desc">{desc}</span>
|
||||||
|
</div>
|
||||||
|
<span className="bsd-stat-value">{player.bonusStats?.[key] ?? 0}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
PlayerColumn.propTypes = {
|
||||||
|
color: string.isRequired,
|
||||||
|
player: object.isRequired,
|
||||||
|
};
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
/**
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { object } from 'prop-types';
|
||||||
|
|
||||||
|
export const Avatar = ({ player }) => {
|
||||||
|
if (!player.registered) return '';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="timer-avatar">
|
||||||
|
{player.avatar
|
||||||
|
? <img src={player.avatar} alt={player.name} className="timer-avatar__img" />
|
||||||
|
: <span className="timer-avatar__initials">{player.name.slice(0, 2).toUpperCase()}</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
Avatar.propTypes = {
|
||||||
|
player: object.isRequired,
|
||||||
|
};
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
/**
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { memo } from 'react';
|
||||||
|
import { IMAGES } from '@mine-utils';
|
||||||
|
import { bool, func, number, string } from 'prop-types';
|
||||||
|
|
||||||
|
const User = memo(function User(
|
||||||
|
{
|
||||||
|
color,
|
||||||
|
webPlayer,
|
||||||
|
name,
|
||||||
|
desc,
|
||||||
|
active,
|
||||||
|
mines,
|
||||||
|
haveBomb,
|
||||||
|
enabledBomb,
|
||||||
|
onClickBombSelector,
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
const buzzClass = 'bomb-container'
|
||||||
|
+ (active && color === webPlayer && haveBomb && enabledBomb ? ' buzz' : '');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`user-container user-${color}`}>
|
||||||
|
<div className="user-header">
|
||||||
|
<div className="user-color">{color}</div>
|
||||||
|
{active && <img src={IMAGES.cursor(color)} alt="" className="user-cursor" />}
|
||||||
|
<img src={IMAGES.figure(color)} alt="" />
|
||||||
|
</div>
|
||||||
|
<div className="user-name"> {name} </div>
|
||||||
|
<div className="user-caret"><i className="fa fa-caret-down" /></div>
|
||||||
|
<div className="user-desc"> {desc} </div>
|
||||||
|
<div className="user-control">
|
||||||
|
<img src={IMAGES.flag(color)} alt="" />
|
||||||
|
<div className="user-control-mines">{mines}</div>
|
||||||
|
<div className={buzzClass} onClick={onClickBombSelector}>
|
||||||
|
<div className="bomb">
|
||||||
|
{haveBomb && <img src={enabledBomb && active ? IMAGES.bomb : IMAGES.bombDisabled} alt="" />}
|
||||||
|
{!haveBomb && <img src={IMAGES.bombExploded} alt="" />}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="clear" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default User;
|
||||||
|
|
||||||
|
User.propTypes = {
|
||||||
|
color: string.isRequired,
|
||||||
|
webPlayer: string,
|
||||||
|
name: string,
|
||||||
|
desc: string,
|
||||||
|
active: bool,
|
||||||
|
mines: number,
|
||||||
|
haveBomb: bool,
|
||||||
|
enabledBomb: bool,
|
||||||
|
onClickBombSelector: func.isRequired,
|
||||||
|
};
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
/**
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { Fragment, useState } from 'react';
|
||||||
|
import { useGame } from '@mine-contexts';
|
||||||
|
import User from './User';
|
||||||
|
import BonusStatsDialog from '../BonusStatsDialog';
|
||||||
|
import { func } from 'prop-types';
|
||||||
|
|
||||||
|
const UserControl = ({ resign }) => {
|
||||||
|
const { webPlayer, activePlayer, foundMines, red, blue, onBombToggle } = useGame();
|
||||||
|
const [bonusDialogOpen, setBonusDialogOpen] = useState(false);
|
||||||
|
const activeColor = activePlayer ? 'blue' : 'red';
|
||||||
|
const resignClass = 'resign' + (activeColor !== webPlayer ? ' disabled' : '');
|
||||||
|
const minesClass = 'active-mines' + (foundMines ? ' found-mine' : '');
|
||||||
|
const remainingMines = 51 - red.mines - blue.mines;
|
||||||
|
|
||||||
|
const handleBombClick = (color, player) => {
|
||||||
|
const p = 'red' === color ? red : blue;
|
||||||
|
if (p.haveBomb && p.enabledBomb && activePlayer === player) {
|
||||||
|
onBombToggle();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBonusClick = () => {
|
||||||
|
setBonusDialogOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<div className="users">
|
||||||
|
<User
|
||||||
|
color="blue" webPlayer={webPlayer} {...blue}
|
||||||
|
onClickBombSelector={() => handleBombClick('blue', 1)}
|
||||||
|
onBonusClick={handleBonusClick}
|
||||||
|
/>
|
||||||
|
<div className="active-mines-container">
|
||||||
|
<i className="fa fa-star" />
|
||||||
|
<div className={minesClass}>
|
||||||
|
<div className="active-mines-nbr">{remainingMines}</div>
|
||||||
|
<div className="active-mines-shine" />
|
||||||
|
</div>
|
||||||
|
<i className="fa fa-star" />
|
||||||
|
</div>
|
||||||
|
<div className="clear" />
|
||||||
|
<User
|
||||||
|
color="red" webPlayer={webPlayer} {...red}
|
||||||
|
onClickBombSelector={() => handleBombClick('red', 0)}
|
||||||
|
onBonusClick={handleBonusClick}
|
||||||
|
/>
|
||||||
|
<button className={resignClass} onClick={resign}>
|
||||||
|
<div className="resign-shine" />
|
||||||
|
Resign
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<BonusStatsDialog
|
||||||
|
open={bonusDialogOpen}
|
||||||
|
onClose={() => setBonusDialogOpen(false)}
|
||||||
|
red={red}
|
||||||
|
blue={blue}
|
||||||
|
/>
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default UserControl;
|
||||||
|
|
||||||
|
UserControl.propTypes = {
|
||||||
|
resign: func.isRequired,
|
||||||
|
};
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
/**
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { createContext, useContext } from 'react';
|
||||||
|
|
||||||
|
const GameContext = createContext(null);
|
||||||
|
|
||||||
|
export const useGame = () => useContext(GameContext);
|
||||||
|
|
||||||
|
export default GameContext;
|
||||||
@@ -0,0 +1,280 @@
|
|||||||
|
/**
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { useRef } from 'react';
|
||||||
|
import { Howl } from 'howler';
|
||||||
|
import GameContext from './GameContext';
|
||||||
|
import { useGameRefs, useGameState } from '@mine-hooks';
|
||||||
|
import { DESC, IMAGES, patchCells } from '@mine-utils';
|
||||||
|
|
||||||
|
export const GameProvider = ({ children }) => {
|
||||||
|
const {
|
||||||
|
webPlayerRef,
|
||||||
|
activePlayerRef,
|
||||||
|
bombSelectedRef,
|
||||||
|
connectionLostRef,
|
||||||
|
redRef,
|
||||||
|
blueRef,
|
||||||
|
lastClickedRef,
|
||||||
|
endRef,
|
||||||
|
} = useGameRefs();
|
||||||
|
|
||||||
|
const {
|
||||||
|
webPlayer, setWebPlayer,
|
||||||
|
activePlayer, setActivePlayer,
|
||||||
|
overlay, setOverlay,
|
||||||
|
overlayTitle, setOverlayTitle,
|
||||||
|
overlaySubTitle, setOverlaySubTitle,
|
||||||
|
mines, setMines,
|
||||||
|
bombSelected, setBombSelected,
|
||||||
|
foundMines, setFoundMines,
|
||||||
|
red, setRed,
|
||||||
|
blue, setBlue,
|
||||||
|
cells, setCells,
|
||||||
|
gridReady, setGridReady,
|
||||||
|
connectionLost, setConnectionLost,
|
||||||
|
} = useGameState();
|
||||||
|
|
||||||
|
const [gameUuid, setGameUuid] = React.useState(null);
|
||||||
|
|
||||||
|
const sounds = useRef({
|
||||||
|
click: new Howl({ src: ['/sound/click.mp3'] }),
|
||||||
|
bomb: new Howl({ src: ['/sound/bomb.mp3'] }),
|
||||||
|
mine: new Howl({ src: ['/sound/mine.mp3'] }),
|
||||||
|
warning: new Howl({ src: ['/sound/warning.mp3'] }),
|
||||||
|
won: new Howl({ src: ['/sound/won.mp3'] }),
|
||||||
|
starting: new Howl({ src: ['/sound/starting.mp3'] }),
|
||||||
|
});
|
||||||
|
|
||||||
|
/** Sync helpers (keep ref + state in lockstep) */
|
||||||
|
|
||||||
|
const syncWebPlayer = v => {
|
||||||
|
webPlayerRef.current = v;
|
||||||
|
setWebPlayer(v);
|
||||||
|
};
|
||||||
|
const syncActivePlayer = v => {
|
||||||
|
activePlayerRef.current = v;
|
||||||
|
setActivePlayer(v);
|
||||||
|
};
|
||||||
|
const syncBombSelected = v => {
|
||||||
|
bombSelectedRef.current = v;
|
||||||
|
setBombSelected(v);
|
||||||
|
};
|
||||||
|
const syncConnLost = v => {
|
||||||
|
connectionLostRef.current = v;
|
||||||
|
setConnectionLost(v);
|
||||||
|
};
|
||||||
|
const syncRed = fn => {
|
||||||
|
const n = fn(redRef.current);
|
||||||
|
redRef.current = n;
|
||||||
|
setRed(n);
|
||||||
|
};
|
||||||
|
const syncBlue = fn => {
|
||||||
|
const n = fn(blueRef.current);
|
||||||
|
blueRef.current = n;
|
||||||
|
setBlue(n);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Overlay */
|
||||||
|
|
||||||
|
const showOverlay = (title, sub) => {
|
||||||
|
setOverlay(true);
|
||||||
|
setOverlayTitle(title);
|
||||||
|
setOverlaySubTitle(sub);
|
||||||
|
};
|
||||||
|
|
||||||
|
const hideOverlay = () => setOverlay(false);
|
||||||
|
|
||||||
|
/** Cell helpers */
|
||||||
|
|
||||||
|
const applyRevealedCell = (cell, player, isMainCell = false) => {
|
||||||
|
const { row, col, value } = cell;
|
||||||
|
setCells(prev => {
|
||||||
|
if (prev[row][col].active) return prev;
|
||||||
|
const patch = 'm' === value
|
||||||
|
? { currentImage: IMAGES.flag(player), currentObj: 'm', active: true }
|
||||||
|
: { currentImage: value, currentObj: value, active: true };
|
||||||
|
if (isMainCell) {
|
||||||
|
patch.lastClickedRed = 'red' === player;
|
||||||
|
patch.lastClickedBlue = 'blue' === player;
|
||||||
|
}
|
||||||
|
return patchCells(prev, [{ row, col, ...patch }]);
|
||||||
|
});
|
||||||
|
if (isMainCell) lastClickedRef.current = { ...lastClickedRef.current, [player]: [row, col] };
|
||||||
|
};
|
||||||
|
|
||||||
|
const showLeftMines = (leftMines = []) => {
|
||||||
|
if (!leftMines.length) return;
|
||||||
|
setCells(prev => {
|
||||||
|
const patches = leftMines
|
||||||
|
.filter(({ row, col }) => !prev[row][col].active)
|
||||||
|
.map(({ row, col }) => ({ row, col, currentImage: IMAGES.leftMine }));
|
||||||
|
return patches.length ? patchCells(prev, patches) : prev;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Game logic */
|
||||||
|
|
||||||
|
const changePlayer = () => {
|
||||||
|
const wasColor = activePlayerRef.current ? 'blue' : 'red';
|
||||||
|
const nextColor = activePlayerRef.current ? 'red' : 'blue';
|
||||||
|
const nextVal = activePlayerRef.current ? 0 : 1;
|
||||||
|
const desc = wasColor === webPlayerRef.current ? DESC.buddy : DESC.you;
|
||||||
|
|
||||||
|
syncActivePlayer(nextVal);
|
||||||
|
syncRed(p => ({ ...p, active: 'red' === nextColor, desc: 'red' === nextColor ? desc : '' }));
|
||||||
|
syncBlue(p => ({ ...p, active: 'blue' === nextColor, desc: 'blue' === nextColor ? desc : '' }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const applyStep = stepData => {
|
||||||
|
const {
|
||||||
|
player,
|
||||||
|
bomb: isBomb,
|
||||||
|
minesFound = 0,
|
||||||
|
revealedCells = [],
|
||||||
|
redPoints: rp,
|
||||||
|
bluePoints: bp,
|
||||||
|
redBonusPoints = 0,
|
||||||
|
blueBonusPoints = 0,
|
||||||
|
redBonusStats = {},
|
||||||
|
blueBonusStats = {},
|
||||||
|
} = stepData;
|
||||||
|
|
||||||
|
if (isBomb) {
|
||||||
|
sounds.current.bomb.play();
|
||||||
|
} else if (0 < minesFound) {
|
||||||
|
const cur = ('red' === player ? redRef : blueRef).current.mines;
|
||||||
|
sounds.current[20 < cur + minesFound ? 'warning' : 'mine'].play();
|
||||||
|
} else {
|
||||||
|
sounds.current.click.play();
|
||||||
|
}
|
||||||
|
|
||||||
|
const lc = lastClickedRef.current[player];
|
||||||
|
setCells(prev => {
|
||||||
|
let next = prev;
|
||||||
|
if (lc) next = patchCells(next, [{ row: lc[0], col: lc[1], lastClickedRed: false, lastClickedBlue: false }]);
|
||||||
|
|
||||||
|
revealedCells.forEach(({ row, col, value }, idx) => {
|
||||||
|
if (next[row][col].active) return;
|
||||||
|
const patch = 'm' === value
|
||||||
|
? { currentImage: IMAGES.flag(player), currentObj: 'm', active: true }
|
||||||
|
: { currentImage: value, currentObj: value, active: true };
|
||||||
|
if (0 === idx) {
|
||||||
|
patch.lastClickedRed = 'red' === player;
|
||||||
|
patch.lastClickedBlue = 'blue' === player;
|
||||||
|
}
|
||||||
|
next = patchCells(next, [{ row, col, ...patch }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isBomb) next = next.map(r => r.map(c => null !== c.bombTargetArea ? { ...c, bombTargetArea: null } : c));
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (0 < revealedCells.length) {
|
||||||
|
lastClickedRef.current = { ...lastClickedRef.current, [player]: [revealedCells[0].row, revealedCells[0].col] };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (0 < minesFound) {
|
||||||
|
setMines(51 - rp - bp);
|
||||||
|
setFoundMines(true);
|
||||||
|
setTimeout(() => setFoundMines(false), 500);
|
||||||
|
syncRed(p => ({ ...p, mines: 'red' === player ? rp : p.mines }));
|
||||||
|
syncBlue(p => ({ ...p, mines: 'blue' === player ? bp : p.mines }));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Update bonus points and stats */
|
||||||
|
syncRed(p => ({
|
||||||
|
...p,
|
||||||
|
bonusPoints: 'red' === player ? redBonusPoints : p.bonusPoints,
|
||||||
|
bonusStats: 'red' === player ? redBonusStats : p.bonusStats,
|
||||||
|
}));
|
||||||
|
syncBlue(p => ({
|
||||||
|
...p,
|
||||||
|
bonusPoints: 'blue' === player ? blueBonusPoints : p.bonusPoints,
|
||||||
|
bonusStats: 'blue' === player ? blueBonusStats : p.bonusStats,
|
||||||
|
}));
|
||||||
|
|
||||||
|
syncRed(p => ({ ...p, enabledBomb: rp <= bp }));
|
||||||
|
syncBlue(p => ({ ...p, enabledBomb: bp <= rp }));
|
||||||
|
|
||||||
|
if (isBomb || 0 === minesFound) changePlayer();
|
||||||
|
|
||||||
|
if (isBomb) {
|
||||||
|
syncBombSelected(false);
|
||||||
|
syncRed(p => 'red' === player ? { ...p, haveBomb: false } : p);
|
||||||
|
syncBlue(p => 'blue' === player ? { ...p, haveBomb: false } : p);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const makeGameEndIfItEnds = (bluePoints, redPoints, resign = false, leftMines = []) => {
|
||||||
|
const redWins = 25 < redPoints;
|
||||||
|
const blueWins = 25 < bluePoints;
|
||||||
|
|
||||||
|
if (redWins || blueWins || resign) {
|
||||||
|
sounds.current.won.play();
|
||||||
|
|
||||||
|
if (!resign) {
|
||||||
|
endRef.current = true;
|
||||||
|
showOverlay((redWins ? 'Red' : 'Blue') + ' wins the game!', null);
|
||||||
|
}
|
||||||
|
|
||||||
|
showLeftMines(leftMines);
|
||||||
|
syncActivePlayer(false);
|
||||||
|
syncRed(p => ({ ...p, desc: '' }));
|
||||||
|
syncBlue(p => ({ ...p, desc: '' }));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const resignProcess = (color, uuid = null) => {
|
||||||
|
const wp = webPlayerRef.current;
|
||||||
|
if (uuid) {
|
||||||
|
setGameUuid(uuid);
|
||||||
|
}
|
||||||
|
showOverlay(
|
||||||
|
color === wp ? 'You have been give up' : 'Your opponent has been resigned',
|
||||||
|
color === wp ? 'You LOSE!' : 'You WIN!',
|
||||||
|
);
|
||||||
|
endRef.current = true;
|
||||||
|
makeGameEndIfItEnds(0, 0, true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onBombToggle = () => {
|
||||||
|
const next = !bombSelectedRef.current;
|
||||||
|
syncBombSelected(next);
|
||||||
|
if (!next) {
|
||||||
|
setCells(prev => prev.map(r => r.map(c => null !== c.bombTargetArea ? { ...c, bombTargetArea: null } : c)));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<GameContext.Provider
|
||||||
|
value={{
|
||||||
|
/** State (for rendering) */
|
||||||
|
webPlayer, activePlayer, overlay, overlayTitle, overlaySubTitle,
|
||||||
|
mines, bombSelected, foundMines, red, blue, cells, gridReady, connectionLost, gameUuid,
|
||||||
|
/** Setters needed by useServerComm */
|
||||||
|
setCells, setGridReady, setGameUuid,
|
||||||
|
/** Refs (needed by useServerComm for async-safe reads) */
|
||||||
|
webPlayerRef, activePlayerRef, bombSelectedRef, connectionLostRef, lastClickedRef, endRef,
|
||||||
|
/** Sync helpers */
|
||||||
|
syncWebPlayer, syncActivePlayer, syncBombSelected, syncConnLost, syncRed, syncBlue,
|
||||||
|
/** Game logic called by useServerComm */
|
||||||
|
showOverlay, hideOverlay,
|
||||||
|
applyRevealedCell, applyStep,
|
||||||
|
makeGameEndIfItEnds, resignProcess,
|
||||||
|
/** UI action */
|
||||||
|
onBombToggle,
|
||||||
|
/** Sounds */
|
||||||
|
sounds,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</GameContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user