Compare commits
109 Commits
0.4.0
...
v2026.2.5-
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 ###
|
||||
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 = []
|
||||
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
|
||||
54
.gitignore
vendored
54
.gitignore
vendored
@@ -1,40 +1,16 @@
|
||||
/app/config/parameters.yml
|
||||
/build/
|
||||
/phpunit.xml
|
||||
/var/*
|
||||
!/var/cache
|
||||
/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/
|
||||
|
||||
###> system7 - jotunheimr ###
|
||||
compose.override.yaml
|
||||
+bak/
|
||||
.idea/
|
||||
node_modules/
|
||||
nohup.out
|
||||
src/Mine/SeekerBundle/Resources/public/js/index.js
|
||||
src/Mine/SeekerBundle/Resources/public/js/index.min.js
|
||||
npm-debug.log
|
||||
/public/build/
|
||||
/config/reference.php
|
||||
###< system7 - jotunheimr ###
|
||||
|
||||
###> symfony/framework-bundle ###
|
||||
/.env
|
||||
/public/bundles/
|
||||
/var/
|
||||
/vendor/
|
||||
###< symfony/framework-bundle ###
|
||||
|
||||
414
CHANGELOG.md
Normal file
414
CHANGELOG.md
Normal file
@@ -0,0 +1,414 @@
|
||||
# Changelog
|
||||
|
||||
|
||||
## 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
|
||||
}
|
||||
58
Dockerfile
Normal file
58
Dockerfile
Normal file
@@ -0,0 +1,58 @@
|
||||
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 apt-get update && apt-get install -y --no-install-recommends \
|
||||
fonts-dejavu-core \
|
||||
fontconfig \
|
||||
&& fc-cache -f -v \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.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)
|
||||
|
||||
|
||||
84
Makefile
Normal file
84
Makefile
Normal file
@@ -0,0 +1,84 @@
|
||||
.PHONY: help start start-build stop build down ps logs prune-everything db-reset mercure-jwt cache-clear og-cache-clear
|
||||
|
||||
.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"
|
||||
|
||||
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"
|
||||
|
||||
|
||||
|
||||
305
README.md
305
README.md
@@ -1,35 +1,302 @@
|
||||
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;
|
||||
The browser will stop the code here!!!
|
||||
## Installation
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Game Documentation
|
||||
|
||||
For detailed information about game mechanics, bonus systems, and scoring rules, see the [docs](./docs/) directory:
|
||||
|
||||
- **[Bonus Points System](./docs/game-mechanics/BONUS_POINTS_SYSTEM.md)** — Complete reference for all bonus point types, calculation rules, and implementation details
|
||||
|
||||
---
|
||||
|
||||
## License
|
||||
|
||||
LGPL-3.0 — see [LICENSE](LICENSE) for details.
|
||||
|
||||
© 2026 [SplendidBear](https://www.splendidbear.org)
|
||||
|
||||
@@ -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';
|
||||
9
assets/css/homepage/_animations.scss
Normal file
9
assets/css/homepage/_animations.scss
Normal file
@@ -0,0 +1,9 @@
|
||||
@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); }
|
||||
}
|
||||
119
assets/css/homepage/_auth-bar.scss
Normal file
119
assets/css/homepage/_auth-bar.scss
Normal file
@@ -0,0 +1,119 @@
|
||||
#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;
|
||||
}
|
||||
}
|
||||
465
assets/css/homepage/_auth.scss
Normal file
465
assets/css/homepage/_auth.scss
Normal file
@@ -0,0 +1,465 @@
|
||||
.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;
|
||||
}
|
||||
}
|
||||
43
assets/css/homepage/_content.scss
Normal file
43
assets/css/homepage/_content.scss
Normal file
@@ -0,0 +1,43 @@
|
||||
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;
|
||||
}
|
||||
75
assets/css/homepage/_cta.scss
Normal file
75
assets/css/homepage/_cta.scss
Normal file
@@ -0,0 +1,75 @@
|
||||
.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);
|
||||
}
|
||||
|
||||
|
||||
354
assets/css/homepage/_features.scss
Normal file
354
assets/css/homepage/_features.scss
Normal file
@@ -0,0 +1,354 @@
|
||||
.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;
|
||||
}
|
||||
}
|
||||
112
assets/css/homepage/_footer.scss
Normal file
112
assets/css/homepage/_footer.scss
Normal file
@@ -0,0 +1,112 @@
|
||||
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; }
|
||||
}
|
||||
}
|
||||
39
assets/css/homepage/_header.scss
Normal file
39
assets/css/homepage/_header.scss
Normal file
@@ -0,0 +1,39 @@
|
||||
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;
|
||||
}
|
||||
49
assets/css/homepage/_hero-compact.scss
Normal file
49
assets/css/homepage/_hero-compact.scss
Normal file
@@ -0,0 +1,49 @@
|
||||
.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;
|
||||
}
|
||||
90
assets/css/homepage/_hero.scss
Normal file
90
assets/css/homepage/_hero.scss
Normal file
@@ -0,0 +1,90 @@
|
||||
.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;
|
||||
}
|
||||
1148
assets/css/homepage/_profile.scss
Normal file
1148
assets/css/homepage/_profile.scss
Normal file
File diff suppressed because it is too large
Load Diff
23
assets/css/homepage/_reset.scss
Normal file
23
assets/css/homepage/_reset.scss
Normal file
@@ -0,0 +1,23 @@
|
||||
* {
|
||||
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%;
|
||||
}
|
||||
104
assets/css/homepage/_responsive.scss
Normal file
104
assets/css/homepage/_responsive.scss
Normal file
@@ -0,0 +1,104 @@
|
||||
@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;
|
||||
}
|
||||
}
|
||||
60
assets/css/homepage/_tech.scss
Normal file
60
assets/css/homepage/_tech.scss
Normal file
@@ -0,0 +1,60 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
24
assets/css/mineseeker/_back-button.scss
Normal file
24
assets/css/mineseeker/_back-button.scss
Normal file
@@ -0,0 +1,24 @@
|
||||
.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);
|
||||
}
|
||||
56
assets/css/mineseeker/_base.scss
Normal file
56
assets/css/mineseeker/_base.scss
Normal file
@@ -0,0 +1,56 @@
|
||||
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;
|
||||
}
|
||||
225
assets/css/mineseeker/_bomb.scss
Normal file
225
assets/css/mineseeker/_bomb.scss
Normal file
@@ -0,0 +1,225 @@
|
||||
#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;
|
||||
}
|
||||
}
|
||||
207
assets/css/mineseeker/_grid.scss
Normal file
207
assets/css/mineseeker/_grid.scss
Normal file
@@ -0,0 +1,207 @@
|
||||
#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%;
|
||||
}
|
||||
90
assets/css/mineseeker/_mine-counter.scss
Normal file
90
assets/css/mineseeker/_mine-counter.scss
Normal file
@@ -0,0 +1,90 @@
|
||||
#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;
|
||||
}
|
||||
678
assets/css/mineseeker/_overlay.scss
Normal file
678
assets/css/mineseeker/_overlay.scss
Normal file
@@ -0,0 +1,678 @@
|
||||
#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;
|
||||
}
|
||||
}
|
||||
|
||||
41
assets/css/mineseeker/_responsive.scss
Normal file
41
assets/css/mineseeker/_responsive.scss
Normal file
@@ -0,0 +1,41 @@
|
||||
@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;
|
||||
}
|
||||
}
|
||||
102
assets/css/mineseeker/_timer.scss
Normal file
102
assets/css/mineseeker/_timer.scss
Normal file
@@ -0,0 +1,102 @@
|
||||
#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;
|
||||
}
|
||||
162
assets/css/mineseeker/_users.scss
Normal file
162
assets/css/mineseeker/_users.scss
Normal file
@@ -0,0 +1,162 @@
|
||||
#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;
|
||||
}
|
||||
347
assets/css/mineseeker/_waiting-dialog.scss
Normal file
347
assets/css/mineseeker/_waiting-dialog.scss
Normal file
@@ -0,0 +1,347 @@
|
||||
.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); }
|
||||
}
|
||||
292
assets/css/passkey.scss
Normal file
292
assets/css/passkey.scss
Normal file
@@ -0,0 +1,292 @@
|
||||
@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);
|
||||
}
|
||||
}
|
||||
15
assets/css/style.homepage.scss
Normal file
15
assets/css/style.homepage.scss
Normal file
@@ -0,0 +1,15 @@
|
||||
@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/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) 2026 @ 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) 2026 @ 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';
|
||||
10
assets/css/style.scss
Normal file
10
assets/css/style.scss
Normal file
@@ -0,0 +1,10 @@
|
||||
.mine-beta {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 150px;
|
||||
|
||||
z-index: 4;
|
||||
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
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 || ''}
|
||||
/>,
|
||||
);
|
||||
71
assets/js/components/AvatarUpload.jsx
Normal file
71
assets/js/components/AvatarUpload.jsx
Normal file
@@ -0,0 +1,71 @@
|
||||
import React, { useRef, useState } from 'react';
|
||||
|
||||
export default function AvatarUpload({ uploadUrl, initialThumbUrl, initials }) {
|
||||
const [thumbUrl, setThumbUrl] = useState(initialThumbUrl || null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
const inputRef = useRef(null);
|
||||
|
||||
function handleClick() {
|
||||
inputRef.current?.click();
|
||||
}
|
||||
|
||||
function handleChange(e) {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
const fd = new FormData();
|
||||
fd.append('avatar', file);
|
||||
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
fetch(uploadUrl, { method: 'POST', body: fd })
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.error) {
|
||||
setError(data.error);
|
||||
return;
|
||||
}
|
||||
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);
|
||||
}
|
||||
})
|
||||
.catch(() => setError('Upload failed. Please try again.'))
|
||||
.finally(() => setLoading(false));
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`profile-avatar${loading ? ' profile-avatar--loading' : ''}`}
|
||||
title="Click to change profile picture"
|
||||
onClick={handleClick}
|
||||
>
|
||||
{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}
|
||||
/>
|
||||
{error && <div className="profile-avatar__error">{error}</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
232
assets/js/components/BattleDialog.jsx
Normal file
232
assets/js/components/BattleDialog.jsx
Normal file
@@ -0,0 +1,232 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import Dialog from '@mui/material/Dialog';
|
||||
import { createTheme, ThemeProvider } from '@mui/material/styles';
|
||||
import Avatar from './battle-dialog/Avatar';
|
||||
import StatRow from './battle-dialog/StatRow';
|
||||
import BonusPoints from './battle-dialog/BonusPoints';
|
||||
|
||||
const darkTheme = createTheme({ palette: { mode: 'dark' } });
|
||||
|
||||
const DIALOG_SX = {
|
||||
'& .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)',
|
||||
},
|
||||
};
|
||||
|
||||
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 default function 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}><Dialog open={false} sx={DIALOG_SX} /></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 shareUrl = `${window.location.origin}/battle/${game.uuid}`;
|
||||
const canContinue = !resign && 26 > maxPoints;
|
||||
const playUrl = `${window.location.origin}/play/${game.uuid}`;
|
||||
|
||||
const formatDuration = (from, to) => {
|
||||
if (!from || !to) return null;
|
||||
const diffMs = new Date(to.replace(' ', 'T')) - new Date(from.replace(' ', 'T'));
|
||||
if (isNaN(diffMs) || 0 >= diffMs) return null;
|
||||
const totalSec = Math.floor(diffMs / 1000);
|
||||
const h = Math.floor(totalSec / 3600);
|
||||
const m = Math.floor((totalSec % 3600) / 60);
|
||||
const s = totalSec % 60;
|
||||
if (0 < h) return `${h}h ${m}m ${s}s`;
|
||||
if (0 < m) return `${m}m ${s}s`;
|
||||
return `${s}s`;
|
||||
};
|
||||
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}>
|
||||
<Dialog open={open} onClose={() => setOpen(false)} sx={DIALOG_SX}>
|
||||
<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 style={{ display: 'flex', gap: 8 }}>
|
||||
{canContinue ? (
|
||||
<a
|
||||
className="bd-continue"
|
||||
href={playUrl}
|
||||
aria-label="Continue the game"
|
||||
title="Continue the game"
|
||||
>
|
||||
<i className="fa fa-play" />
|
||||
Continue
|
||||
</a>
|
||||
) : (
|
||||
<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>
|
||||
)}
|
||||
<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" style={{ marginBottom: 8 }}>
|
||||
<span style={{
|
||||
font: '700 13px \'Rajdhani\', sans-serif',
|
||||
color: '#f67d52',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 4,
|
||||
}}
|
||||
>
|
||||
<i className="fa fa-star" style={{ fontSize: 11 }} /> {(game.redBonusPoints ?? 0).toFixed(1)}
|
||||
</span>
|
||||
<span className="bd-vs-score__sep">:</span>
|
||||
<span style={{
|
||||
font: '700 13px \'Rajdhani\', sans-serif',
|
||||
color: '#95cff5',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 4,
|
||||
}}
|
||||
>
|
||||
{(game.blueBonusPoints ?? 0).toFixed(1)} <i className="fa fa-star" style={{ fontSize: 11 }} />
|
||||
</span>
|
||||
</div>
|
||||
<div className="bd-vs-label">VS</div>
|
||||
<div
|
||||
className="bd-result-badge"
|
||||
style={{ background: meta.bg, border: `1px solid ${meta.border}`, 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>
|
||||
</Dialog>
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
83
assets/js/components/ContactForm.jsx
Normal file
83
assets/js/components/ContactForm.jsx
Normal file
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* 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';
|
||||
|
||||
/**
|
||||
* 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;
|
||||
};
|
||||
|
||||
export default ContactForm;
|
||||
110
assets/js/components/PasskeyLogin.jsx
Normal file
110
assets/js/components/PasskeyLogin.jsx
Normal file
@@ -0,0 +1,110 @@
|
||||
/**
|
||||
* This file is part of the SplendidBear Websites' projects.
|
||||
*
|
||||
* Copyright (c) 2026 @ www.splendidbear.org
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
import React, { useState, useCallback } from 'react';
|
||||
|
||||
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;
|
||||
410
assets/js/components/PasskeyManager.jsx
Normal file
410
assets/js/components/PasskeyManager.jsx
Normal file
@@ -0,0 +1,410 @@
|
||||
/**
|
||||
* 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 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';
|
||||
|
||||
const DIALOG_SX = {
|
||||
'& .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>
|
||||
|
||||
<Dialog open={addModalOpen} onClose={closeAddModal} sx={DIALOG_SX}>
|
||||
<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>
|
||||
</Dialog>
|
||||
|
||||
<Dialog open={renameModalOpen} onClose={closeRenameModal} sx={DIALOG_SX}>
|
||||
<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>
|
||||
</Dialog>
|
||||
|
||||
<Dialog open={deleteModalOpen} onClose={closeDeleteModal} sx={DIALOG_SX}>
|
||||
<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>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PasskeyManager;
|
||||
138
assets/js/components/ProfileCharts.jsx
Normal file
138
assets/js/components/ProfileCharts.jsx
Normal file
@@ -0,0 +1,138 @@
|
||||
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';
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
98
assets/js/components/battle-dialog/Avatar.jsx
Normal file
98
assets/js/components/battle-dialog/Avatar.jsx
Normal file
@@ -0,0 +1,98 @@
|
||||
/**
|
||||
* 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';
|
||||
|
||||
export default function Avatar({ name, color, avatarUrl, bonusPoints = 0 }) {
|
||||
const isRed = 'red' === color;
|
||||
const initials = (name || '?').slice(0, 2).toUpperCase();
|
||||
|
||||
const gradient = isRed
|
||||
? 'linear-gradient(135deg, rgba(173,10,5,0.6) 0%, rgba(246,125,82,0.4) 100%)'
|
||||
: 'linear-gradient(135deg, rgba(35,111,135,0.6) 0%, rgba(41,128,185,0.4) 100%)';
|
||||
const glow = isRed
|
||||
? '0 0 0 3px rgba(173,10,5,0.2), 0 0 28px rgba(173,10,5,0.35)'
|
||||
: '0 0 0 3px rgba(35,111,135,0.2), 0 0 28px rgba(35,111,135,0.35)';
|
||||
const border = isRed
|
||||
? 'rgba(173,10,5,0.5)'
|
||||
: 'rgba(35,111,135,0.5)';
|
||||
const textColor = isRed ? '#f67d52' : '#95cff5';
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 10, position: 'relative' }}>
|
||||
<div style={{ position: 'relative' }}>
|
||||
<div style={{
|
||||
width: 72, height: 72, borderRadius: '50%',
|
||||
background: avatarUrl ? 'transparent' : gradient,
|
||||
border: `2px solid ${border}`,
|
||||
boxShadow: glow,
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
font: '800 24px \'Rajdhani\', sans-serif',
|
||||
color: textColor,
|
||||
letterSpacing: 2,
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
{avatarUrl ? (
|
||||
<img
|
||||
src={avatarUrl}
|
||||
alt={name}
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
objectFit: 'cover',
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
initials
|
||||
)}
|
||||
</div>
|
||||
{0 < bonusPoints && (
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
bottom: -6,
|
||||
right: -6,
|
||||
background: '#ffd700',
|
||||
borderRadius: '50%',
|
||||
width: 28,
|
||||
height: 28,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
boxShadow: '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)',
|
||||
zIndex: 10,
|
||||
}}
|
||||
>
|
||||
<i className="fa fa-star" style={{ color: '#000', fontSize: 14 }} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<span style={{
|
||||
font: '700 15px \'Rajdhani\', sans-serif',
|
||||
color: textColor,
|
||||
letterSpacing: 1,
|
||||
maxWidth: 120, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
{name}
|
||||
</span>
|
||||
<span style={{
|
||||
font: '600 10px \'Rajdhani\', sans-serif',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: 2,
|
||||
color: 'rgba(255,255,255,0.3)',
|
||||
}}
|
||||
>
|
||||
{isRed ? 'Red' : 'Blue'}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
139
assets/js/components/battle-dialog/BonusPoints.jsx
Normal file
139
assets/js/components/battle-dialog/BonusPoints.jsx
Normal file
@@ -0,0 +1,139 @@
|
||||
import { useMemo } from 'react';
|
||||
import StatRow from './StatRow';
|
||||
|
||||
export default function 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 style={{
|
||||
padding: '16px 20px 0',
|
||||
borderTop: '1px solid rgba(255,255,255,0.08)',
|
||||
marginTop: 16,
|
||||
marginBottom: 16,
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16 }}>
|
||||
{/* Red Bonus */}
|
||||
<div style={{
|
||||
padding: 16,
|
||||
border: '1px solid rgba(173,10,5,0.2)',
|
||||
borderRadius: 6,
|
||||
background: 'rgba(173,10,5,0.05)',
|
||||
}}
|
||||
>
|
||||
<span style={{
|
||||
font: '700 12px \'Rajdhani\', sans-serif',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: 2,
|
||||
color: '#ffd700',
|
||||
display: 'block',
|
||||
marginBottom: 12,
|
||||
}}
|
||||
>
|
||||
<i className="fa fa-star" style={{ marginRight: 8 }} /> Red Bonus Statistics
|
||||
</span>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 0 }}>
|
||||
<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 style={{
|
||||
padding: 16,
|
||||
border: '1px solid rgba(149,207,245,0.2)',
|
||||
borderRadius: 6,
|
||||
background: 'rgba(149,207,245,0.05)',
|
||||
}}
|
||||
>
|
||||
<span style={{
|
||||
font: '700 12px \'Rajdhani\', sans-serif',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: 2,
|
||||
color: '#ffd700',
|
||||
display: 'block',
|
||||
marginBottom: 12,
|
||||
}}
|
||||
>
|
||||
<i className="fa fa-star" style={{ marginRight: 8 }} /> Blue Bonus Statistics
|
||||
</span>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 0 }}>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
40
assets/js/components/battle-dialog/StatRow.jsx
Normal file
40
assets/js/components/battle-dialog/StatRow.jsx
Normal file
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* 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';
|
||||
|
||||
export default function StatRow({ icon, label, value, valueColor }) {
|
||||
return (
|
||||
<div style={{
|
||||
display: 'flex', alignItems: 'center',
|
||||
gap: 10, padding: '9px 0',
|
||||
borderBottom: '1px solid rgba(255,255,255,0.05)',
|
||||
}}
|
||||
>
|
||||
<i className={`fa ${icon}`} style={{ width: 16, color: 'rgba(149,207,245,0.4)', fontSize: 13 }} />
|
||||
<span style={{
|
||||
font: '500 13px \'Rajdhani\', sans-serif',
|
||||
color: 'rgba(255,255,255,0.45)',
|
||||
flex: 1,
|
||||
letterSpacing: 0.5,
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
<span style={{
|
||||
font: '700 13px \'Rajdhani\', sans-serif',
|
||||
color: valueColor || 'rgba(255,255,255,0.75)',
|
||||
letterSpacing: 0.5,
|
||||
}}
|
||||
>
|
||||
{value}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
31
assets/js/contact.jsx
Normal file
31
assets/js/contact.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 { createRoot } from 'react-dom/client';
|
||||
import ContactForm from './components/ContactForm';
|
||||
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
||||
36
assets/js/mine-seeker/MineSeeker.jsx
Normal file
36
assets/js/mine-seeker/MineSeeker.jsx
Normal file
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* 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';
|
||||
|
||||
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;
|
||||
25
assets/js/mine-seeker/components/BonusBox.jsx
Normal file
25
assets/js/mine-seeker/components/BonusBox.jsx
Normal file
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* 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';
|
||||
|
||||
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;
|
||||
97
assets/js/mine-seeker/components/BonusStatsDialog.jsx
Normal file
97
assets/js/mine-seeker/components/BonusStatsDialog.jsx
Normal file
@@ -0,0 +1,97 @@
|
||||
/**
|
||||
* 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 { BONUS_LABELS } from '@mine-utils';
|
||||
|
||||
const DIALOG_SX = {
|
||||
'& .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)',
|
||||
},
|
||||
};
|
||||
|
||||
const formatPlayerName = name => {
|
||||
if (name && name.startsWith('anon_')) {
|
||||
return 'Anonymous';
|
||||
}
|
||||
|
||||
if (name && 10 < name.length) {
|
||||
return name.substring(0, 7) + '...';
|
||||
}
|
||||
|
||||
return name || 'Unknown';
|
||||
};
|
||||
|
||||
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>
|
||||
);
|
||||
|
||||
const BonusStatsDialog = ({ open, onClose, red, blue }) => (
|
||||
<Dialog open={open} onClose={onClose} sx={DIALOG_SX}>
|
||||
<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>
|
||||
</Dialog>
|
||||
);
|
||||
|
||||
export default BonusStatsDialog;
|
||||
169
assets/js/mine-seeker/components/CaptchaOverlay.jsx
Normal file
169
assets/js/mine-seeker/components/CaptchaOverlay.jsx
Normal file
@@ -0,0 +1,169 @@
|
||||
/**
|
||||
* 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';
|
||||
|
||||
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);
|
||||
|
||||
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]);
|
||||
|
||||
const handleToken = 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?.();
|
||||
};
|
||||
|
||||
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 <>{children}</>;
|
||||
}
|
||||
|
||||
const overlayStyles = {
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
background: 'rgba(7, 9, 13, 0.95)',
|
||||
backdropFilter: 'blur(8px)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
zIndex: 1000,
|
||||
};
|
||||
|
||||
const contentStyles = {
|
||||
textAlign: 'center',
|
||||
color: '#fff',
|
||||
maxWidth: '400px',
|
||||
padding: '40px',
|
||||
};
|
||||
|
||||
const iconStyles = {
|
||||
fontSize: '64px',
|
||||
color: '#236f87',
|
||||
marginBottom: '24px',
|
||||
};
|
||||
|
||||
const h1Styles = {
|
||||
font: '800 32px Rajdhani, sans-serif',
|
||||
margin: '0 0 16px',
|
||||
letterSpacing: '1px',
|
||||
};
|
||||
|
||||
const pStyles = {
|
||||
color: 'rgba(149, 207, 245, 0.7)',
|
||||
font: '400 16px Rajdhani, sans-serif',
|
||||
margin: '0 0 32px',
|
||||
letterSpacing: '0.5px',
|
||||
};
|
||||
|
||||
const buttonStyles = {
|
||||
background: error
|
||||
? 'linear-gradient(#8a2323 0%, #681a1a 100%)'
|
||||
: loading
|
||||
? 'linear-gradient(#236f87 0%, #1a5068 100%)'
|
||||
: 'linear-gradient(#236f87 0%, #1a5068 100%)',
|
||||
border: `2px solid ${error ? '#9a2e2e' : loading ? '#2e7a9a' : '#2e7a9a'}`,
|
||||
borderRadius: '8px',
|
||||
color: '#e0f4ff',
|
||||
cursor: loading ? 'wait' : 'pointer',
|
||||
font: '800 18px Rajdhani, sans-serif',
|
||||
letterSpacing: '2px',
|
||||
padding: '16px 40px',
|
||||
textTransform: 'uppercase',
|
||||
transition: 'all 0.3s ease',
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: '12px',
|
||||
opacity: loading ? 0.7 : 1,
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={overlayStyles}>
|
||||
<div style={contentStyles}>
|
||||
<div style={iconStyles}>
|
||||
<i className="fa fa-shield-halved" />
|
||||
</div>
|
||||
<h1 style={h1Styles}>Ready to Play?</h1>
|
||||
<p style={pStyles}>
|
||||
Click below to verify you're human and start playing.
|
||||
</p>
|
||||
<button
|
||||
style={buttonStyles}
|
||||
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;
|
||||
41
assets/js/mine-seeker/components/ChallengeCountdown.jsx
Normal file
41
assets/js/mine-seeker/components/ChallengeCountdown.jsx
Normal file
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* 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';
|
||||
|
||||
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;
|
||||
34
assets/js/mine-seeker/components/GameBoard.jsx
Normal file
34
assets/js/mine-seeker/components/GameBoard.jsx
Normal file
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* 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 { useGame } from '@mine-contexts';
|
||||
import { useServerCommunication } from '@mine-hooks';
|
||||
import GridControl from './grid/GridControl';
|
||||
|
||||
export const GameBoard = ({ gameAssoc, gameInherited, opponentName = '', isEnvDev }) => {
|
||||
const { gridReady } = useGame();
|
||||
const { onClick, resign } = useServerCommunication(gameAssoc, gameInherited, opponentName, isEnvDev);
|
||||
|
||||
if (!gridReady) {
|
||||
return (
|
||||
<div className="game-overlay">
|
||||
<div className="game-overlay-window"><h1>Loading…</h1></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<GridControl
|
||||
gameAssoc={gameAssoc}
|
||||
onClick={onClick}
|
||||
resign={resign}
|
||||
/>
|
||||
);
|
||||
};
|
||||
188
assets/js/mine-seeker/components/GameTimer.jsx
Normal file
188
assets/js/mine-seeker/components/GameTimer.jsx
Normal file
@@ -0,0 +1,188 @@
|
||||
/**
|
||||
* 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, useRef, useState } from 'react';
|
||||
import { useGame } from '@mine-contexts';
|
||||
import BonusBox from './BonusBox';
|
||||
import BonusStatsDialog from './BonusStatsDialog';
|
||||
|
||||
const renderAvatar = player => {
|
||||
if (!player.registered) return null;
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
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);
|
||||
|
||||
// Use timestamps instead of counters for more reliable background tracking
|
||||
const redStartTimeRef = useRef(null);
|
||||
const blueStartTimeRef = useRef(null);
|
||||
const lastActivePlayerRef = useRef(null);
|
||||
const pausedRedTimeRef = useRef(0);
|
||||
const pausedBlueTimeRef = useRef(0);
|
||||
|
||||
// Start timer when overlay is hidden (both players connected and game started)
|
||||
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;
|
||||
}
|
||||
}, [overlay]);
|
||||
|
||||
// Stop timer on game end (resign/win)
|
||||
useEffect(() => {
|
||||
if (endRef.current) {
|
||||
setIsRunning(false);
|
||||
}
|
||||
}, [endRef.current]);
|
||||
|
||||
// Stop timer on connection loss
|
||||
useEffect(() => {
|
||||
if (connectionLost) {
|
||||
setIsRunning(false);
|
||||
}
|
||||
}, [connectionLost]);
|
||||
|
||||
// Handle player switch - pause one timer, resume the other
|
||||
useEffect(() => {
|
||||
if (!isRunning) return;
|
||||
|
||||
if (lastActivePlayerRef.current !== activePlayer) {
|
||||
// Player switched, save current accumulated time for whoever was active
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// Start the new active player's timer
|
||||
if (activePlayer) {
|
||||
blueStartTimeRef.current = Date.now();
|
||||
} else {
|
||||
redStartTimeRef.current = Date.now();
|
||||
}
|
||||
|
||||
lastActivePlayerRef.current = activePlayer;
|
||||
}
|
||||
}, [activePlayer, isRunning]);
|
||||
|
||||
// Main timer effect - update display every 100ms
|
||||
useEffect(() => {
|
||||
if (!isRunning) {
|
||||
if (timerIntervalRef.current) {
|
||||
clearInterval(timerIntervalRef.current);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
timerIntervalRef.current = setInterval(() => {
|
||||
let currentRedTime = pausedRedTimeRef.current;
|
||||
let currentBlueTime = pausedBlueTimeRef.current;
|
||||
|
||||
// Add elapsed time for the active player
|
||||
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);
|
||||
}, 100);
|
||||
|
||||
return () => {
|
||||
if (timerIntervalRef.current) {
|
||||
clearInterval(timerIntervalRef.current);
|
||||
}
|
||||
};
|
||||
}, [isRunning, activePlayer]);
|
||||
|
||||
// Handle focus/blur to synchronize timer when tab regains focus
|
||||
useEffect(() => {
|
||||
const handleFocus = () => {
|
||||
// Force update when tab regains focus to sync any background drift
|
||||
if (isRunning) {
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('focus', handleFocus);
|
||||
return () => window.removeEventListener('focus', handleFocus);
|
||||
}, [isRunning, activePlayer]);
|
||||
|
||||
// Cleanup on unmount
|
||||
useEffect(() => () => {
|
||||
if (timerIntervalRef.current) {
|
||||
clearInterval(timerIntervalRef.current);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const formatTime = seconds => {
|
||||
const mins = Math.floor(seconds / 60);
|
||||
const secs = seconds % 60;
|
||||
return `${String(mins).padStart(2, '0')}:${String(secs).padStart(2, '0')}`;
|
||||
};
|
||||
|
||||
const openBonusDialog = () => setBonusDialogOpen(true);
|
||||
const closeBonusDialog = () => setBonusDialogOpen(false);
|
||||
|
||||
return (
|
||||
<div className="game-timer-container">
|
||||
<BonusBox color="red" points={red.bonusPoints ?? 0} onClick={openBonusDialog} />
|
||||
<div className={`game-timer red-timer ${!activePlayer ? 'active' : ''}`}>
|
||||
{renderAvatar(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' : ''}`}>
|
||||
{renderAvatar(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={openBonusDialog} />
|
||||
<BonusStatsDialog open={bonusDialogOpen} onClose={closeBonusDialog} red={red} blue={blue} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default GameTimer;
|
||||
286
assets/js/mine-seeker/components/OnlinePlayersDialog.jsx
Normal file
286
assets/js/mine-seeker/components/OnlinePlayersDialog.jsx
Normal file
@@ -0,0 +1,286 @@
|
||||
/**
|
||||
* 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 Dialog from '@mui/material/Dialog';
|
||||
|
||||
const DIALOG_SX = {
|
||||
'& .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)',
|
||||
},
|
||||
};
|
||||
|
||||
const formatSince = isoStr => {
|
||||
const diff = Math.floor((Date.now() - new Date(isoStr)) / 60000);
|
||||
if (1 > diff) return 'just now';
|
||||
if (1 === diff) return '1 min ago';
|
||||
return `${diff} min ago`;
|
||||
};
|
||||
|
||||
const OnlinePlayersDialog = ({ open, onClose, currentGameAssoc }) => {
|
||||
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 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);
|
||||
fetch('/api/game/waiting')
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
// Filter out current user's game from the snapshot
|
||||
const filtered = data.filter(p => p.gameAssoc !== currentGameAssoc);
|
||||
setPlayers(filtered);
|
||||
setSnapshotLoaded(true);
|
||||
setLoading(false);
|
||||
})
|
||||
.catch(() => {
|
||||
setPlayers([]);
|
||||
setSnapshotLoaded(true);
|
||||
setLoading(false);
|
||||
});
|
||||
}, [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(() => {
|
||||
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);
|
||||
fetch('/api/game/challenge/' + player.gameAssoc, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ challengerGameAssoc: currentGameAssoc }),
|
||||
}).catch(() => {
|
||||
setChallengingGameAssoc(null);
|
||||
setWaitingCountdown(0);
|
||||
});
|
||||
};
|
||||
|
||||
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 ('development' === process.env.NODE_ENV && 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 (
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={0 < waitingCountdown ? undefined : onClose}
|
||||
disableEscapeKeyDown={0 < waitingCountdown}
|
||||
sx={DIALOG_SX}
|
||||
>
|
||||
<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>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default OnlinePlayersDialog;
|
||||
96
assets/js/mine-seeker/components/WaitingOverlayContent.jsx
Normal file
96
assets/js/mine-seeker/components/WaitingOverlayContent.jsx
Normal file
@@ -0,0 +1,96 @@
|
||||
/**
|
||||
* 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';
|
||||
|
||||
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;
|
||||
113
assets/js/mine-seeker/components/grid/GridControl.jsx
Normal file
113
assets/js/mine-seeker/components/grid/GridControl.jsx
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.
|
||||
*/
|
||||
|
||||
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';
|
||||
|
||||
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}/battle/${gameAssoc}` : null;
|
||||
const isAuthenticated = '1' === document.getElementById('mine-wrapper')?.dataset.isAuthenticated;
|
||||
|
||||
const handleShare = () => {
|
||||
if (!shareUrl) return;
|
||||
navigator.clipboard.writeText(shareUrl).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>
|
||||
) : (
|
||||
overlaySubTitle
|
||||
)}
|
||||
{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;
|
||||
77
assets/js/mine-seeker/components/grid/GridField.jsx
Normal file
77
assets/js/mine-seeker/components/grid/GridField.jsx
Normal file
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* 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';
|
||||
|
||||
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;
|
||||
18
assets/js/mine-seeker/components/index.js
Normal file
18
assets/js/mine-seeker/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 { 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';
|
||||
54
assets/js/mine-seeker/components/user/User.jsx
Normal file
54
assets/js/mine-seeker/components/user/User.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, { memo } from 'react';
|
||||
import { IMAGES } from '@mine-utils';
|
||||
|
||||
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;
|
||||
70
assets/js/mine-seeker/components/user/UserControl.jsx
Normal file
70
assets/js/mine-seeker/components/user/UserControl.jsx
Normal file
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* 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';
|
||||
|
||||
const UserControl = ({ resign }) => {
|
||||
const { webPlayer, activePlayer, mines, 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 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">{mines}</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;
|
||||
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;
|
||||
274
assets/js/mine-seeker/contexts/GameProvider.jsx
Normal file
274
assets/js/mine-seeker/contexts/GameProvider.jsx
Normal file
@@ -0,0 +1,274 @@
|
||||
/**
|
||||
* 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'] }),
|
||||
});
|
||||
|
||||
/** 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) showOverlay((redWins ? 'Red' : 'Blue') + ' wins the game!', 'Play again!');
|
||||
|
||||
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,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</GameContext.Provider>
|
||||
);
|
||||
};
|
||||
13
assets/js/mine-seeker/contexts/index.js
Normal file
13
assets/js/mine-seeker/contexts/index.js
Normal file
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* 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 { useGame } from './GameContext';
|
||||
export { GameProvider } from './GameProvider';
|
||||
export { default as GameContext } from './GameContext';
|
||||
|
||||
14
assets/js/mine-seeker/hooks/index.js
Normal file
14
assets/js/mine-seeker/hooks/index.js
Normal file
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* 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 { default as useGameRefs } from './useGameRefs';
|
||||
export { default as useGameState } from './useGameState';
|
||||
export { default as useServerCommunication } from './useServerCommunication';
|
||||
export { default as useStepTimer } from './useStepTimer';
|
||||
|
||||
35
assets/js/mine-seeker/hooks/useGameRefs.jsx
Normal file
35
assets/js/mine-seeker/hooks/useGameRefs.jsx
Normal file
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* 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 { useRef } from 'react';
|
||||
import { PLAYER_DEF } from '@mine-utils';
|
||||
|
||||
const useGameRefs = () => {
|
||||
const webPlayerRef = useRef(null);
|
||||
const activePlayerRef = useRef(false);
|
||||
const bombSelectedRef = useRef(false);
|
||||
const connectionLostRef = useRef(false);
|
||||
const redRef = useRef({ ...PLAYER_DEF });
|
||||
const blueRef = useRef({ ...PLAYER_DEF });
|
||||
const lastClickedRef = useRef({ red: null, blue: null });
|
||||
const endRef = useRef(false);
|
||||
|
||||
return {
|
||||
webPlayerRef,
|
||||
activePlayerRef,
|
||||
bombSelectedRef,
|
||||
connectionLostRef,
|
||||
redRef,
|
||||
blueRef,
|
||||
lastClickedRef,
|
||||
endRef,
|
||||
};
|
||||
};
|
||||
|
||||
export default useGameRefs;
|
||||
45
assets/js/mine-seeker/hooks/useGameState.jsx
Normal file
45
assets/js/mine-seeker/hooks/useGameState.jsx
Normal file
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* 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 { useState } from 'react';
|
||||
import { initCells, PLAYER_DEF } from '@mine-utils';
|
||||
|
||||
const useGameState = () => {
|
||||
const [webPlayer, setWebPlayer] = useState(null);
|
||||
const [activePlayer, setActivePlayer] = useState(false);
|
||||
const [overlay, setOverlay] = useState(true);
|
||||
const [overlayTitle, setOverlayTitle] = useState('');
|
||||
const [overlaySubTitle, setOverlaySubTitle] = useState('');
|
||||
const [mines, setMines] = useState(51);
|
||||
const [bombSelected, setBombSelected] = useState(false);
|
||||
const [foundMines, setFoundMines] = useState(false);
|
||||
const [red, setRed] = useState({ ...PLAYER_DEF });
|
||||
const [blue, setBlue] = useState({ ...PLAYER_DEF });
|
||||
const [cells, setCells] = useState(initCells);
|
||||
const [gridReady, setGridReady] = useState(false);
|
||||
const [connectionLost, setConnectionLost] = useState(false);
|
||||
|
||||
return {
|
||||
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,
|
||||
};
|
||||
};
|
||||
|
||||
export default useGameState;
|
||||
566
assets/js/mine-seeker/hooks/useServerCommunication.jsx
Normal file
566
assets/js/mine-seeker/hooks/useServerCommunication.jsx
Normal file
@@ -0,0 +1,566 @@
|
||||
/**
|
||||
* 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, useRef } from 'react';
|
||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||
import { useGame } from '@mine-contexts';
|
||||
import { DESC, IMAGES } from '@mine-utils';
|
||||
import useStepTimer from './useStepTimer';
|
||||
import { ChallengeCountdown, WaitingOverlayContent } from '@mine-components';
|
||||
|
||||
const useServerCommunication = (gameAssoc, gameInherited, opponentName, isEnvDev) => {
|
||||
const {
|
||||
/** Async-safe refs */
|
||||
webPlayerRef, activePlayerRef, bombSelectedRef, connectionLostRef, lastClickedRef, endRef,
|
||||
/** State setters */
|
||||
setCells, setGridReady, setGameUuid,
|
||||
/** Sync helpers */
|
||||
syncWebPlayer, syncActivePlayer, syncBombSelected, syncConnLost, syncRed, syncBlue,
|
||||
/** Game logic */
|
||||
showOverlay, hideOverlay, applyStep, makeGameEndIfItEnds, resignProcess,
|
||||
/** Current cells snapshot (for active-check in onClick) */
|
||||
cells,
|
||||
} = useGame();
|
||||
|
||||
const eventSourceRef = useRef(null);
|
||||
const rpcUsersRef = useRef(null);
|
||||
const stepCacheRef = useRef([]);
|
||||
const lastStepRef = useRef(null);
|
||||
const isGameFinishedRef = useRef(false);
|
||||
const { getStepElapsed, resetStepTimer, startNewTurn } = useStepTimer();
|
||||
const isGameRunningRef = useRef(false);
|
||||
const lastActivePlayerRef = useRef(null);
|
||||
const heartbeatPubIntervalRef = useRef(null);
|
||||
const opponentLastSeenRef = useRef(0);
|
||||
const isTrueRestoredRef = useRef(false);
|
||||
|
||||
const HEARTBEAT_INTERVAL_MS = 1500;
|
||||
|
||||
/** REST mutations / queries */
|
||||
|
||||
const connectQuery = useQuery({
|
||||
queryKey: ['game-connect', gameAssoc],
|
||||
queryFn: () => fetch('/api/game/connect/' + gameAssoc)
|
||||
.then(r => r.text())
|
||||
.then(b64 => JSON.parse(window.atob(b64))),
|
||||
enabled: false,
|
||||
retry: false,
|
||||
});
|
||||
|
||||
const startMutation = useMutation({
|
||||
mutationFn: () => fetch('/api/game/start', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ gameAssoc }),
|
||||
}),
|
||||
});
|
||||
|
||||
const joinMutation = useMutation({
|
||||
mutationFn: () => fetch('/api/game/join/' + gameAssoc, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}).catch(e => isEnvDev && console.error('Join error', e)),
|
||||
});
|
||||
|
||||
const stepMutation = useMutation({
|
||||
mutationFn: dataPack => fetch('/api/game/step/' + gameAssoc, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(dataPack),
|
||||
}).then(r => r.json()),
|
||||
});
|
||||
|
||||
/** Game-start helpers (triggered by server events) */
|
||||
|
||||
const wInit = (revealedCells = [], lastStep = {}, gameState = {}, isGameFinished = false) => {
|
||||
/** Detect if this is a restored game */
|
||||
const isRestoredGame = 0 < revealedCells.length;
|
||||
isTrueRestoredRef.current = isRestoredGame;
|
||||
|
||||
/** Store game finished status */
|
||||
isGameFinishedRef.current = isGameFinished;
|
||||
|
||||
/** Apply game state (points, bonus) immediately for restored games */
|
||||
if (0 < Object.keys(gameState).length) {
|
||||
const {
|
||||
redPoints = 0,
|
||||
bluePoints = 0,
|
||||
redBonusPoints = 0,
|
||||
blueBonusPoints = 0,
|
||||
redBonusStats = {},
|
||||
blueBonusStats = {},
|
||||
} = gameState;
|
||||
syncRed(p => ({
|
||||
...p,
|
||||
mines: redPoints,
|
||||
bonusPoints: redBonusPoints,
|
||||
bonusStats: redBonusStats,
|
||||
}));
|
||||
syncBlue(p => ({
|
||||
...p,
|
||||
mines: bluePoints,
|
||||
bonusPoints: blueBonusPoints,
|
||||
bonusStats: blueBonusStats,
|
||||
}));
|
||||
}
|
||||
|
||||
/** Apply revealed cells immediately (not in setTimeout) */
|
||||
if (0 < revealedCells.length) {
|
||||
setCells(prev => {
|
||||
let next = prev.map(r => [...r]);
|
||||
revealedCells.forEach(({ row, col, value, player }) => {
|
||||
if (next[row][col].active) return;
|
||||
/** Check if this cell is the last step for either player */
|
||||
const isRedLastStep = lastStep.red && lastStep.red.player === player && lastStep.red.row === row && lastStep.red.col === col;
|
||||
const isBlueLastStep = lastStep.blue && lastStep.blue.player === player && lastStep.blue.row === row && lastStep.blue.col === col;
|
||||
const patch = 'm' === value
|
||||
? { currentImage: IMAGES.flag(player), currentObj: 'm', active: true }
|
||||
: { currentImage: value, currentObj: value, active: true };
|
||||
if (isRedLastStep || isBlueLastStep) {
|
||||
patch.lastClickedRed = 'red' === player;
|
||||
patch.lastClickedBlue = 'blue' === player;
|
||||
}
|
||||
next[row][col] = { ...next[row][col], ...patch };
|
||||
});
|
||||
return next;
|
||||
});
|
||||
}
|
||||
|
||||
/** Update the lastClickedRef so applyStep knows about it */
|
||||
if (lastStep.red) {
|
||||
lastClickedRef.current = {
|
||||
...lastClickedRef.current,
|
||||
red: [lastStep.red.row, lastStep.red.col],
|
||||
};
|
||||
}
|
||||
if (lastStep.blue) {
|
||||
lastClickedRef.current = {
|
||||
...lastClickedRef.current,
|
||||
blue: [lastStep.blue.row, lastStep.blue.col],
|
||||
};
|
||||
}
|
||||
|
||||
/** Determine overlay message */
|
||||
let overlayTitle, overlaySubtitle;
|
||||
|
||||
if (isGameFinished) {
|
||||
/** Game is finished - show game over message */
|
||||
const redPoints = gameState.redPoints ?? 0;
|
||||
const bluePoints = gameState.bluePoints ?? 0;
|
||||
const winner = redPoints > bluePoints ? 'Red' : 'Blue';
|
||||
overlayTitle = `${winner} wins the game!`;
|
||||
overlaySubtitle = 'Play again!';
|
||||
/** Mark the game as ended */
|
||||
endRef.current = true;
|
||||
} else if (isRestoredGame) {
|
||||
overlayTitle = 'Waiting for opponent to reconnect...';
|
||||
overlaySubtitle = gameAssoc ? (
|
||||
<WaitingOverlayContent
|
||||
shareUrl={`${window.location.origin}/play/${gameAssoc}`}
|
||||
currentGameAssoc={gameAssoc}
|
||||
opponentName={opponentName}
|
||||
inviteOnly
|
||||
/>
|
||||
) : (
|
||||
<div style={{ textAlign: 'center', padding: '20px' }}>
|
||||
<p>Waiting for opponent to join...</p>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
overlayTitle = 'Choose an opponent!';
|
||||
overlaySubtitle = gameAssoc ? (
|
||||
<WaitingOverlayContent
|
||||
shareUrl={`${window.location.origin}/battle/${gameAssoc}`}
|
||||
currentGameAssoc={gameAssoc}
|
||||
/>
|
||||
) : '';
|
||||
}
|
||||
|
||||
showOverlay(overlayTitle, overlaySubtitle);
|
||||
|
||||
/** Use Promise.resolve to defer setGridReady slightly to ensure overlay is rendered first */
|
||||
Promise.resolve().then(() => setGridReady(true));
|
||||
};
|
||||
|
||||
const makeGameStart = (payload, lastStep = {}) => {
|
||||
/** Don't start a finished game */
|
||||
if (isGameFinishedRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
/** If game is being restored and has a most recent step, determine starter based on that */
|
||||
let starterIsBlue;
|
||||
|
||||
/** lastStepRef contains the single most recent step from the server */
|
||||
if (lastStepRef.current && lastStepRef.current.player) {
|
||||
/** The NEXT player is opposite of who made the last step */
|
||||
starterIsBlue = 'red' === lastStepRef.current.player; // If red played last, blue plays next
|
||||
} else {
|
||||
/** New game: blue always starts */
|
||||
starterIsBlue = true;
|
||||
}
|
||||
|
||||
const starterColor = starterIsBlue ? 'blue' : 'red';
|
||||
const starterVal = starterIsBlue ? 1 : 0;
|
||||
const starterDesc = starterColor === webPlayerRef.current ? DESC.you : DESC.buddy;
|
||||
syncActivePlayer(starterVal);
|
||||
syncRed(p => ({
|
||||
...p,
|
||||
name: payload.users.red || payload.users.redAnon || p.name,
|
||||
registered: !!payload.users.red,
|
||||
avatar: payload.users.redAvatar ?? null,
|
||||
desc: 'red' === starterColor ? starterDesc : '',
|
||||
active: 'red' === starterColor,
|
||||
}));
|
||||
syncBlue(p => ({
|
||||
...p,
|
||||
name: payload.users.blue || payload.users.blueAnon || p.name,
|
||||
registered: !!payload.users.blue,
|
||||
avatar: payload.users.blueAvatar ?? null,
|
||||
desc: 'blue' === starterColor ? starterDesc : '',
|
||||
active: 'blue' === starterColor,
|
||||
}));
|
||||
isGameRunningRef.current = true;
|
||||
lastActivePlayerRef.current = starterVal;
|
||||
startNewTurn();
|
||||
resetStepTimer();
|
||||
/**
|
||||
* For a truly restored game, keep the "Waiting for opponent..." overlay
|
||||
* up until we actually see a heartbeat from the other player.
|
||||
*/
|
||||
if (!isTrueRestoredRef.current || 0 !== opponentLastSeenRef.current) {
|
||||
hideOverlay();
|
||||
}
|
||||
};
|
||||
|
||||
const publishHeartbeat = () => {
|
||||
const me = webPlayerRef.current;
|
||||
if (!me || endRef.current) return;
|
||||
fetch('/api/game/heartbeat/' + gameAssoc, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ color: me }),
|
||||
}).catch(e => isEnvDev && console.warn('Heartbeat publish failed', e));
|
||||
};
|
||||
|
||||
const startHeartbeat = () => {
|
||||
if (heartbeatPubIntervalRef.current) return;
|
||||
publishHeartbeat();
|
||||
heartbeatPubIntervalRef.current = setInterval(publishHeartbeat, HEARTBEAT_INTERVAL_MS);
|
||||
};
|
||||
|
||||
const stopHeartbeat = () => {
|
||||
if (heartbeatPubIntervalRef.current) {
|
||||
clearInterval(heartbeatPubIntervalRef.current);
|
||||
heartbeatPubIntervalRef.current = null;
|
||||
}
|
||||
};
|
||||
|
||||
/** Mercure / SSE message handlers */
|
||||
|
||||
const wSubscribe = (payload, rpcUsers = null, lastStep = null) => {
|
||||
isEnvDev && console.info((payload.user ?? 'user') + ' subscribed');
|
||||
const firstUser = !rpcUsers;
|
||||
|
||||
if (null === webPlayerRef.current) {
|
||||
const isBlue = payload.user === payload.users.blue
|
||||
|| (firstUser ? '' !== payload.users.blueAnon : '' === rpcUsers.blueAnon && '' === rpcUsers.blue);
|
||||
syncWebPlayer(isBlue ? 'blue' : 'red');
|
||||
}
|
||||
|
||||
if (
|
||||
2 === payload.userCnt
|
||||
&& (!connectionLostRef.current
|
||||
|| (connectionLostRef.current && false === activePlayerRef.current && !endRef.current))
|
||||
) {
|
||||
makeGameStart(payload, lastStep);
|
||||
}
|
||||
};
|
||||
|
||||
const wUnsubscribe = payload => {
|
||||
isEnvDev && console.info(payload.msg);
|
||||
const isAuthenticated = '1' === document.getElementById('mine-wrapper')?.dataset.isAuthenticated;
|
||||
const redirectPath = isAuthenticated ? '/profile' : '/';
|
||||
const buttonText = isAuthenticated ? 'My Profile' : 'Homepage';
|
||||
const buttonIcon = isAuthenticated ? 'fa-user' : 'fa-house';
|
||||
|
||||
showOverlay(
|
||||
'The connection has been lost w/ your friend...',
|
||||
(
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '12px', width: '100%' }}>
|
||||
<p style={{ margin: 0 }}>Please, restart the game!</p>
|
||||
<a
|
||||
className="game-overlay-profile"
|
||||
href={redirectPath}
|
||||
title={isAuthenticated ? 'Go to your profile' : 'Go to homepage'}
|
||||
aria-label={isAuthenticated ? 'Go to your profile' : 'Go to homepage'}
|
||||
>
|
||||
<i className={`fa ${buttonIcon}`} />
|
||||
{buttonText}
|
||||
</a>
|
||||
</div>
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
const wChallenge = payload => {
|
||||
const { challengerName, challengerGameAssoc } = payload;
|
||||
let declineTimeout = null;
|
||||
|
||||
const handleAccept = () => {
|
||||
clearTimeout(declineTimeout);
|
||||
fetch('/api/game/challenge/respond/' + challengerGameAssoc, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ accepted: true, targetGameAssoc: gameAssoc }),
|
||||
}).then(() => {
|
||||
showOverlay('Challenge accepted!', 'Waiting for the challenger to join...');
|
||||
}).catch(() => {
|
||||
});
|
||||
};
|
||||
|
||||
const handleDecline = () => {
|
||||
clearTimeout(declineTimeout);
|
||||
fetch('/api/game/challenge/respond/' + challengerGameAssoc, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ accepted: false, targetGameAssoc: gameAssoc }),
|
||||
}).then(() => {
|
||||
showOverlay('We are waiting for your opponent...', gameAssoc ? (
|
||||
<WaitingOverlayContent
|
||||
shareUrl={window.location.origin + '/play/' + gameAssoc}
|
||||
currentGameAssoc={gameAssoc}
|
||||
/>
|
||||
) : '');
|
||||
}).catch(() => {
|
||||
});
|
||||
};
|
||||
|
||||
declineTimeout = setTimeout(handleDecline, 30000);
|
||||
|
||||
showOverlay(
|
||||
challengerName + ' wants to challenge you!',
|
||||
<ChallengeCountdown onAccept={handleAccept} onDecline={handleDecline} />,
|
||||
);
|
||||
};
|
||||
|
||||
const wChallengeResponse = payload => {
|
||||
if (payload.accepted) {
|
||||
window.location.href = '/play/' + payload.targetGameAssoc;
|
||||
} else {
|
||||
window.dispatchEvent(new CustomEvent('challenge-declined'));
|
||||
}
|
||||
};
|
||||
|
||||
const wTopic = payload => {
|
||||
if (webPlayerRef.current !== payload.data.player) {
|
||||
if (null === payload.data.resign) {
|
||||
isEnvDev && console.warn(payload.user + ' stepped to ' + payload.data.coords.join(','));
|
||||
syncBombSelected(payload.data.bomb);
|
||||
|
||||
/**
|
||||
* Detect if turn switched (other player made a move)
|
||||
* After their move, it's now our turn (or the opposite player's turn)
|
||||
*/
|
||||
if (lastActivePlayerRef.current !== activePlayerRef.current) {
|
||||
startNewTurn();
|
||||
lastActivePlayerRef.current = activePlayerRef.current;
|
||||
}
|
||||
|
||||
applyStep(payload.data);
|
||||
if (payload.data.uuid && !endRef.current) {
|
||||
setGameUuid(payload.data.uuid);
|
||||
}
|
||||
makeGameEndIfItEnds(payload.data.bluePoints, payload.data.redPoints, false, payload.data.leftMines);
|
||||
} else {
|
||||
resignProcess(payload.data.resign, payload.data.uuid);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleMercureMessage = payload => {
|
||||
if (undefined !== payload.type) {
|
||||
if ('challenge' === payload.type) wChallenge(payload);
|
||||
else if ('challenge-response' === payload.type) wChallengeResponse(payload);
|
||||
else if ('heartbeat' === payload.type) {
|
||||
const me = webPlayerRef.current;
|
||||
if (me && payload.color && payload.color !== me) {
|
||||
const wasFirst = 0 === opponentLastSeenRef.current;
|
||||
opponentLastSeenRef.current = Date.now();
|
||||
if (wasFirst && isTrueRestoredRef.current) {
|
||||
hideOverlay();
|
||||
}
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (undefined !== payload.data) {
|
||||
wTopic(payload);
|
||||
} else if (undefined === payload.msg) {
|
||||
wSubscribe(payload, rpcUsersRef.current, lastStepRef.current);
|
||||
} else {
|
||||
wUnsubscribe(payload);
|
||||
}
|
||||
|
||||
if (2 === payload.userCnt && connectionLostRef.current) {
|
||||
isEnvDev && console.info('Reconnection');
|
||||
stepCacheRef.current.forEach(item => stepMutation.mutate(item));
|
||||
stepCacheRef.current = [];
|
||||
syncConnLost(false);
|
||||
}
|
||||
};
|
||||
|
||||
const openEventSource = () => {
|
||||
const wrapper = document.getElementById('mine-wrapper');
|
||||
const hubUrl = wrapper.dataset.mercureHubUrl;
|
||||
const subscriberJwt = wrapper.dataset.mercureSubscriberJwt;
|
||||
const url = new URL(hubUrl, window.location.origin);
|
||||
|
||||
url.searchParams.append('topic', 'mineseeker/channel/' + gameAssoc);
|
||||
|
||||
if (subscriberJwt) url.searchParams.append('authorization', subscriberJwt);
|
||||
if (eventSourceRef.current) eventSourceRef.current.close();
|
||||
|
||||
const es = new EventSource(url.toString());
|
||||
es.onmessage = e => handleMercureMessage(JSON.parse(e.data));
|
||||
es.onopen = () => {
|
||||
isEnvDev && console.info('SSE opened');
|
||||
if (connectionLostRef.current) {
|
||||
isEnvDev && console.info('SSE reconnected');
|
||||
joinMutation.mutate();
|
||||
}
|
||||
};
|
||||
es.onerror = () => {
|
||||
isEnvDev && console.error('SSE error');
|
||||
syncConnLost(true);
|
||||
};
|
||||
eventSourceRef.current = es;
|
||||
};
|
||||
|
||||
/** Initialization */
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
if (connectionLostRef.current) {
|
||||
openEventSource();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (gameInherited) {
|
||||
const serverData = await connectQuery.refetch().then(r => {
|
||||
if (r.error) throw r.error;
|
||||
return r.data;
|
||||
});
|
||||
|
||||
if ('undefined' === typeof serverData.users || null === serverData.users) {
|
||||
showOverlay('This channel does not exists!', <a href="/play" target="_self">Restart game!</a>);
|
||||
console.error('This channel does not exists!');
|
||||
return;
|
||||
}
|
||||
|
||||
rpcUsersRef.current = serverData.users;
|
||||
lastStepRef.current = serverData.mostRecentStep || null;
|
||||
|
||||
/** Pass game state (points, bonus) to wInit */
|
||||
const gameState = {
|
||||
redPoints: serverData.redPoints ?? 0,
|
||||
bluePoints: serverData.bluePoints ?? 0,
|
||||
redBonusPoints: serverData.redBonusPoints ?? 0,
|
||||
blueBonusPoints: serverData.blueBonusPoints ?? 0,
|
||||
redBonusStats: serverData.redBonusStats ?? {},
|
||||
blueBonusStats: serverData.blueBonusStats ?? {},
|
||||
};
|
||||
const isGameFinished = serverData.gameFinished ?? false;
|
||||
wInit(serverData.revealedCells || [], serverData.lastStep || {}, gameState, isGameFinished);
|
||||
|
||||
/** Open event source after showing overlay */
|
||||
openEventSource();
|
||||
} else {
|
||||
await startMutation.mutateAsync();
|
||||
openEventSource();
|
||||
wInit();
|
||||
}
|
||||
|
||||
isEnvDev && console.info('Connection initialised — joining channel');
|
||||
await joinMutation.mutateAsync();
|
||||
startHeartbeat();
|
||||
} catch (e) {
|
||||
isEnvDev && console.error('Connection error', e);
|
||||
setTimeout(() => window.location.reload(), 500);
|
||||
}
|
||||
})();
|
||||
|
||||
window.addEventListener('pagehide', () => navigator.sendBeacon('/api/game/leave/' + gameAssoc));
|
||||
|
||||
return () => {
|
||||
stopHeartbeat();
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
/** UI-facing callbacks */
|
||||
|
||||
const onClick = async coords => {
|
||||
const activeColor = activePlayerRef.current ? 'blue' : 'red';
|
||||
if (activeColor !== webPlayerRef.current) return;
|
||||
|
||||
const [r, c] = coords;
|
||||
if (cells[r]?.[c]?.active) return;
|
||||
|
||||
const stepElapsed = getStepElapsed(activePlayerRef.current, isGameRunningRef.current);
|
||||
const dataPack = { coords, player: activeColor, bomb: bombSelectedRef.current, resign: null, stepElapsed };
|
||||
|
||||
if (connectionLostRef.current) {
|
||||
stepCacheRef.current.push(dataPack);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await stepMutation.mutateAsync(dataPack);
|
||||
applyStep(result);
|
||||
if (result.uuid && !endRef.current) {
|
||||
setGameUuid(result.uuid);
|
||||
}
|
||||
makeGameEndIfItEnds(result.bluePoints, result.redPoints, false, result.leftMines);
|
||||
} catch (e) {
|
||||
isEnvDev && console.error('Step error', e);
|
||||
}
|
||||
};
|
||||
|
||||
const clickResign = () => {
|
||||
const color = activePlayerRef.current ? 'blue' : 'red';
|
||||
const stepElapsed = getStepElapsed(activePlayerRef.current, isGameRunningRef.current);
|
||||
stepMutation.mutate(
|
||||
{ resign: color, stepElapsed },
|
||||
{
|
||||
onSuccess: result => {
|
||||
if (result?.uuid && !endRef.current) {
|
||||
resignProcess(webPlayerRef.current, result.uuid);
|
||||
}
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const resign = () => {
|
||||
const activeColor = activePlayerRef.current ? 'blue' : 'red';
|
||||
if (webPlayerRef.current !== activeColor) return;
|
||||
showOverlay('Are u sure u want to resign?!', (
|
||||
<div className="resign">
|
||||
<a onClick={clickResign}>Yes</a>
|
||||
<a onClick={hideOverlay}>No!</a>
|
||||
</div>
|
||||
));
|
||||
};
|
||||
|
||||
return { onClick, resign };
|
||||
};
|
||||
|
||||
export default useServerCommunication;
|
||||
53
assets/js/mine-seeker/hooks/useStepTimer.jsx
Normal file
53
assets/js/mine-seeker/hooks/useStepTimer.jsx
Normal file
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* 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 { useRef } from 'react';
|
||||
|
||||
const useStepTimer = () => {
|
||||
// Record when the current turn started (timestamp)
|
||||
const turnStartTimeRef = useRef(null);
|
||||
// Flag to track if we've already recorded a turn start
|
||||
const turnStartedRef = useRef(false);
|
||||
|
||||
const getStepElapsed = (currentActivePlayer, isGameRunning) => {
|
||||
// If game not running, return 0
|
||||
if (!isGameRunning) return 0;
|
||||
|
||||
// Only initialize the turn timer ONCE per call to getStepElapsed
|
||||
// This prevents resetting on multiple calls
|
||||
if (!turnStartedRef.current) {
|
||||
turnStartTimeRef.current = Date.now();
|
||||
turnStartedRef.current = true;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// After initialization, just calculate elapsed time
|
||||
if (turnStartTimeRef.current) {
|
||||
return Math.floor((Date.now() - turnStartTimeRef.current) / 1000);
|
||||
}
|
||||
|
||||
return 0;
|
||||
};
|
||||
|
||||
const resetStepTimer = () => {
|
||||
turnStartTimeRef.current = null;
|
||||
turnStartedRef.current = false;
|
||||
};
|
||||
|
||||
// Call this when we know a turn has actually changed (from server response)
|
||||
const startNewTurn = () => {
|
||||
turnStartTimeRef.current = Date.now();
|
||||
turnStartedRef.current = true;
|
||||
};
|
||||
|
||||
return { getStepElapsed, resetStepTimer, startNewTurn };
|
||||
};
|
||||
|
||||
export default useStepTimer;
|
||||
|
||||
102
assets/js/mine-seeker/utils/constants.jsx
Normal file
102
assets/js/mine-seeker/utils/constants.jsx
Normal file
@@ -0,0 +1,102 @@
|
||||
/**
|
||||
* 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';
|
||||
|
||||
export const ROWS = 16;
|
||||
export const COLS = 16;
|
||||
export const IMG = '/images/';
|
||||
|
||||
export const WAVES = {
|
||||
1: 'bg-wave-1-outbg.png',
|
||||
2: 'bg-wave-1-outbg.png',
|
||||
3: 'bg-wave-2-outbg.png',
|
||||
};
|
||||
|
||||
export const IMAGES = {
|
||||
target: `${IMG}bg-target-outbg.png`,
|
||||
bomb: `${IMG}bg-bomb-outbg.png`,
|
||||
bombDisabled: `${IMG}bg-bomb-disabled-outbg.png`,
|
||||
bombExploded: `${IMG}bg-bomb-exploded-outbg.png`,
|
||||
bombEmpty: `${IMG}bg-bomb-empty-outbg.png`,
|
||||
leftMine: `${IMG}bg-left-mine-outbg.png`,
|
||||
cursor: color => `${IMG}bg-cursor-${color}-outbg.png`,
|
||||
figure: color => `${IMG}bg-figure-${color}-outbg.png`,
|
||||
flag: player => `${IMG}bg-flag-${player}-outbg.png`,
|
||||
last: color => `${IMG}bg-last-${color}-outbg.png`,
|
||||
wave: n => `${IMG}${WAVES[n]}`,
|
||||
bombPos: (hor, vert) => `${IMG}bg-bomb-${hor}-${vert}-outbg.png`,
|
||||
};
|
||||
|
||||
export const BONUS_STATS_DEF = {
|
||||
blindHits: 0, chainBest: 0, chainCurrent: 0, lastMineHits: 0, edgeMines: 0, biggestReveal: 0,
|
||||
};
|
||||
|
||||
export const BONUS_LABELS = {
|
||||
blindHits: { label: 'Blind hits', desc: 'Mines clicked with no revealed number nearby' },
|
||||
chainBest: { label: 'Best chain', desc: 'Longest streak of consecutive mine-clicks' },
|
||||
chainCurrent: { label: 'Current chain', desc: 'Active consecutive mine-click streak' },
|
||||
lastMineHits: { label: 'Endgame mines', desc: 'Mines clicked while few remain on the board' },
|
||||
edgeMines: { label: 'Edge mines', desc: 'Mines clicked on the board boundary' },
|
||||
biggestReveal: { label: 'Biggest reveal', desc: 'Largest number of safe cells revealed in one click' },
|
||||
};
|
||||
|
||||
export const PLAYER_DEF = {
|
||||
name: '...', desc: '', active: false, mines: 0, haveBomb: true, enabledBomb: true,
|
||||
registered: false, avatar: null,
|
||||
bonusPoints: 0, bonusStats: { ...BONUS_STATS_DEF },
|
||||
};
|
||||
|
||||
export const DESC = {
|
||||
buddy: <div>Your buddy is <br />making a <br />move.</div>,
|
||||
you: <div>It is your turn! <br />Make a move.</div>,
|
||||
};
|
||||
|
||||
export const BOMB_SYMBOLS = [
|
||||
[null, null], [0, 0], [1, 0], [2, 0], [0, 1], [2, 1], [0, 2], [1, 2], [2, 2],
|
||||
[null, null], [null, null], [null, null], [null, null], [null, null], [null, null], [null, null],
|
||||
[null, null], [null, null], [null, null], [null, null], [null, null], [null, null], [null, null],
|
||||
[null, null], [null, null],
|
||||
];
|
||||
|
||||
export const bombRadius = (row, col, rows, cols) => {
|
||||
const centre = 1 < row && row < rows - 2 && 1 < col && col < cols - 2;
|
||||
if (!centre) {
|
||||
col = Math.max(2, Math.min(col, cols - 3));
|
||||
row = Math.max(2, Math.min(row, rows - 3));
|
||||
}
|
||||
return [
|
||||
[row, col], [row - 2, col - 2], [row - 2, col], [row - 2, col + 2],
|
||||
[row, col - 2], [row, col + 2], [row + 2, col - 2], [row + 2, col],
|
||||
[row + 2, col + 2], [row - 2, col + 1], [row - 2, col - 1],
|
||||
[row - 1, col - 2], [row - 1, col - 1], [row - 1, col], [row - 1, col + 1], [row - 1, col + 2],
|
||||
[row, col - 1], [row, col + 1], [row + 1, col - 2], [row + 1, col - 1], [row + 1, col],
|
||||
[row + 1, col + 1], [row + 1, col + 2], [row + 2, col - 1], [row + 2, col + 1],
|
||||
];
|
||||
};
|
||||
|
||||
export const patchCells = (prev, patches) => {
|
||||
const next = prev.map(r => [...r]);
|
||||
for (const { row, col, ...rest } of patches) {
|
||||
next[row][col] = { ...next[row][col], ...rest };
|
||||
}
|
||||
return next;
|
||||
};
|
||||
|
||||
export const initCells = () =>
|
||||
Array.from({ length: ROWS }, () =>
|
||||
Array.from({ length: COLS }, () => ({
|
||||
currentImage: IMAGES.wave(Math.floor(Math.random() * 3) + 1),
|
||||
currentObj: 'w',
|
||||
active: false,
|
||||
lastClickedRed: false,
|
||||
lastClickedBlue: false,
|
||||
bombTargetArea: null,
|
||||
})),
|
||||
);
|
||||
10
assets/js/mine-seeker/utils/index.js
Normal file
10
assets/js/mine-seeker/utils/index.js
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.
|
||||
*/
|
||||
|
||||
export { DESC, IMAGES, BOMB_SYMBOLS, PLAYER_DEF, BONUS_STATS_DEF, BONUS_LABELS, bombRadius, initCells, patchCells } from './constants';
|
||||
37
assets/js/passkey.jsx
Normal file
37
assets/js/passkey.jsx
Normal file
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* 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 PasskeyManager from './components/PasskeyManager';
|
||||
import PasskeyLogin from './components/PasskeyLogin';
|
||||
|
||||
const passkeyManagerRoot = document.getElementById('passkey-manager-root');
|
||||
|
||||
if (passkeyManagerRoot) {
|
||||
const credentials = JSON.parse(passkeyManagerRoot.dataset.credentials || '[]');
|
||||
const apiRoutes = JSON.parse(passkeyManagerRoot.dataset.apiRoutes || '{}');
|
||||
|
||||
createRoot(passkeyManagerRoot).render(
|
||||
<PasskeyManager
|
||||
credentials={credentials}
|
||||
apiRoutes={apiRoutes}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
|
||||
const passkeyLoginRoot = document.getElementById('passkey-login-root');
|
||||
|
||||
if (passkeyLoginRoot) {
|
||||
const apiRoutes = JSON.parse(passkeyLoginRoot.dataset.apiRoutes || '{}');
|
||||
|
||||
createRoot(passkeyLoginRoot).render(
|
||||
<PasskeyLogin apiRoutes={apiRoutes} />,
|
||||
);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user