Compare commits
148 Commits
0.4.0
...
v2026.2.8-
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 | |||
| 9ef0711acc | |||
| 5afc237ffb | |||
| 13749186fb |
13
.dockerignore
Normal file
13
.dockerignore
Normal file
@@ -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
.editorconfig
Normal file
884
.editorconfig
Normal file
@@ -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
|
||||||
59
.env.dist
Normal file
59
.env.dist
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
# 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
|
||||||
|
# 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 15432 — 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:15432/POSTGRES_DB?serverVersion=18&charset=utf8
|
||||||
|
###< doctrine/doctrine-bundle ###
|
||||||
|
|
||||||
|
###> google/recaptcha ###
|
||||||
|
RECAPTCHA_SITE_KEY=changethis
|
||||||
|
RECAPTCHA_SECRET_KEY=changethis
|
||||||
|
###< google/recaptcha ###
|
||||||
|
|
||||||
|
###> minio/minio ###
|
||||||
|
MINIO_ROOT_USER=changethis
|
||||||
|
MINIO_ROOT_PASSWORD=changethis
|
||||||
|
MINIO_ENDPOINT=http://localhost:9000
|
||||||
|
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.local/.well-known/mercure
|
||||||
|
# The public URL of the Mercure hub, used by the browser to connect
|
||||||
|
MERCURE_PUBLIC_URL=https://mine.local/.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.local
|
||||||
|
WEBAUTHN_ORIGIN=https://mine.local
|
||||||
|
###< web-auth/webauthn-framework ###
|
||||||
3
.env.test
Normal file
3
.env.test
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# define your env variables for the test env here
|
||||||
|
KERNEL_CLASS='App\Kernel'
|
||||||
|
APP_SECRET='$ecretf0rt3st'
|
||||||
32
.gitchangelog.rc
Normal file
32
.gitchangelog.rc
Normal file
@@ -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 = []
|
||||||
142
.gitea/workflows/ci.yml-bak
Normal file
142
.gitea/workflows/ci.yml-bak
Normal file
@@ -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
|
||||||
39
.gitea/workflows/deploy.yml
Normal file
39
.gitea/workflows/deploy.yml
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
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: Start services
|
||||||
|
run: |
|
||||||
|
cd "${{ vars.PROD_APP_DIR }}"
|
||||||
|
docker compose up -d
|
||||||
126
.gitea/workflows/deploy.yml-bak
Normal file
126
.gitea/workflows/deploy.yml-bak
Normal file
@@ -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
|
||||||
59
.gitignore
vendored
59
.gitignore
vendored
@@ -1,40 +1,21 @@
|
|||||||
/app/config/parameters.yml
|
###> system7 - jotunheimr ###
|
||||||
/build/
|
compose.override.yaml
|
||||||
/phpunit.xml
|
+bak/
|
||||||
/var/*
|
.idea/
|
||||||
!/var/cache
|
node_modules/
|
||||||
/var/cache/*
|
|
||||||
!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
|
||||||
npm-debug.log
|
###< system7 - jotunheimr ###
|
||||||
|
|
||||||
|
###> symfony/framework-bundle ###
|
||||||
|
/.env
|
||||||
|
/public/bundles/
|
||||||
|
/var/
|
||||||
|
/vendor/
|
||||||
|
###< symfony/framework-bundle ###
|
||||||
|
|
||||||
|
###> phpunit/phpunit ###
|
||||||
|
/phpunit.xml
|
||||||
|
/.phpunit.cache/
|
||||||
|
###< phpunit/phpunit ###
|
||||||
|
|||||||
464
AGENTS.md
Normal file
464
AGENTS.md
Normal file
@@ -0,0 +1,464 @@
|
|||||||
|
# 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
|
||||||
|
│ └── contact.jsx # Contact form 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! 🚀**
|
||||||
545
CHANGELOG.md
Normal file
545
CHANGELOG.md
Normal file
@@ -0,0 +1,545 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
|
||||||
|
## v2026.2.8-2 (2026-04-21)
|
||||||
|
|
||||||
|
### Changes
|
||||||
|
|
||||||
|
* Increase the 2 MB avatar maximum file size to 10 MB #10. [Lang]
|
||||||
|
|
||||||
|
|
||||||
|
## v2026.2.8-1 (2026-04-21)
|
||||||
|
|
||||||
|
### Changes
|
||||||
|
|
||||||
|
* Upgrade front-end & back-end deps to the latest available version #10. [Lang]
|
||||||
|
|
||||||
|
|
||||||
|
## v2026.2.8-0 (2026-04-21)
|
||||||
|
|
||||||
|
### New
|
||||||
|
|
||||||
|
* Add CI/CD improvements - add new CI workflow - & improve the deployment w/ tests #10. [Lang]
|
||||||
|
|
||||||
|
* Add test cases to back-end w/ real database connection in it #10. [Lang]
|
||||||
|
|
||||||
|
### Changes
|
||||||
|
|
||||||
|
* The original CI/CD workflow is restored - the work with tests is postponed #10. [Lang]
|
||||||
|
|
||||||
|
* Add sound when the game start #10. [Lang]
|
||||||
|
|
||||||
|
* Create AGENTS.md file for future maintenance #9. [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]
|
||||||
|
|
||||||
|
|
||||||
|
## 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]
|
||||||
|
|
||||||
|
|
||||||
29
Caddyfile
Normal file
29
Caddyfile
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
{$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_url {$MERCURE_TRANSPORT_URL:bolt:///data/mercure.db}
|
||||||
|
publisher_jwt {$MERCURE_JWT_SECRET} HS256
|
||||||
|
subscriber_jwt {$MERCURE_JWT_SECRET} HS256
|
||||||
|
anonymous
|
||||||
|
subscriptions
|
||||||
|
}
|
||||||
|
|
||||||
|
php_server
|
||||||
|
}
|
||||||
53
Dockerfile
Normal file
53
Dockerfile
Normal file
@@ -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"]
|
||||||
124
LICENSE
Normal file
124
LICENSE
Normal file
@@ -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)
|
||||||
|
|
||||||
|
|
||||||
105
Makefile
Normal file
105
Makefile
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
.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 without building (uses existing images)"
|
||||||
|
@echo " make start-build - Start services and build images if needed"
|
||||||
|
@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
|
||||||
|
|
||||||
|
start-build:
|
||||||
|
composer i
|
||||||
|
bun run build
|
||||||
|
docker compose up -d --build
|
||||||
|
|
||||||
|
stop:
|
||||||
|
docker compose stop
|
||||||
|
|
||||||
|
build:
|
||||||
|
docker compose build
|
||||||
|
|
||||||
|
down:
|
||||||
|
docker compose 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 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
|
||||||
382
README.md
382
README.md
@@ -1,35 +1,379 @@
|
|||||||
This is a Symfony 3 project w/ React JS in standalone mode and w/ WebSocket.
|
# MineSeeker
|
||||||
|
|
||||||
0.) Must installed modules w/ npm are in package.json + to global:
|
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.
|
||||||
|
|
||||||
$ npm install webpack -g
|
Created by [SplendidBear](https://www.splendidbear.org).
|
||||||
|
|
||||||
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
|
## Features
|
||||||
|
|
||||||
|
- **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 MinIO 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
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
1.) Backend WebSocket server start as daemon - GeniusesOfSymfony/WebSocketBundle
|
## Tech stack
|
||||||
|
|
||||||
$ nohup bin/console gos:websocket:server &
|
| 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 | MinIO (S3-compatible) |
|
||||||
|
| Image processing | LiipImagine + Flysystem |
|
||||||
|
| Server | FrankenPHP (Caddy + PHP in one binary) |
|
||||||
|
| Auth | Symfony Security, Scheb 2FA, web-auth/webauthn-framework |
|
||||||
|
|
||||||
2.) React JS WebPack watch generator w/ babel presets: es2015, react
|
---
|
||||||
|
|
||||||
$ webpack -p --config=webpack-prod.config.js
|
## Requirements
|
||||||
|
|
||||||
PROD
|
### 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)
|
||||||
|
- MinIO (listening on port 9000)
|
||||||
|
|
||||||
$ webpack --progress --colors --watch -d
|
### Docker
|
||||||
|
- Docker Engine 24+
|
||||||
|
- Docker Compose v2
|
||||||
|
- Bun (for building frontend assets before `make start-build`)
|
||||||
|
- Composer (for `make start-build`)
|
||||||
|
|
||||||
DEV
|
---
|
||||||
|
|
||||||
-d --> Debugger; If you write this line somewhere: debugger;
|
## Installation
|
||||||
The browser will stop the code here!!!
|
|
||||||
|
|
||||||
3.) Connect to Prod
|
### 1. Clone the repository
|
||||||
|
|
||||||
ssh xxsvci@laszlolang.com -i ~/.ssh/id_rsa_laszlolang
|
```bash
|
||||||
|
git clone https://github.com/splendidbear/mineseeker.git
|
||||||
|
cd mineseeker
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Configure environment
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp .env.dist .env
|
||||||
|
```
|
||||||
|
|
||||||
|
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` | MinIO admin credentials |
|
||||||
|
| `MINIO_ENDPOINT` | `http://localhost:9000` (bare-metal) |
|
||||||
|
| `MINIO_PUBLIC_URL` | Public URL browsers use to reach MinIO |
|
||||||
|
| `RECAPTCHA_SITE_KEY/SECRET_KEY` | Google reCAPTCHA v3 keys for your domain |
|
||||||
|
| `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 image, and starts all services (`app`, `db`, `mail`, `minio`, `minio_init`).
|
||||||
|
|
||||||
|
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 MinIO and MailHog, then open the URL configured in your Caddy vhost.
|
||||||
|
|
||||||
|
### 5. MinIO bucket setup
|
||||||
|
|
||||||
|
**Docker** — handled automatically on first start by the `minio_init` service. No action needed.
|
||||||
|
|
||||||
|
**Bare-metal** — run once after MinIO is up:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mc alias set local http://localhost:9000 $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://minio:9000
|
||||||
|
MINIO_PUBLIC_URL=https://aws.mineseeker.hu
|
||||||
|
|
||||||
|
MAILER_DSN=smtp://mail:25?verify_peer=0
|
||||||
|
MAIL_DOMAIN=mineseeker.hu
|
||||||
|
|
||||||
|
RECAPTCHA_SITE_KEY="<your reCAPTCHA v3 site key>"
|
||||||
|
RECAPTCHA_SECRET_KEY="<your reCAPTCHA v3 secret key>"
|
||||||
|
|
||||||
|
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 image, starts all services, runs migrations, and initialises the MinIO buckets automatically via `minio_init`.
|
||||||
|
|
||||||
|
#### 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,107 +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
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
# 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: bazmeg
|
|
||||||
mailer_transport: smtp
|
|
||||||
mailer_host: 127.0.0.1
|
|
||||||
mailer_user: null
|
|
||||||
mailer_password: null
|
|
||||||
secret: bbcd5df99fc340558fb3995c198a9b4764db72ba
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
# This file is auto-generated during the composer install
|
|
||||||
parameters:
|
|
||||||
database_host: 127.0.0.1
|
|
||||||
database_port: null
|
|
||||||
# database_name: xxsvci_mineseeker
|
|
||||||
# database_user: xxsvci_mine
|
|
||||||
# database_password: "XTw#8qC$faa*"
|
|
||||||
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%"]
|
|
||||||
10
assets/css/_fontawesome-config.scss
Normal file
10
assets/css/_fontawesome-config.scss
Normal file
@@ -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';
|
||||||
12
assets/css/_fonts-config.scss
Normal file
12
assets/css/_fonts-config.scss
Normal file
@@ -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';
|
||||||
18
assets/css/homepage/_animations.scss
Normal file
18
assets/css/homepage/_animations.scss
Normal file
@@ -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); }
|
||||||
|
}
|
||||||
128
assets/css/homepage/_auth-bar.scss
Normal file
128
assets/css/homepage/_auth-bar.scss
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
474
assets/css/homepage/_auth.scss
Normal file
474
assets/css/homepage/_auth.scss
Normal file
@@ -0,0 +1,474 @@
|
|||||||
|
/*!*
|
||||||
|
* 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: 420px;
|
||||||
|
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-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;
|
||||||
|
}
|
||||||
|
}
|
||||||
200
assets/css/homepage/_battle-dialog.scss
Normal file
200
assets/css/homepage/_battle-dialog.scss
Normal file
@@ -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);
|
||||||
|
}
|
||||||
52
assets/css/homepage/_content.scss
Normal file
52
assets/css/homepage/_content.scss
Normal file
@@ -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;
|
||||||
|
}
|
||||||
84
assets/css/homepage/_cta.scss
Normal file
84
assets/css/homepage/_cta.scss
Normal file
@@ -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;
|
||||||
|
}
|
||||||
72
assets/css/homepage/_donate.scss
Normal file
72
assets/css/homepage/_donate.scss
Normal file
@@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
363
assets/css/homepage/_features.scss
Normal file
363
assets/css/homepage/_features.scss
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
121
assets/css/homepage/_footer.scss
Normal file
121
assets/css/homepage/_footer.scss
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
/*!*
|
||||||
|
* 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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
48
assets/css/homepage/_header.scss
Normal file
48
assets/css/homepage/_header.scss
Normal file
@@ -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;
|
||||||
|
}
|
||||||
58
assets/css/homepage/_hero-compact.scss
Normal file
58
assets/css/homepage/_hero-compact.scss
Normal file
@@ -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;
|
||||||
|
}
|
||||||
99
assets/css/homepage/_hero.scss
Normal file
99
assets/css/homepage/_hero.scss
Normal file
@@ -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;
|
||||||
|
}
|
||||||
1245
assets/css/homepage/_profile.scss
Normal file
1245
assets/css/homepage/_profile.scss
Normal file
File diff suppressed because it is too large
Load Diff
32
assets/css/homepage/_reset.scss
Normal file
32
assets/css/homepage/_reset.scss
Normal file
@@ -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%;
|
||||||
|
}
|
||||||
113
assets/css/homepage/_responsive.scss
Normal file
113
assets/css/homepage/_responsive.scss
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
69
assets/css/homepage/_tech.scss
Normal file
69
assets/css/homepage/_tech.scss
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
33
assets/css/mineseeker/_back-button.scss
Normal file
33
assets/css/mineseeker/_back-button.scss
Normal file
@@ -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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.back-from-game {
|
||||||
|
display: inline-block;
|
||||||
|
position: fixed;
|
||||||
|
top: 20px;
|
||||||
|
left: 20px;
|
||||||
|
|
||||||
|
-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);
|
||||||
|
}
|
||||||
65
assets/css/mineseeker/_base.scss
Normal file
65
assets/css/mineseeker/_base.scss
Normal file
@@ -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;
|
||||||
|
}
|
||||||
234
assets/css/mineseeker/_bomb.scss
Normal file
234
assets/css/mineseeker/_bomb.scss
Normal file
@@ -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;
|
||||||
|
}
|
||||||
250
assets/css/mineseeker/_bonus-box.scss
Normal file
250
assets/css/mineseeker/_bonus-box.scss
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
216
assets/css/mineseeker/_grid.scss
Normal file
216
assets/css/mineseeker/_grid.scss
Normal file
@@ -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%;
|
||||||
|
}
|
||||||
99
assets/css/mineseeker/_mine-counter.scss
Normal file
99
assets/css/mineseeker/_mine-counter.scss
Normal file
@@ -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;
|
||||||
|
}
|
||||||
780
assets/css/mineseeker/_overlay.scss
Normal file
780
assets/css/mineseeker/_overlay.scss
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
50
assets/css/mineseeker/_responsive.scss
Normal file
50
assets/css/mineseeker/_responsive.scss
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
111
assets/css/mineseeker/_timer.scss
Normal file
111
assets/css/mineseeker/_timer.scss
Normal file
@@ -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;
|
||||||
|
}
|
||||||
171
assets/css/mineseeker/_users.scss
Normal file
171
assets/css/mineseeker/_users.scss
Normal file
@@ -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;
|
||||||
|
}
|
||||||
356
assets/css/mineseeker/_waiting-dialog.scss
Normal file
356
assets/css/mineseeker/_waiting-dialog.scss
Normal file
@@ -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); }
|
||||||
|
}
|
||||||
301
assets/css/passkey.scss
Normal file
301
assets/css/passkey.scss
Normal file
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
25
assets/css/style.homepage.scss
Normal file
25
assets/css/style.homepage.scss
Normal file
@@ -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';
|
||||||
403
assets/css/style.layout.scss
Normal file
403
assets/css/style.layout.scss
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
24
assets/css/style.mineseeker.scss
Normal file
24
assets/css/style.mineseeker.scss
Normal file
@@ -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';
|
||||||
19
assets/css/style.scss
Normal file
19
assets/css/style.scss
Normal file
@@ -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);
|
||||||
|
}
|
||||||
BIN
assets/fonts/Carlito-Bold.ttf
Normal file
BIN
assets/fonts/Carlito-Bold.ttf
Normal file
Binary file not shown.
170
assets/images/waiting-dialog-design.svg
Normal file
170
assets/images/waiting-dialog-design.svg
Normal file
@@ -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 |
22
assets/js/app.jsx
Normal file
22
assets/js/app.jsx
Normal file
@@ -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 || ''}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
76
assets/js/components/AvatarUpload.jsx
Normal file
76
assets/js/components/AvatarUpload.jsx
Normal file
@@ -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,
|
||||||
|
};
|
||||||
223
assets/js/components/BattleDialog.jsx
Normal file
223
assets/js/components/BattleDialog.jsx
Normal file
@@ -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)',
|
||||||
|
},
|
||||||
|
});
|
||||||
89
assets/js/components/ContactForm.jsx
Normal file
89
assets/js/components/ContactForm.jsx
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
/**
|
||||||
|
* 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 { useEffect, useRef } from 'react';
|
||||||
|
import { string } from 'prop-types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ContactForm Component
|
||||||
|
*
|
||||||
|
* Handles reCAPTCHA v3 integration for the contact form.
|
||||||
|
* Intercepts form submission, executes reCAPTCHA, and submits the form with the token.
|
||||||
|
*
|
||||||
|
* @param {string} siteKey - Google reCAPTCHA site key
|
||||||
|
* @param {string} recaptchaFieldId - ID of the hidden recaptcha input field
|
||||||
|
*/
|
||||||
|
const ContactForm = ({ siteKey, recaptchaFieldId }) => {
|
||||||
|
const formRef = useRef(null);
|
||||||
|
const isSubmittingRef = useRef(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const form = document.querySelector('.auth-form');
|
||||||
|
|
||||||
|
if (!form) {
|
||||||
|
console.warn('ContactForm: No .auth-form found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
formRef.current = form;
|
||||||
|
|
||||||
|
const handleSubmit = e => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (isSubmittingRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isSubmittingRef.current = true;
|
||||||
|
|
||||||
|
if ('undefined' !== typeof grecaptcha) {
|
||||||
|
grecaptcha.ready(() => {
|
||||||
|
grecaptcha
|
||||||
|
.execute(siteKey, { action: 'contact' })
|
||||||
|
.then(token => {
|
||||||
|
const recaptchaField = document.getElementById(recaptchaFieldId);
|
||||||
|
|
||||||
|
if (recaptchaField) {
|
||||||
|
recaptchaField.value = token;
|
||||||
|
} else {
|
||||||
|
console.error(`ContactForm: Recaptcha field with ID "${recaptchaFieldId}" not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
isSubmittingRef.current = false;
|
||||||
|
form.submit();
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('ContactForm: reCAPTCHA execution failed', error);
|
||||||
|
isSubmittingRef.current = false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.error('ContactForm: grecaptcha is not loaded');
|
||||||
|
isSubmittingRef.current = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
form.addEventListener('submit', handleSubmit);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (formRef.current) {
|
||||||
|
formRef.current.removeEventListener('submit', handleSubmit);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [siteKey, recaptchaFieldId]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
ContactForm.propTypes = {
|
||||||
|
siteKey: string.isRequired,
|
||||||
|
recaptchaFieldId: string.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ContactForm;
|
||||||
118
assets/js/components/PasskeyLogin.jsx
Normal file
118
assets/js/components/PasskeyLogin.jsx
Normal file
@@ -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,
|
||||||
|
};
|
||||||
428
assets/js/components/PasskeyManager.jsx
Normal file
428
assets/js/components/PasskeyManager.jsx
Normal file
@@ -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,
|
||||||
|
};
|
||||||
165
assets/js/components/ProfileCharts.jsx
Normal file
165
assets/js/components/ProfileCharts.jsx
Normal file
@@ -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,
|
||||||
|
};
|
||||||
54
assets/js/components/battle-dialog/Avatar.jsx
Normal file
54
assets/js/components/battle-dialog/Avatar.jsx
Normal file
@@ -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,
|
||||||
|
};
|
||||||
122
assets/js/components/battle-dialog/BonusPoints.jsx
Normal file
122
assets/js/components/battle-dialog/BonusPoints.jsx
Normal file
@@ -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,
|
||||||
|
};
|
||||||
31
assets/js/components/battle-dialog/StatRow.jsx
Normal file
31
assets/js/components/battle-dialog/StatRow.jsx
Normal file
@@ -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,
|
||||||
|
};
|
||||||
18
assets/js/components/index.js
Normal file
18
assets/js/components/index.js
Normal file
@@ -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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export { AvatarUpload } from './AvatarUpload';
|
||||||
|
export { BattleDialog } from './BattleDialog';
|
||||||
|
export { default as ContactForm } from './ContactForm';
|
||||||
|
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';
|
||||||
30
assets/js/contact.jsx
Normal file
30
assets/js/contact.jsx
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
/**
|
||||||
|
* 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 { ContactForm } from '@global-components';
|
||||||
|
|
||||||
|
const wrapper = document.getElementById('contact-form-wrapper');
|
||||||
|
|
||||||
|
if (wrapper) {
|
||||||
|
const siteKey = wrapper.dataset.siteKey;
|
||||||
|
const recaptchaFieldId = wrapper.dataset.recaptchaFieldId;
|
||||||
|
|
||||||
|
if (siteKey && recaptchaFieldId) {
|
||||||
|
createRoot(wrapper).render(
|
||||||
|
<ContactForm
|
||||||
|
siteKey={siteKey}
|
||||||
|
recaptchaFieldId={recaptchaFieldId}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
console.error('ContactForm: Missing siteKey or recaptchaFieldId in data attributes');
|
||||||
|
}
|
||||||
|
}
|
||||||
43
assets/js/mine-seeker/MineSeeker.jsx
Normal file
43
assets/js/mine-seeker/MineSeeker.jsx
Normal file
@@ -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,
|
||||||
|
};
|
||||||
33
assets/js/mine-seeker/components/BonusBox.jsx
Normal file
33
assets/js/mine-seeker/components/BonusBox.jsx
Normal file
@@ -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,
|
||||||
|
};
|
||||||
79
assets/js/mine-seeker/components/BonusStatsDialog.jsx
Normal file
79
assets/js/mine-seeker/components/BonusStatsDialog.jsx
Normal file
@@ -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,
|
||||||
|
};
|
||||||
123
assets/js/mine-seeker/components/CaptchaOverlay.jsx
Normal file
123
assets/js/mine-seeker/components/CaptchaOverlay.jsx
Normal file
@@ -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, useCallback, useEffect, useMemo, 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 RECAPTCHA_ACTION = 'mineseeker_play';
|
||||||
|
|
||||||
|
const CaptchaOverlay = ({ siteKey, onVerified, children }) => {
|
||||||
|
const [verified, setVerified] = useState(false);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState(false);
|
||||||
|
|
||||||
|
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]);
|
||||||
|
|
||||||
|
const buttonClasses = useMemo(() => [
|
||||||
|
'captcha-button',
|
||||||
|
error && 'captcha-button--error',
|
||||||
|
loading && 'captcha-button--loading',
|
||||||
|
].filter(Boolean).join(' '), [error, loading]);
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (window.grecaptcha) {
|
||||||
|
window.grecaptcha.ready(() => {
|
||||||
|
window.grecaptcha
|
||||||
|
.execute(siteKey, { action: RECAPTCHA_ACTION })
|
||||||
|
.then(token => {
|
||||||
|
handleToken(token);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
setError(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [siteKey, onVerified, handleToken]);
|
||||||
|
|
||||||
|
|
||||||
|
const handleClick = () => {
|
||||||
|
setLoading(true);
|
||||||
|
setError(false);
|
||||||
|
|
||||||
|
window.grecaptcha.ready(() => {
|
||||||
|
window.grecaptcha
|
||||||
|
.execute(siteKey, { action: RECAPTCHA_ACTION })
|
||||||
|
.then(token => {
|
||||||
|
handleToken(token);
|
||||||
|
setLoading(false);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
setLoading(false);
|
||||||
|
setError(true);
|
||||||
|
setTimeout(() => setError(false), 2000);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
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">
|
||||||
|
Click below to verify you're human and start playing.
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
className={buttonClasses}
|
||||||
|
onClick={handleClick}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
<i className={`fa ${loading ? 'fa-spinner fa-spin' : error ? 'fa-exclamation-circle' : 'fa-play'}`} />
|
||||||
|
{loading ? 'Verifying...' : error ? 'Try Again' : 'Start Playing'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CaptchaOverlay;
|
||||||
|
|
||||||
|
CaptchaOverlay.propTypes = {
|
||||||
|
siteKey: string.isRequired,
|
||||||
|
onVerified: func,
|
||||||
|
children: node,
|
||||||
|
};
|
||||||
48
assets/js/mine-seeker/components/ChallengeCountdown.jsx
Normal file
48
assets/js/mine-seeker/components/ChallengeCountdown.jsx
Normal file
@@ -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,
|
||||||
|
};
|
||||||
52
assets/js/mine-seeker/components/GameBoard.jsx
Normal file
52
assets/js/mine-seeker/components/GameBoard.jsx
Normal file
@@ -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 siteKey = document.getElementById('mine-wrapper')?.dataset.recaptchaSiteKey;
|
||||||
|
|
||||||
|
if (!gridReady) {
|
||||||
|
return (
|
||||||
|
<div className="game-overlay">
|
||||||
|
<div className="game-overlay-window"><h1>Loading…</h1></div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!captchaVerified && siteKey) {
|
||||||
|
return (
|
||||||
|
<CaptchaOverlay siteKey={siteKey} onVerified={() => setCaptchaVerified(true)} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<GridControl
|
||||||
|
gameAssoc={gameAssoc}
|
||||||
|
onClick={onClick}
|
||||||
|
resign={resign}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
GameBoard.propTypes = {
|
||||||
|
gameAssoc: string.isRequired,
|
||||||
|
gameInherited: bool.isRequired,
|
||||||
|
opponentName: string,
|
||||||
|
isEnvDev: bool,
|
||||||
|
};
|
||||||
135
assets/js/mine-seeker/components/GameTimer.jsx
Normal file
135
assets/js/mine-seeker/components/GameTimer.jsx
Normal file
@@ -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;
|
||||||
294
assets/js/mine-seeker/components/OnlinePlayersDialog.jsx
Normal file
294
assets/js/mine-seeker/components/OnlinePlayersDialog.jsx
Normal file
@@ -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,
|
||||||
|
};
|
||||||
104
assets/js/mine-seeker/components/WaitingOverlayContent.jsx
Normal file
104
assets/js/mine-seeker/components/WaitingOverlayContent.jsx
Normal file
@@ -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,
|
||||||
|
};
|
||||||
123
assets/js/mine-seeker/components/grid/GridControl.jsx
Normal file
123
assets/js/mine-seeker/components/grid/GridControl.jsx
Normal file
@@ -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,
|
||||||
|
};
|
||||||
91
assets/js/mine-seeker/components/grid/GridField.jsx
Normal file
91
assets/js/mine-seeker/components/grid/GridField.jsx
Normal file
@@ -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,
|
||||||
|
};
|
||||||
22
assets/js/mine-seeker/components/index.js
Normal file
22
assets/js/mine-seeker/components/index.js
Normal file
@@ -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';
|
||||||
52
assets/js/mine-seeker/components/profile/PlayerColumn.jsx
Normal file
52
assets/js/mine-seeker/components/profile/PlayerColumn.jsx
Normal file
@@ -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,
|
||||||
|
};
|
||||||
28
assets/js/mine-seeker/components/timer/Avatar.jsx
Normal file
28
assets/js/mine-seeker/components/timer/Avatar.jsx
Normal file
@@ -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,
|
||||||
|
};
|
||||||
67
assets/js/mine-seeker/components/user/User.jsx
Normal file
67
assets/js/mine-seeker/components/user/User.jsx
Normal file
@@ -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,
|
||||||
|
};
|
||||||
76
assets/js/mine-seeker/components/user/UserControl.jsx
Normal file
76
assets/js/mine-seeker/components/user/UserControl.jsx
Normal file
@@ -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,
|
||||||
|
};
|
||||||
16
assets/js/mine-seeker/contexts/GameContext.jsx
Normal file
16
assets/js/mine-seeker/contexts/GameContext.jsx
Normal file
@@ -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;
|
||||||
280
assets/js/mine-seeker/contexts/GameProvider.jsx
Normal file
280
assets/js/mine-seeker/contexts/GameProvider.jsx
Normal file
@@ -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