Compare commits
144 Commits
0.4.0
...
v2026.2.8-
| Author | SHA1 | Date | |
|---|---|---|---|
| ddfa395c6b | |||
| 8694627817 | |||
| f796819af4 | |||
| b209ad4220 | |||
| df1eefdfe0 | |||
| 7aaaf120b2 | |||
| e48d651eb5 | |||
| d704be5bff | |||
| 6bf908b43e | |||
| 085e010907 | |||
| 8935216525 | |||
| 1d8efa4e61 | |||
| 69fce52bed | |||
| 13adf908bf | |||
| 3bbfb8740f | |||
| 0d04ec91e7 | |||
| 20a969705d | |||
| 4944d2aa21 | |||
| 2ec37a802b | |||
| 6a5ba84b5e | |||
| 6be0d52fb7 | |||
| f493f94368 | |||
| cd93a26c2c | |||
| 175581cdd5 | |||
| 5f856e4d70 | |||
| e0495d182e | |||
| 0b7c1406cf | |||
| 30edc5782b | |||
| d92a7f3aa0 | |||
| f72cd45afd | |||
| 51bd909879 | |||
| db37ab45b2 | |||
| 9256db7f8c | |||
| d9059acb78 | |||
| 5da8a04c18 | |||
| ba8a0befb0 | |||
| 5ac291de81 | |||
| 991b114a3c | |||
| c79584c7d2 | |||
| e77c8a8f7c | |||
| c2308ba408 | |||
| e5a22cdfe3 | |||
| 09b0d21621 | |||
| 9aef27a0eb | |||
| c00ed57240 | |||
| ef4cf6ef69 | |||
| dc9c5f6545 | |||
| 25f2aaab8c | |||
| 0cc9cdaf07 | |||
| 247f437445 | |||
| 0e94367223 | |||
| a9ee28b395 | |||
| bd074c5c9d | |||
| 42c552c528 | |||
| 3b376e5386 | |||
| 45a8e6b4a1 | |||
| 1f8e9c3c56 | |||
| a511b86db8 | |||
| 1c0ad054bb | |||
| 5a8799bb7f | |||
| 6c443d8e86 | |||
| 8795fedda9 | |||
| 588fb57299 | |||
| eb345e17ca | |||
| c2693c4648 | |||
| 43efc16562 | |||
| 80d6440ece | |||
| 5ee972f003 | |||
| 6f3edb41ea | |||
| c52939a7a3 | |||
| 573d409606 | |||
| 9a58bc9a5e | |||
| 8780800dff | |||
| f442942faf | |||
| a61d881a4e | |||
| 926b614136 | |||
| c0c84f4651 | |||
| 176e255037 | |||
| b134358e9e | |||
| 3525aaeeb7 | |||
| af67ec3931 | |||
| d515f42cfd | |||
| 5d6aff8d90 | |||
| 15ba26ccf2 | |||
| d3fa0cbbf9 | |||
| ed776ca75b | |||
| 67133a4efa | |||
| 6044ee5c50 | |||
| 184925ab13 | |||
| 5a5467fda9 | |||
| f135746826 | |||
| 62915fe5e4 | |||
| 68a25aafa4 | |||
| 9fff5bd5d1 | |||
| 304663adb7 | |||
| cd593e99fc | |||
| 9d51654aec | |||
| 82465322f2 | |||
| 055e59d896 | |||
| 3db8a30115 | |||
| 2e8d878337 | |||
| 28221e092a | |||
| 0c0b8ae920 | |||
| 98f6e8cb6e | |||
| 4239177563 | |||
| e9c6795eb7 | |||
| fb8a54f687 | |||
| 0144a3953c | |||
| acbe9c7f63 | |||
| e2b227ed7a | |||
| c0dcc2896a | |||
| 92bfa5b301 | |||
| 826690769f | |||
| 6b3e19b063 | |||
| eff849549b | |||
| 63a0f442ed | |||
| 1cf21622b8 | |||
| 85a440017c | |||
| d388e25192 | |||
| 5b55a6ce73 | |||
| c660c13ea2 | |||
| d186a96f0d | |||
| b57442bec1 | |||
| 086d6c601e | |||
| 15806a6e04 | |||
| fe2de91e91 | |||
| 76143fca3e | |||
| 7219471a86 | |||
| b55c223d8a | |||
| 23547f4237 | |||
| 4cdca43ecc | |||
| fa0fc0743d | |||
| dd4b410624 | |||
| 1b5e87c033 | |||
| 2daab7140e | |||
| 8355cc90ed | |||
| c909c036a3 | |||
| 3bbc96c76c | |||
| 6caf340302 | |||
| 98b71d75e9 | |||
| aae8b9ebec | |||
| 9ef0711acc | |||
| 5afc237ffb | |||
| 13749186fb |
13
.dockerignore
Normal file
13
.dockerignore
Normal file
@@ -0,0 +1,13 @@
|
||||
.git
|
||||
.gitignore
|
||||
node_modules
|
||||
var/cache
|
||||
var/log
|
||||
var/sessions
|
||||
public/build
|
||||
*.md
|
||||
docker-compose*.yml
|
||||
compose.override.yaml
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
884
.editorconfig
Normal file
884
.editorconfig
Normal file
@@ -0,0 +1,884 @@
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
indent_size = 2
|
||||
indent_style = space
|
||||
insert_final_newline = true
|
||||
max_line_length = 120
|
||||
tab_width = 2
|
||||
trim_trailing_whitespace = true
|
||||
ij_continuation_indent_size = 8
|
||||
ij_formatter_off_tag = @formatter:off
|
||||
ij_formatter_on_tag = @formatter:on
|
||||
ij_formatter_tags_enabled = true
|
||||
ij_smart_tabs = false
|
||||
ij_visual_guides = 80,120
|
||||
ij_wrap_on_typing = false
|
||||
|
||||
[*.blade.php]
|
||||
ij_visual_guides =
|
||||
ij_blade_keep_indents_on_empty_lines = false
|
||||
|
||||
[*.css]
|
||||
indent_size = 2
|
||||
tab_width = 2
|
||||
ij_continuation_indent_size = 4
|
||||
ij_visual_guides =
|
||||
ij_css_align_closing_brace_with_properties = false
|
||||
ij_css_blank_lines_around_nested_selector = 1
|
||||
ij_css_blank_lines_between_blocks = 1
|
||||
ij_css_block_comment_add_space = false
|
||||
ij_css_brace_placement = end_of_line
|
||||
ij_css_enforce_quotes_on_format = false
|
||||
ij_css_hex_color_long_format = false
|
||||
ij_css_hex_color_lower_case = false
|
||||
ij_css_hex_color_short_format = false
|
||||
ij_css_hex_color_upper_case = false
|
||||
ij_css_keep_blank_lines_in_code = 2
|
||||
ij_css_keep_indents_on_empty_lines = false
|
||||
ij_css_keep_single_line_blocks = false
|
||||
ij_css_properties_order = font,font-family,font-size,font-weight,font-style,font-variant,font-size-adjust,font-stretch,line-height,position,z-index,top,right,bottom,left,display,visibility,float,clear,overflow,overflow-x,overflow-y,clip,zoom,align-content,align-items,align-self,flex,flex-flow,flex-basis,flex-direction,flex-grow,flex-shrink,flex-wrap,justify-content,order,box-sizing,width,min-width,max-width,height,min-height,max-height,margin,margin-top,margin-right,margin-bottom,margin-left,padding,padding-top,padding-right,padding-bottom,padding-left,table-layout,empty-cells,caption-side,border-spacing,border-collapse,list-style,list-style-position,list-style-type,list-style-image,content,quotes,counter-reset,counter-increment,resize,cursor,user-select,nav-index,nav-up,nav-right,nav-down,nav-left,transition,transition-delay,transition-timing-function,transition-duration,transition-property,transform,transform-origin,animation,animation-name,animation-duration,animation-play-state,animation-timing-function,animation-delay,animation-iteration-count,animation-direction,text-align,text-align-last,vertical-align,white-space,text-decoration,text-emphasis,text-emphasis-color,text-emphasis-style,text-emphasis-position,text-indent,text-justify,letter-spacing,word-spacing,text-outline,text-transform,text-wrap,text-overflow,text-overflow-ellipsis,text-overflow-mode,word-wrap,word-break,tab-size,hyphens,pointer-events,opacity,color,border,border-width,border-style,border-color,border-top,border-top-width,border-top-style,border-top-color,border-right,border-right-width,border-right-style,border-right-color,border-bottom,border-bottom-width,border-bottom-style,border-bottom-color,border-left,border-left-width,border-left-style,border-left-color,border-radius,border-top-left-radius,border-top-right-radius,border-bottom-right-radius,border-bottom-left-radius,border-image,border-image-source,border-image-slice,border-image-width,border-image-outset,border-image-repeat,outline,outline-width,outline-style,outline-color,outline-offset,background,background-color,background-image,background-repeat,background-attachment,background-position,background-position-x,background-position-y,background-clip,background-origin,background-size,box-decoration-break,box-shadow,text-shadow
|
||||
ij_css_space_after_colon = true
|
||||
ij_css_space_before_opening_brace = true
|
||||
ij_css_use_double_quotes = true
|
||||
ij_css_value_alignment = do_not_align
|
||||
|
||||
[*.feature]
|
||||
indent_size = 2
|
||||
ij_visual_guides =
|
||||
ij_gherkin_keep_indents_on_empty_lines = false
|
||||
|
||||
[*.less]
|
||||
indent_size = 2
|
||||
ij_visual_guides =
|
||||
ij_less_align_closing_brace_with_properties = false
|
||||
ij_less_blank_lines_around_nested_selector = 1
|
||||
ij_less_blank_lines_between_blocks = 1
|
||||
ij_less_block_comment_add_space = false
|
||||
ij_less_brace_placement = 0
|
||||
ij_less_enforce_quotes_on_format = false
|
||||
ij_less_hex_color_long_format = false
|
||||
ij_less_hex_color_lower_case = false
|
||||
ij_less_hex_color_short_format = false
|
||||
ij_less_hex_color_upper_case = false
|
||||
ij_less_keep_blank_lines_in_code = 2
|
||||
ij_less_keep_indents_on_empty_lines = false
|
||||
ij_less_keep_single_line_blocks = false
|
||||
ij_less_line_comment_add_space = false
|
||||
ij_less_line_comment_at_first_column = false
|
||||
ij_less_properties_order = font,font-family,font-size,font-weight,font-style,font-variant,font-size-adjust,font-stretch,line-height,position,z-index,top,right,bottom,left,display,visibility,float,clear,overflow,overflow-x,overflow-y,clip,zoom,align-content,align-items,align-self,flex,flex-flow,flex-basis,flex-direction,flex-grow,flex-shrink,flex-wrap,justify-content,order,box-sizing,width,min-width,max-width,height,min-height,max-height,margin,margin-top,margin-right,margin-bottom,margin-left,padding,padding-top,padding-right,padding-bottom,padding-left,table-layout,empty-cells,caption-side,border-spacing,border-collapse,list-style,list-style-position,list-style-type,list-style-image,content,quotes,counter-reset,counter-increment,resize,cursor,user-select,nav-index,nav-up,nav-right,nav-down,nav-left,transition,transition-delay,transition-timing-function,transition-duration,transition-property,transform,transform-origin,animation,animation-name,animation-duration,animation-play-state,animation-timing-function,animation-delay,animation-iteration-count,animation-direction,text-align,text-align-last,vertical-align,white-space,text-decoration,text-emphasis,text-emphasis-color,text-emphasis-style,text-emphasis-position,text-indent,text-justify,letter-spacing,word-spacing,text-outline,text-transform,text-wrap,text-overflow,text-overflow-ellipsis,text-overflow-mode,word-wrap,word-break,tab-size,hyphens,pointer-events,opacity,color,border,border-width,border-style,border-color,border-top,border-top-width,border-top-style,border-top-color,border-right,border-right-width,border-right-style,border-right-color,border-bottom,border-bottom-width,border-bottom-style,border-bottom-color,border-left,border-left-width,border-left-style,border-left-color,border-radius,border-top-left-radius,border-top-right-radius,border-bottom-right-radius,border-bottom-left-radius,border-image,border-image-source,border-image-slice,border-image-width,border-image-outset,border-image-repeat,outline,outline-width,outline-style,outline-color,outline-offset,background,background-color,background-image,background-repeat,background-attachment,background-position,background-position-x,background-position-y,background-clip,background-origin,background-size,box-decoration-break,box-shadow,text-shadow
|
||||
ij_less_space_after_colon = true
|
||||
ij_less_space_before_opening_brace = true
|
||||
ij_less_use_double_quotes = true
|
||||
ij_less_value_alignment = 0
|
||||
|
||||
[*.neon]
|
||||
ij_visual_guides =
|
||||
|
||||
[*.sass]
|
||||
indent_size = 2
|
||||
ij_visual_guides =
|
||||
ij_sass_align_closing_brace_with_properties = false
|
||||
ij_sass_blank_lines_around_nested_selector = 1
|
||||
ij_sass_blank_lines_between_blocks = 1
|
||||
ij_sass_brace_placement = 0
|
||||
ij_sass_enforce_quotes_on_format = false
|
||||
ij_sass_hex_color_long_format = false
|
||||
ij_sass_hex_color_lower_case = false
|
||||
ij_sass_hex_color_short_format = false
|
||||
ij_sass_hex_color_upper_case = false
|
||||
ij_sass_keep_blank_lines_in_code = 2
|
||||
ij_sass_keep_indents_on_empty_lines = false
|
||||
ij_sass_keep_single_line_blocks = false
|
||||
ij_sass_line_comment_add_space = false
|
||||
ij_sass_line_comment_at_first_column = false
|
||||
ij_sass_properties_order = font,font-family,font-size,font-weight,font-style,font-variant,font-size-adjust,font-stretch,line-height,position,z-index,top,right,bottom,left,display,visibility,float,clear,overflow,overflow-x,overflow-y,clip,zoom,align-content,align-items,align-self,flex,flex-flow,flex-basis,flex-direction,flex-grow,flex-shrink,flex-wrap,justify-content,order,box-sizing,width,min-width,max-width,height,min-height,max-height,margin,margin-top,margin-right,margin-bottom,margin-left,padding,padding-top,padding-right,padding-bottom,padding-left,table-layout,empty-cells,caption-side,border-spacing,border-collapse,list-style,list-style-position,list-style-type,list-style-image,content,quotes,counter-reset,counter-increment,resize,cursor,user-select,nav-index,nav-up,nav-right,nav-down,nav-left,transition,transition-delay,transition-timing-function,transition-duration,transition-property,transform,transform-origin,animation,animation-name,animation-duration,animation-play-state,animation-timing-function,animation-delay,animation-iteration-count,animation-direction,text-align,text-align-last,vertical-align,white-space,text-decoration,text-emphasis,text-emphasis-color,text-emphasis-style,text-emphasis-position,text-indent,text-justify,letter-spacing,word-spacing,text-outline,text-transform,text-wrap,text-overflow,text-overflow-ellipsis,text-overflow-mode,word-wrap,word-break,tab-size,hyphens,pointer-events,opacity,color,border,border-width,border-style,border-color,border-top,border-top-width,border-top-style,border-top-color,border-right,border-right-width,border-right-style,border-right-color,border-bottom,border-bottom-width,border-bottom-style,border-bottom-color,border-left,border-left-width,border-left-style,border-left-color,border-radius,border-top-left-radius,border-top-right-radius,border-bottom-right-radius,border-bottom-left-radius,border-image,border-image-source,border-image-slice,border-image-width,border-image-outset,border-image-repeat,outline,outline-width,outline-style,outline-color,outline-offset,background,background-color,background-image,background-repeat,background-attachment,background-position,background-position-x,background-position-y,background-clip,background-origin,background-size,box-decoration-break,box-shadow,text-shadow
|
||||
ij_sass_space_after_colon = true
|
||||
ij_sass_space_before_opening_brace = true
|
||||
ij_sass_use_double_quotes = true
|
||||
ij_sass_value_alignment = 0
|
||||
|
||||
[*.scss]
|
||||
indent_size = 2
|
||||
tab_width = 2
|
||||
ij_continuation_indent_size = 2
|
||||
ij_visual_guides =
|
||||
ij_scss_align_closing_brace_with_properties = false
|
||||
ij_scss_blank_lines_around_nested_selector = 1
|
||||
ij_scss_blank_lines_between_blocks = 1
|
||||
ij_scss_block_comment_add_space = false
|
||||
ij_scss_brace_placement = 0
|
||||
ij_scss_enforce_quotes_on_format = false
|
||||
ij_scss_hex_color_long_format = false
|
||||
ij_scss_hex_color_lower_case = false
|
||||
ij_scss_hex_color_short_format = false
|
||||
ij_scss_hex_color_upper_case = false
|
||||
ij_scss_keep_blank_lines_in_code = 2
|
||||
ij_scss_keep_indents_on_empty_lines = false
|
||||
ij_scss_keep_single_line_blocks = false
|
||||
ij_scss_line_comment_add_space = false
|
||||
ij_scss_line_comment_at_first_column = false
|
||||
ij_scss_properties_order = font,font-family,font-size,font-weight,font-style,font-variant,font-size-adjust,font-stretch,line-height,position,z-index,top,right,bottom,left,display,visibility,float,clear,overflow,overflow-x,overflow-y,clip,zoom,align-content,align-items,align-self,flex,flex-flow,flex-basis,flex-direction,flex-grow,flex-shrink,flex-wrap,justify-content,order,box-sizing,width,min-width,max-width,height,min-height,max-height,margin,margin-top,margin-right,margin-bottom,margin-left,padding,padding-top,padding-right,padding-bottom,padding-left,table-layout,empty-cells,caption-side,border-spacing,border-collapse,list-style,list-style-position,list-style-type,list-style-image,content,quotes,counter-reset,counter-increment,resize,cursor,user-select,nav-index,nav-up,nav-right,nav-down,nav-left,transition,transition-delay,transition-timing-function,transition-duration,transition-property,transform,transform-origin,animation,animation-name,animation-duration,animation-play-state,animation-timing-function,animation-delay,animation-iteration-count,animation-direction,text-align,text-align-last,vertical-align,white-space,text-decoration,text-emphasis,text-emphasis-color,text-emphasis-style,text-emphasis-position,text-indent,text-justify,letter-spacing,word-spacing,text-outline,text-transform,text-wrap,text-overflow,text-overflow-ellipsis,text-overflow-mode,word-wrap,word-break,tab-size,hyphens,pointer-events,opacity,color,border,border-width,border-style,border-color,border-top,border-top-width,border-top-style,border-top-color,border-right,border-right-width,border-right-style,border-right-color,border-bottom,border-bottom-width,border-bottom-style,border-bottom-color,border-left,border-left-width,border-left-style,border-left-color,border-radius,border-top-left-radius,border-top-right-radius,border-bottom-right-radius,border-bottom-left-radius,border-image,border-image-source,border-image-slice,border-image-width,border-image-outset,border-image-repeat,outline,outline-width,outline-style,outline-color,outline-offset,background,background-color,background-image,background-repeat,background-attachment,background-position,background-position-x,background-position-y,background-clip,background-origin,background-size,box-decoration-break,box-shadow,text-shadow
|
||||
ij_scss_space_after_colon = true
|
||||
ij_scss_space_before_opening_brace = true
|
||||
ij_scss_use_double_quotes = true
|
||||
ij_scss_value_alignment = 0
|
||||
|
||||
[*.twig]
|
||||
indent_size = 2
|
||||
tab_width = 2
|
||||
ij_continuation_indent_size = 2
|
||||
ij_visual_guides =
|
||||
ij_twig_keep_indents_on_empty_lines = false
|
||||
ij_twig_spaces_inside_comments_delimiters = true
|
||||
ij_twig_spaces_inside_delimiters = true
|
||||
ij_twig_spaces_inside_variable_delimiters = true
|
||||
|
||||
[*.vue]
|
||||
indent_size = 2
|
||||
tab_width = 2
|
||||
ij_continuation_indent_size = 4
|
||||
ij_visual_guides =
|
||||
ij_vue_indent_children_of_top_level = template
|
||||
ij_vue_interpolation_new_line_after_start_delimiter = true
|
||||
ij_vue_interpolation_new_line_before_end_delimiter = true
|
||||
ij_vue_interpolation_wrap = off
|
||||
ij_vue_keep_indents_on_empty_lines = false
|
||||
ij_vue_spaces_within_interpolation_expressions = true
|
||||
|
||||
[.editorconfig]
|
||||
ij_visual_guides =
|
||||
ij_editorconfig_align_group_field_declarations = false
|
||||
ij_editorconfig_space_after_colon = true
|
||||
ij_editorconfig_space_after_comma = true
|
||||
ij_editorconfig_space_before_colon = true
|
||||
ij_editorconfig_space_before_comma = false
|
||||
ij_editorconfig_spaces_around_assignment_operators = true
|
||||
|
||||
[{*.ant,*.fxml,*.jhm,*.jnlp,*.jrxml,*.rng,*.tld,*.wsdl,*.xlf,*.xml,*.xsd,*.xsl,*.xslt,*.xul,phpunit.xml.dist}]
|
||||
indent_size = 2
|
||||
tab_width = 2
|
||||
ij_visual_guides =
|
||||
ij_xml_align_attributes = true
|
||||
ij_xml_align_text = false
|
||||
ij_xml_attribute_wrap = normal
|
||||
ij_xml_block_comment_add_space = false
|
||||
ij_xml_block_comment_at_first_column = true
|
||||
ij_xml_keep_blank_lines = 2
|
||||
ij_xml_keep_indents_on_empty_lines = false
|
||||
ij_xml_keep_line_breaks = true
|
||||
ij_xml_keep_line_breaks_in_text = true
|
||||
ij_xml_keep_whitespaces = false
|
||||
ij_xml_keep_whitespaces_around_cdata = preserve
|
||||
ij_xml_keep_whitespaces_inside_cdata = false
|
||||
ij_xml_line_comment_at_first_column = true
|
||||
ij_xml_space_after_tag_name = false
|
||||
ij_xml_space_around_equals_in_attribute = false
|
||||
ij_xml_space_inside_empty_tag = true
|
||||
ij_xml_text_wrap = normal
|
||||
|
||||
[{*.ats,*.cts,*.mts,*.ts,*.tsx}]
|
||||
indent_size = 2
|
||||
tab_width = 2
|
||||
ij_continuation_indent_size = 2
|
||||
ij_visual_guides =
|
||||
ij_typescript_align_imports = false
|
||||
ij_typescript_align_multiline_array_initializer_expression = false
|
||||
ij_typescript_align_multiline_binary_operation = false
|
||||
ij_typescript_align_multiline_chained_methods = false
|
||||
ij_typescript_align_multiline_extends_list = false
|
||||
ij_typescript_align_multiline_for = true
|
||||
ij_typescript_align_multiline_parameters = true
|
||||
ij_typescript_align_multiline_parameters_in_calls = false
|
||||
ij_typescript_align_multiline_ternary_operation = false
|
||||
ij_typescript_align_object_properties = 0
|
||||
ij_typescript_align_union_types = false
|
||||
ij_typescript_align_var_statements = 0
|
||||
ij_typescript_array_initializer_new_line_after_left_brace = false
|
||||
ij_typescript_array_initializer_right_brace_on_new_line = false
|
||||
ij_typescript_array_initializer_wrap = off
|
||||
ij_typescript_assignment_wrap = off
|
||||
ij_typescript_binary_operation_sign_on_next_line = false
|
||||
ij_typescript_binary_operation_wrap = off
|
||||
ij_typescript_blacklist_imports = rxjs/Rx,node_modules/**,**/node_modules/**,@angular/material,@angular/material/typings/**
|
||||
ij_typescript_blank_lines_after_imports = 1
|
||||
ij_typescript_blank_lines_around_class = 1
|
||||
ij_typescript_blank_lines_around_field = 0
|
||||
ij_typescript_blank_lines_around_field_in_interface = 0
|
||||
ij_typescript_blank_lines_around_function = 1
|
||||
ij_typescript_blank_lines_around_method = 1
|
||||
ij_typescript_blank_lines_around_method_in_interface = 1
|
||||
ij_typescript_block_brace_style = end_of_line
|
||||
ij_typescript_block_comment_add_space = false
|
||||
ij_typescript_block_comment_at_first_column = true
|
||||
ij_typescript_call_parameters_new_line_after_left_paren = false
|
||||
ij_typescript_call_parameters_right_paren_on_new_line = false
|
||||
ij_typescript_call_parameters_wrap = off
|
||||
ij_typescript_catch_on_new_line = false
|
||||
ij_typescript_chained_call_dot_on_new_line = true
|
||||
ij_typescript_class_brace_style = end_of_line
|
||||
ij_typescript_class_decorator_wrap = split_into_lines
|
||||
ij_typescript_class_field_decorator_wrap = off
|
||||
ij_typescript_class_method_decorator_wrap = off
|
||||
ij_typescript_comma_on_new_line = false
|
||||
ij_typescript_do_while_brace_force = never
|
||||
ij_typescript_else_on_new_line = false
|
||||
ij_typescript_enforce_trailing_comma = whenmultiline
|
||||
ij_typescript_enum_constants_wrap = on_every_item
|
||||
ij_typescript_extends_keyword_wrap = off
|
||||
ij_typescript_extends_list_wrap = off
|
||||
ij_typescript_field_prefix = _
|
||||
ij_typescript_file_name_style = relaxed
|
||||
ij_typescript_finally_on_new_line = false
|
||||
ij_typescript_for_brace_force = never
|
||||
ij_typescript_for_statement_new_line_after_left_paren = false
|
||||
ij_typescript_for_statement_right_paren_on_new_line = false
|
||||
ij_typescript_for_statement_wrap = off
|
||||
ij_typescript_force_quote_style = true
|
||||
ij_typescript_force_semicolon_style = true
|
||||
ij_typescript_function_expression_brace_style = end_of_line
|
||||
ij_typescript_function_parameter_decorator_wrap = off
|
||||
ij_typescript_if_brace_force = never
|
||||
ij_typescript_import_merge_members = global
|
||||
ij_typescript_import_prefer_absolute_path = global
|
||||
ij_typescript_import_sort_members = true
|
||||
ij_typescript_import_sort_module_name = false
|
||||
ij_typescript_import_use_node_resolution = true
|
||||
ij_typescript_imports_wrap = on_every_item
|
||||
ij_typescript_indent_case_from_switch = true
|
||||
ij_typescript_indent_chained_calls = true
|
||||
ij_typescript_indent_package_children = 0
|
||||
ij_typescript_jsdoc_include_types = false
|
||||
ij_typescript_jsx_attribute_value = braces
|
||||
ij_typescript_keep_blank_lines_in_code = 2
|
||||
ij_typescript_keep_first_column_comment = true
|
||||
ij_typescript_keep_indents_on_empty_lines = false
|
||||
ij_typescript_keep_line_breaks = true
|
||||
ij_typescript_keep_simple_blocks_in_one_line = false
|
||||
ij_typescript_keep_simple_methods_in_one_line = false
|
||||
ij_typescript_jsx_self_closing_tag = true
|
||||
ij_typescript_line_comment_add_space = true
|
||||
ij_typescript_line_comment_at_first_column = false
|
||||
ij_typescript_method_brace_style = end_of_line
|
||||
ij_typescript_method_call_chain_wrap = off
|
||||
ij_typescript_method_parameters_new_line_after_left_paren = false
|
||||
ij_typescript_method_parameters_right_paren_on_new_line = false
|
||||
ij_typescript_method_parameters_wrap = off
|
||||
ij_typescript_object_literal_wrap = on_every_item
|
||||
ij_typescript_object_types_wrap = on_every_item
|
||||
ij_typescript_parentheses_expression_new_line_after_left_paren = false
|
||||
ij_typescript_parentheses_expression_right_paren_on_new_line = false
|
||||
ij_typescript_place_assignment_sign_on_next_line = false
|
||||
ij_typescript_prefer_as_type_cast = false
|
||||
ij_typescript_prefer_explicit_types_function_expression_returns = false
|
||||
ij_typescript_prefer_explicit_types_function_returns = false
|
||||
ij_typescript_prefer_explicit_types_vars_fields = false
|
||||
ij_typescript_prefer_parameters_wrap = false
|
||||
ij_typescript_property_prefix =
|
||||
ij_typescript_reformat_c_style_comments = false
|
||||
ij_typescript_space_after_colon = true
|
||||
ij_typescript_space_after_comma = true
|
||||
ij_typescript_space_after_dots_in_rest_parameter = false
|
||||
ij_typescript_space_after_generator_mult = true
|
||||
ij_typescript_space_after_property_colon = true
|
||||
ij_typescript_space_after_quest = true
|
||||
ij_typescript_space_after_type_colon = true
|
||||
ij_typescript_space_after_unary_not = false
|
||||
ij_typescript_space_before_async_arrow_lparen = true
|
||||
ij_typescript_space_before_catch_keyword = true
|
||||
ij_typescript_space_before_catch_left_brace = true
|
||||
ij_typescript_space_before_catch_parentheses = true
|
||||
ij_typescript_space_before_class_lbrace = true
|
||||
ij_typescript_space_before_class_left_brace = true
|
||||
ij_typescript_space_before_colon = true
|
||||
ij_typescript_space_before_comma = false
|
||||
ij_typescript_space_before_do_left_brace = true
|
||||
ij_typescript_space_before_else_keyword = true
|
||||
ij_typescript_space_before_else_left_brace = true
|
||||
ij_typescript_space_before_finally_keyword = true
|
||||
ij_typescript_space_before_finally_left_brace = true
|
||||
ij_typescript_space_before_for_left_brace = true
|
||||
ij_typescript_space_before_for_parentheses = true
|
||||
ij_typescript_space_before_for_semicolon = false
|
||||
ij_typescript_space_before_function_left_parenth = false
|
||||
ij_typescript_space_before_generator_mult = false
|
||||
ij_typescript_space_before_if_left_brace = true
|
||||
ij_typescript_space_before_if_parentheses = true
|
||||
ij_typescript_space_before_method_call_parentheses = false
|
||||
ij_typescript_space_before_method_left_brace = true
|
||||
ij_typescript_space_before_method_parentheses = false
|
||||
ij_typescript_space_before_property_colon = false
|
||||
ij_typescript_space_before_quest = true
|
||||
ij_typescript_space_before_switch_left_brace = true
|
||||
ij_typescript_space_before_switch_parentheses = true
|
||||
ij_typescript_space_before_try_left_brace = true
|
||||
ij_typescript_space_before_type_colon = false
|
||||
ij_typescript_space_before_unary_not = false
|
||||
ij_typescript_space_before_while_keyword = true
|
||||
ij_typescript_space_before_while_left_brace = true
|
||||
ij_typescript_space_before_while_parentheses = true
|
||||
ij_typescript_spaces_around_additive_operators = true
|
||||
ij_typescript_spaces_around_arrow_function_operator = true
|
||||
ij_typescript_spaces_around_assignment_operators = true
|
||||
ij_typescript_spaces_around_bitwise_operators = true
|
||||
ij_typescript_spaces_around_equality_operators = true
|
||||
ij_typescript_spaces_around_logical_operators = true
|
||||
ij_typescript_spaces_around_multiplicative_operators = true
|
||||
ij_typescript_spaces_around_relational_operators = true
|
||||
ij_typescript_spaces_around_shift_operators = true
|
||||
ij_typescript_spaces_around_unary_operator = false
|
||||
ij_typescript_spaces_within_array_initializer_brackets = true
|
||||
ij_typescript_spaces_within_brackets = false
|
||||
ij_typescript_spaces_within_catch_parentheses = false
|
||||
ij_typescript_spaces_within_for_parentheses = false
|
||||
ij_typescript_spaces_within_if_parentheses = false
|
||||
ij_typescript_spaces_within_imports = true
|
||||
ij_typescript_spaces_within_interpolation_expressions = false
|
||||
ij_typescript_spaces_within_method_call_parentheses = false
|
||||
ij_typescript_spaces_within_method_parentheses = false
|
||||
ij_typescript_spaces_within_object_literal_braces = true
|
||||
ij_typescript_spaces_within_object_type_braces = true
|
||||
ij_typescript_spaces_within_parentheses = false
|
||||
ij_typescript_spaces_within_switch_parentheses = false
|
||||
ij_typescript_spaces_within_type_assertion = false
|
||||
ij_typescript_spaces_within_union_types = true
|
||||
ij_typescript_spaces_within_while_parentheses = false
|
||||
ij_typescript_special_else_if_treatment = true
|
||||
ij_typescript_ternary_operation_signs_on_next_line = false
|
||||
ij_typescript_ternary_operation_wrap = off
|
||||
ij_typescript_union_types_wrap = on_every_item
|
||||
ij_typescript_use_chained_calls_group_indents = false
|
||||
ij_typescript_use_double_quotes = false
|
||||
ij_typescript_use_explicit_js_extension = never
|
||||
ij_typescript_use_import_type = auto
|
||||
ij_typescript_use_path_mapping = always
|
||||
ij_typescript_use_public_modifier = false
|
||||
ij_typescript_use_semicolon_after_statement = true
|
||||
ij_typescript_var_declaration_wrap = normal
|
||||
ij_typescript_while_brace_force = never
|
||||
ij_typescript_while_on_new_line = false
|
||||
ij_typescript_wrap_comments = false
|
||||
|
||||
[{*.bash,*.sh,*.zsh}]
|
||||
indent_size = 2
|
||||
tab_width = 2
|
||||
ij_visual_guides =
|
||||
ij_shell_binary_ops_start_line = false
|
||||
ij_shell_keep_column_alignment_padding = false
|
||||
ij_shell_minify_program = false
|
||||
ij_shell_redirect_followed_by_space = false
|
||||
ij_shell_switch_cases_indented = false
|
||||
ij_shell_use_unix_line_separator = true
|
||||
|
||||
[{*.cjs,*.es6,*.js,*.mjs,*.jsx}]
|
||||
indent_size = 2
|
||||
tab_width = 2
|
||||
ij_continuation_indent_size = 2
|
||||
ij_visual_guides =
|
||||
ij_javascript_align_imports = false
|
||||
ij_javascript_align_multiline_array_initializer_expression = false
|
||||
ij_javascript_align_multiline_binary_operation = false
|
||||
ij_javascript_align_multiline_chained_methods = false
|
||||
ij_javascript_align_multiline_extends_list = false
|
||||
ij_javascript_align_multiline_for = true
|
||||
ij_javascript_align_multiline_parameters = true
|
||||
ij_javascript_align_multiline_parameters_in_calls = false
|
||||
ij_javascript_align_multiline_ternary_operation = false
|
||||
ij_javascript_align_object_properties = 0
|
||||
ij_javascript_align_union_types = false
|
||||
ij_javascript_align_var_statements = 0
|
||||
ij_javascript_array_initializer_new_line_after_left_brace = false
|
||||
ij_javascript_array_initializer_right_brace_on_new_line = false
|
||||
ij_javascript_array_initializer_wrap = off
|
||||
ij_javascript_assignment_wrap = off
|
||||
ij_javascript_binary_operation_sign_on_next_line = false
|
||||
ij_javascript_binary_operation_wrap = off
|
||||
ij_javascript_blacklist_imports = rxjs/Rx,node_modules/**,**/node_modules/**,@angular/material,@angular/material/typings/**
|
||||
ij_javascript_blank_lines_after_imports = 1
|
||||
ij_javascript_blank_lines_around_class = 1
|
||||
ij_javascript_blank_lines_around_field = 0
|
||||
ij_javascript_blank_lines_around_function = 1
|
||||
ij_javascript_blank_lines_around_method = 1
|
||||
ij_javascript_block_brace_style = end_of_line
|
||||
ij_javascript_block_comment_add_space = false
|
||||
ij_javascript_block_comment_at_first_column = true
|
||||
ij_javascript_call_parameters_new_line_after_left_paren = false
|
||||
ij_javascript_call_parameters_right_paren_on_new_line = false
|
||||
ij_javascript_call_parameters_wrap = off
|
||||
ij_javascript_catch_on_new_line = false
|
||||
ij_javascript_chained_call_dot_on_new_line = true
|
||||
ij_javascript_class_brace_style = end_of_line
|
||||
ij_javascript_class_decorator_wrap = split_into_lines
|
||||
ij_javascript_class_field_decorator_wrap = off
|
||||
ij_javascript_class_method_decorator_wrap = off
|
||||
ij_javascript_comma_on_new_line = false
|
||||
ij_javascript_do_while_brace_force = never
|
||||
ij_javascript_else_on_new_line = false
|
||||
ij_javascript_enforce_trailing_comma = whenmultiline
|
||||
ij_javascript_extends_keyword_wrap = off
|
||||
ij_javascript_extends_list_wrap = off
|
||||
ij_javascript_field_prefix = _
|
||||
ij_javascript_file_name_style = relaxed
|
||||
ij_javascript_finally_on_new_line = false
|
||||
ij_javascript_for_brace_force = never
|
||||
ij_javascript_for_statement_new_line_after_left_paren = false
|
||||
ij_javascript_for_statement_right_paren_on_new_line = false
|
||||
ij_javascript_for_statement_wrap = off
|
||||
ij_javascript_force_quote_style = true
|
||||
ij_javascript_force_semicolon_style = true
|
||||
ij_javascript_function_expression_brace_style = end_of_line
|
||||
ij_javascript_function_parameter_decorator_wrap = off
|
||||
ij_javascript_if_brace_force = never
|
||||
ij_javascript_import_merge_members = global
|
||||
ij_javascript_import_prefer_absolute_path = global
|
||||
ij_javascript_import_sort_members = true
|
||||
ij_javascript_import_sort_module_name = false
|
||||
ij_javascript_import_use_node_resolution = true
|
||||
ij_javascript_imports_wrap = on_every_item
|
||||
ij_javascript_indent_case_from_switch = true
|
||||
ij_javascript_indent_chained_calls = true
|
||||
ij_javascript_indent_package_children = 0
|
||||
ij_javascript_jsx_attribute_value = braces
|
||||
ij_javascript_keep_blank_lines_in_code = 2
|
||||
ij_javascript_keep_first_column_comment = true
|
||||
ij_javascript_keep_indents_on_empty_lines = false
|
||||
ij_javascript_keep_line_breaks = true
|
||||
ij_javascript_keep_simple_blocks_in_one_line = false
|
||||
ij_javascript_keep_simple_methods_in_one_line = false
|
||||
ij_javascript_jsx_self_closing_tag = true
|
||||
ij_javascript_line_comment_add_space = true
|
||||
ij_javascript_line_comment_at_first_column = false
|
||||
ij_javascript_method_brace_style = end_of_line
|
||||
ij_javascript_method_call_chain_wrap = off
|
||||
ij_javascript_method_parameters_new_line_after_left_paren = false
|
||||
ij_javascript_method_parameters_right_paren_on_new_line = false
|
||||
ij_javascript_method_parameters_wrap = off
|
||||
ij_javascript_object_literal_wrap = on_every_item
|
||||
ij_javascript_object_types_wrap = on_every_item
|
||||
ij_javascript_parentheses_expression_new_line_after_left_paren = false
|
||||
ij_javascript_parentheses_expression_right_paren_on_new_line = false
|
||||
ij_javascript_place_assignment_sign_on_next_line = false
|
||||
ij_javascript_prefer_as_type_cast = false
|
||||
ij_javascript_prefer_explicit_types_function_expression_returns = false
|
||||
ij_javascript_prefer_explicit_types_function_returns = false
|
||||
ij_javascript_prefer_explicit_types_vars_fields = false
|
||||
ij_javascript_prefer_parameters_wrap = false
|
||||
ij_javascript_property_prefix =
|
||||
ij_javascript_reformat_c_style_comments = true
|
||||
ij_javascript_space_after_colon = true
|
||||
ij_javascript_space_after_comma = true
|
||||
ij_javascript_space_after_dots_in_rest_parameter = false
|
||||
ij_javascript_space_after_generator_mult = true
|
||||
ij_javascript_space_after_property_colon = true
|
||||
ij_javascript_space_after_quest = true
|
||||
ij_javascript_space_after_type_colon = true
|
||||
ij_javascript_space_after_unary_not = false
|
||||
ij_javascript_space_before_async_arrow_lparen = true
|
||||
ij_javascript_space_before_catch_keyword = true
|
||||
ij_javascript_space_before_catch_left_brace = true
|
||||
ij_javascript_space_before_catch_parentheses = true
|
||||
ij_javascript_space_before_class_lbrace = true
|
||||
ij_javascript_space_before_class_left_brace = true
|
||||
ij_javascript_space_before_colon = true
|
||||
ij_javascript_space_before_comma = false
|
||||
ij_javascript_space_before_do_left_brace = true
|
||||
ij_javascript_space_before_else_keyword = true
|
||||
ij_javascript_space_before_else_left_brace = true
|
||||
ij_javascript_space_before_finally_keyword = true
|
||||
ij_javascript_space_before_finally_left_brace = true
|
||||
ij_javascript_space_before_for_left_brace = true
|
||||
ij_javascript_space_before_for_parentheses = true
|
||||
ij_javascript_space_before_for_semicolon = false
|
||||
ij_javascript_space_before_function_left_parenth = false
|
||||
ij_javascript_space_before_generator_mult = false
|
||||
ij_javascript_space_before_if_left_brace = true
|
||||
ij_javascript_space_before_if_parentheses = true
|
||||
ij_javascript_space_before_method_call_parentheses = false
|
||||
ij_javascript_space_before_method_left_brace = true
|
||||
ij_javascript_space_before_method_parentheses = false
|
||||
ij_javascript_space_before_property_colon = false
|
||||
ij_javascript_space_before_quest = true
|
||||
ij_javascript_space_before_switch_left_brace = true
|
||||
ij_javascript_space_before_switch_parentheses = true
|
||||
ij_javascript_space_before_try_left_brace = true
|
||||
ij_javascript_space_before_type_colon = false
|
||||
ij_javascript_space_before_unary_not = false
|
||||
ij_javascript_space_before_while_keyword = true
|
||||
ij_javascript_space_before_while_left_brace = true
|
||||
ij_javascript_space_before_while_parentheses = true
|
||||
ij_javascript_spaces_around_additive_operators = true
|
||||
ij_javascript_spaces_around_arrow_function_operator = true
|
||||
ij_javascript_spaces_around_assignment_operators = true
|
||||
ij_javascript_spaces_around_bitwise_operators = true
|
||||
ij_javascript_spaces_around_equality_operators = true
|
||||
ij_javascript_spaces_around_logical_operators = true
|
||||
ij_javascript_spaces_around_multiplicative_operators = true
|
||||
ij_javascript_spaces_around_relational_operators = true
|
||||
ij_javascript_spaces_around_shift_operators = true
|
||||
ij_javascript_spaces_around_unary_operator = false
|
||||
ij_javascript_spaces_within_array_initializer_brackets = false
|
||||
ij_javascript_spaces_within_brackets = false
|
||||
ij_javascript_spaces_within_catch_parentheses = false
|
||||
ij_javascript_spaces_within_for_parentheses = false
|
||||
ij_javascript_spaces_within_if_parentheses = false
|
||||
ij_javascript_spaces_within_imports = true
|
||||
ij_javascript_spaces_within_interpolation_expressions = false
|
||||
ij_javascript_spaces_within_method_call_parentheses = false
|
||||
ij_javascript_spaces_within_method_parentheses = false
|
||||
ij_javascript_spaces_within_object_literal_braces = true
|
||||
ij_javascript_spaces_within_object_type_braces = true
|
||||
ij_javascript_spaces_within_parentheses = false
|
||||
ij_javascript_spaces_within_switch_parentheses = false
|
||||
ij_javascript_spaces_within_type_assertion = false
|
||||
ij_javascript_spaces_within_union_types = true
|
||||
ij_javascript_spaces_within_while_parentheses = false
|
||||
ij_javascript_special_else_if_treatment = true
|
||||
ij_javascript_ternary_operation_signs_on_next_line = false
|
||||
ij_javascript_ternary_operation_wrap = off
|
||||
ij_javascript_union_types_wrap = on_every_item
|
||||
ij_javascript_use_chained_calls_group_indents = false
|
||||
ij_javascript_use_double_quotes = false
|
||||
ij_javascript_use_explicit_js_extension = never
|
||||
ij_javascript_use_import_type = auto
|
||||
ij_javascript_use_path_mapping = always
|
||||
ij_javascript_use_public_modifier = false
|
||||
ij_javascript_use_semicolon_after_statement = true
|
||||
ij_javascript_var_declaration_wrap = normal
|
||||
ij_javascript_while_brace_force = never
|
||||
ij_javascript_while_on_new_line = false
|
||||
ij_javascript_wrap_comments = true
|
||||
|
||||
[{*.ctp,*.hphp,*.inc,*.module,*.php,*.php4,*.php5,*.phtml}]
|
||||
indent_size = 4
|
||||
tab_width = 4
|
||||
ij_continuation_indent_size = 4
|
||||
ij_visual_guides =
|
||||
ij_php_align_assignments = false
|
||||
ij_php_align_class_constants = true
|
||||
ij_php_align_enum_cases = false
|
||||
ij_php_align_group_field_declarations = false
|
||||
ij_php_align_inline_comments = false
|
||||
ij_php_align_key_value_pairs = true
|
||||
ij_php_align_match_arm_bodies = true
|
||||
ij_php_align_multiline_array_initializer_expression = false
|
||||
ij_php_align_multiline_binary_operation = false
|
||||
ij_php_align_multiline_chained_methods = true
|
||||
ij_php_align_multiline_extends_list = false
|
||||
ij_php_align_multiline_for = true
|
||||
ij_php_align_multiline_parameters = true
|
||||
ij_php_align_multiline_parameters_in_calls = false
|
||||
ij_php_align_multiline_ternary_operation = false
|
||||
ij_php_align_named_arguments = false
|
||||
ij_php_align_phpdoc_comments = true
|
||||
ij_php_align_phpdoc_param_names = true
|
||||
ij_php_anonymous_brace_style = end_of_line
|
||||
ij_php_api_weight = 28
|
||||
ij_php_array_initializer_new_line_after_left_brace = false
|
||||
ij_php_array_initializer_right_brace_on_new_line = true
|
||||
ij_php_array_initializer_wrap = off
|
||||
ij_php_assignment_wrap = off
|
||||
ij_php_attributes_wrap = off
|
||||
ij_php_author_weight = 1
|
||||
ij_php_binary_operation_sign_on_next_line = false
|
||||
ij_php_binary_operation_wrap = off
|
||||
ij_php_blank_lines_after_class_header = 0
|
||||
ij_php_blank_lines_after_function = 1
|
||||
ij_php_blank_lines_after_imports = 1
|
||||
ij_php_blank_lines_after_opening_tag = 0
|
||||
ij_php_blank_lines_after_package = 0
|
||||
ij_php_blank_lines_around_class = 1
|
||||
ij_php_blank_lines_around_constants = 0
|
||||
ij_php_blank_lines_around_enum_cases = 0
|
||||
ij_php_blank_lines_around_field = 1
|
||||
ij_php_blank_lines_around_method = 1
|
||||
ij_php_blank_lines_before_class_end = 0
|
||||
ij_php_blank_lines_before_imports = 1
|
||||
ij_php_blank_lines_before_method_body = 0
|
||||
ij_php_blank_lines_before_package = 1
|
||||
ij_php_blank_lines_before_return_statement = 0
|
||||
ij_php_blank_lines_between_imports = 0
|
||||
ij_php_block_brace_style = end_of_line
|
||||
ij_php_call_parameters_new_line_after_left_paren = false
|
||||
ij_php_call_parameters_right_paren_on_new_line = false
|
||||
ij_php_call_parameters_wrap = off
|
||||
ij_php_catch_on_new_line = false
|
||||
ij_php_category_weight = 28
|
||||
ij_php_class_brace_style = next_line
|
||||
ij_php_comma_after_last_argument = false
|
||||
ij_php_comma_after_last_argument_style = when_multiline
|
||||
ij_php_comma_after_last_array_element = true
|
||||
ij_php_comma_after_last_closure_use_var = false
|
||||
ij_php_comma_after_last_closure_use_var_style = when_multiline
|
||||
ij_php_comma_after_last_match_arm = false
|
||||
ij_php_comma_after_last_parameter = false
|
||||
ij_php_comma_after_last_parameter_style = when_multiline
|
||||
ij_php_concat_spaces = true
|
||||
ij_php_copyright_weight = 28
|
||||
ij_php_deprecated_weight = 28
|
||||
ij_php_do_while_brace_force = never
|
||||
ij_php_else_if_style = as_is
|
||||
ij_php_else_on_new_line = false
|
||||
ij_php_example_weight = 28
|
||||
ij_php_extends_keyword_wrap = off
|
||||
ij_php_extends_list_wrap = off
|
||||
ij_php_fields_default_visibility = private
|
||||
ij_php_filesource_weight = 28
|
||||
ij_php_finally_on_new_line = false
|
||||
ij_php_for_brace_force = never
|
||||
ij_php_for_statement_new_line_after_left_paren = false
|
||||
ij_php_for_statement_right_paren_on_new_line = false
|
||||
ij_php_for_statement_wrap = off
|
||||
ij_php_force_empty_classes_in_one_line = false
|
||||
ij_php_force_empty_methods_in_one_line = false
|
||||
ij_php_force_short_declaration_array_style = false
|
||||
ij_php_getters_setters_naming_style = camel_case
|
||||
ij_php_getters_setters_order_style = getters_first
|
||||
ij_php_global_weight = 28
|
||||
ij_php_group_use_wrap = on_every_item
|
||||
ij_php_heredoc_on_same_line = false
|
||||
ij_php_if_brace_force = never
|
||||
ij_php_if_lparen_on_next_line = false
|
||||
ij_php_if_rparen_on_next_line = false
|
||||
ij_php_ignore_weight = 28
|
||||
ij_php_import_sorting = alphabetic
|
||||
ij_php_indent_break_from_case = true
|
||||
ij_php_indent_case_from_switch = true
|
||||
ij_php_indent_code_in_php_tags = false
|
||||
ij_php_internal_weight = 28
|
||||
ij_php_keep_blank_lines_after_lbrace = 2
|
||||
ij_php_keep_blank_lines_before_right_brace = 2
|
||||
ij_php_keep_blank_lines_in_code = 2
|
||||
ij_php_keep_blank_lines_in_declarations = 2
|
||||
ij_php_keep_control_statement_in_one_line = true
|
||||
ij_php_keep_first_column_comment = false
|
||||
ij_php_keep_indents_on_empty_lines = false
|
||||
ij_php_keep_line_breaks = true
|
||||
ij_php_keep_rparen_and_lbrace_on_one_line = true
|
||||
ij_php_keep_simple_classes_in_one_line = true
|
||||
ij_php_keep_simple_methods_in_one_line = true
|
||||
ij_php_lambda_brace_style = end_of_line
|
||||
ij_php_license_weight = 28
|
||||
ij_php_line_comment_add_space = false
|
||||
ij_php_line_comment_at_first_column = true
|
||||
ij_php_link_weight = 28
|
||||
ij_php_lower_case_boolean_const = true
|
||||
ij_php_lower_case_keywords = true
|
||||
ij_php_lower_case_null_const = true
|
||||
ij_php_method_brace_style = next_line
|
||||
ij_php_method_call_chain_wrap = normal
|
||||
ij_php_method_parameters_new_line_after_left_paren = true
|
||||
ij_php_method_parameters_right_paren_on_new_line = true
|
||||
ij_php_method_parameters_wrap = on_every_item
|
||||
ij_php_method_weight = 5
|
||||
ij_php_modifier_list_wrap = false
|
||||
ij_php_multiline_chained_calls_first_call_on_new_line = false
|
||||
ij_php_multiline_chained_calls_semicolon_on_new_line = false
|
||||
ij_php_multiline_closure_lambda_on_new_line = false
|
||||
ij_php_namespace_brace_style = 2
|
||||
ij_php_new_line_after_php_opening_tag = false
|
||||
ij_php_null_type_position = in_the_end
|
||||
ij_php_package_weight = 0
|
||||
ij_php_param_weight = 2
|
||||
ij_php_parameters_attributes_wrap = off
|
||||
ij_php_parentheses_expression_new_line_after_left_paren = false
|
||||
ij_php_parentheses_expression_right_paren_on_new_line = false
|
||||
ij_php_phpdoc_blank_line_before_tags = true
|
||||
ij_php_phpdoc_blank_lines_around_parameters = true
|
||||
ij_php_phpdoc_keep_blank_lines = true
|
||||
ij_php_phpdoc_param_spaces_between_name_and_description = 1
|
||||
ij_php_phpdoc_param_spaces_between_tag_and_type = 1
|
||||
ij_php_phpdoc_param_spaces_between_type_and_name = 1
|
||||
ij_php_phpdoc_use_fqcn = false
|
||||
ij_php_phpdoc_wrap_long_lines = true
|
||||
ij_php_place_assignment_sign_on_next_line = false
|
||||
ij_php_place_parens_for_constructor = 0
|
||||
ij_php_property_read_weight = 28
|
||||
ij_php_property_weight = 28
|
||||
ij_php_property_write_weight = 28
|
||||
ij_php_return_type_on_new_line = false
|
||||
ij_php_return_weight = 3
|
||||
ij_php_see_weight = 28
|
||||
ij_php_since_weight = 28
|
||||
ij_php_sort_phpdoc_elements = true
|
||||
ij_php_space_after_colon = true
|
||||
ij_php_space_after_colon_in_enum_backed_type = true
|
||||
ij_php_space_after_colon_in_named_argument = true
|
||||
ij_php_space_after_colon_in_return_type = true
|
||||
ij_php_space_after_comma = true
|
||||
ij_php_space_after_for_semicolon = true
|
||||
ij_php_space_after_quest = true
|
||||
ij_php_space_after_type_cast = false
|
||||
ij_php_space_after_unary_not = false
|
||||
ij_php_space_before_array_initializer_left_brace = false
|
||||
ij_php_space_before_catch_keyword = true
|
||||
ij_php_space_before_catch_left_brace = true
|
||||
ij_php_space_before_catch_parentheses = true
|
||||
ij_php_space_before_class_left_brace = true
|
||||
ij_php_space_before_closure_left_parenthesis = true
|
||||
ij_php_space_before_colon = true
|
||||
ij_php_space_before_colon_in_enum_backed_type = false
|
||||
ij_php_space_before_colon_in_named_argument = false
|
||||
ij_php_space_before_colon_in_return_type = false
|
||||
ij_php_space_before_comma = false
|
||||
ij_php_space_before_do_left_brace = true
|
||||
ij_php_space_before_else_keyword = true
|
||||
ij_php_space_before_else_left_brace = true
|
||||
ij_php_space_before_finally_keyword = true
|
||||
ij_php_space_before_finally_left_brace = true
|
||||
ij_php_space_before_for_left_brace = true
|
||||
ij_php_space_before_for_parentheses = true
|
||||
ij_php_space_before_for_semicolon = false
|
||||
ij_php_space_before_if_left_brace = true
|
||||
ij_php_space_before_if_parentheses = true
|
||||
ij_php_space_before_method_call_parentheses = false
|
||||
ij_php_space_before_method_left_brace = true
|
||||
ij_php_space_before_method_parentheses = false
|
||||
ij_php_space_before_quest = true
|
||||
ij_php_space_before_short_closure_left_parenthesis = false
|
||||
ij_php_space_before_switch_left_brace = true
|
||||
ij_php_space_before_switch_parentheses = true
|
||||
ij_php_space_before_try_left_brace = true
|
||||
ij_php_space_before_unary_not = false
|
||||
ij_php_space_before_while_keyword = true
|
||||
ij_php_space_before_while_left_brace = true
|
||||
ij_php_space_before_while_parentheses = true
|
||||
ij_php_space_between_ternary_quest_and_colon = false
|
||||
ij_php_spaces_around_additive_operators = true
|
||||
ij_php_spaces_around_arrow = false
|
||||
ij_php_spaces_around_assignment_in_declare = false
|
||||
ij_php_spaces_around_assignment_operators = true
|
||||
ij_php_spaces_around_bitwise_operators = true
|
||||
ij_php_spaces_around_equality_operators = true
|
||||
ij_php_spaces_around_logical_operators = true
|
||||
ij_php_spaces_around_multiplicative_operators = true
|
||||
ij_php_spaces_around_null_coalesce_operator = true
|
||||
ij_php_spaces_around_pipe_in_union_type = false
|
||||
ij_php_spaces_around_relational_operators = true
|
||||
ij_php_spaces_around_shift_operators = true
|
||||
ij_php_spaces_around_unary_operator = false
|
||||
ij_php_spaces_around_var_within_brackets = false
|
||||
ij_php_spaces_within_array_initializer_braces = false
|
||||
ij_php_spaces_within_brackets = false
|
||||
ij_php_spaces_within_catch_parentheses = false
|
||||
ij_php_spaces_within_for_parentheses = false
|
||||
ij_php_spaces_within_if_parentheses = false
|
||||
ij_php_spaces_within_method_call_parentheses = false
|
||||
ij_php_spaces_within_method_parentheses = false
|
||||
ij_php_spaces_within_parentheses = false
|
||||
ij_php_spaces_within_short_echo_tags = true
|
||||
ij_php_spaces_within_switch_parentheses = false
|
||||
ij_php_spaces_within_while_parentheses = false
|
||||
ij_php_special_else_if_treatment = false
|
||||
ij_php_subpackage_weight = 28
|
||||
ij_php_ternary_operation_signs_on_next_line = true
|
||||
ij_php_ternary_operation_wrap = off
|
||||
ij_php_throws_weight = 4
|
||||
ij_php_todo_weight = 28
|
||||
ij_php_treat_multiline_arrays_and_lambdas_multiline = false
|
||||
ij_php_unknown_tag_weight = 28
|
||||
ij_php_upper_case_boolean_const = false
|
||||
ij_php_upper_case_null_const = false
|
||||
ij_php_uses_weight = 28
|
||||
ij_php_var_weight = 28
|
||||
ij_php_variable_naming_style = mixed
|
||||
ij_php_version_weight = 28
|
||||
ij_php_while_brace_force = never
|
||||
ij_php_while_on_new_line = false
|
||||
|
||||
[{*.har,*.jsb2,*.jsb3,*.json,*.jsonc,*.postman_collection,*.postman_collection.json,*.postman_environment,*.postman_environment.json,.babelrc,.eslintrc,.prettierrc,.stylelintrc,.ws-context,composer.lock,jest.config}]
|
||||
indent_size = 2
|
||||
ij_visual_guides =
|
||||
ij_json_array_wrapping = split_into_lines
|
||||
ij_json_keep_blank_lines_in_code = 0
|
||||
ij_json_keep_indents_on_empty_lines = false
|
||||
ij_json_keep_line_breaks = true
|
||||
ij_json_keep_trailing_comma = false
|
||||
ij_json_object_wrapping = split_into_lines
|
||||
ij_json_property_alignment = do_not_align
|
||||
ij_json_space_after_colon = true
|
||||
ij_json_space_after_comma = true
|
||||
ij_json_space_before_colon = false
|
||||
ij_json_space_before_comma = false
|
||||
ij_json_spaces_within_braces = false
|
||||
ij_json_spaces_within_brackets = false
|
||||
ij_json_wrap_long_lines = false
|
||||
|
||||
[{*.htm,*.html,*.sht,*.shtm,*.shtml}]
|
||||
indent_size = 2
|
||||
tab_width = 2
|
||||
ij_continuation_indent_size = 2
|
||||
ij_visual_guides =
|
||||
ij_html_add_new_line_before_tags = body,div,p,form,h1,h2,h3
|
||||
ij_html_align_attributes = true
|
||||
ij_html_align_text = false
|
||||
ij_html_attribute_wrap = on_every_item
|
||||
ij_html_block_comment_add_space = false
|
||||
ij_html_block_comment_at_first_column = true
|
||||
ij_html_do_not_align_children_of_min_lines = 0
|
||||
ij_html_do_not_break_if_inline_tags = title,h1,h2,h3,h4,h5,h6
|
||||
ij_html_do_not_indent_children_of_tags = thead,tbody,tfoot
|
||||
ij_html_enforce_quotes = true
|
||||
ij_html_inline_tags = a,abbr,acronym,b,basefont,bdo,big,br,cite,cite,code,dfn,em,font,i,img,input,kbd,label,q,s,samp,select,small,strike,strong,sub,sup,textarea,tt,u,var
|
||||
ij_html_keep_blank_lines = 2
|
||||
ij_html_keep_indents_on_empty_lines = false
|
||||
ij_html_keep_line_breaks = true
|
||||
ij_html_keep_line_breaks_in_text = true
|
||||
ij_html_keep_whitespaces = false
|
||||
ij_html_keep_whitespaces_inside = span,pre,textarea
|
||||
ij_html_line_comment_at_first_column = true
|
||||
ij_html_new_line_after_last_attribute = when_multiline
|
||||
ij_html_new_line_before_first_attribute = when_multiline
|
||||
ij_html_quote_style = double
|
||||
ij_html_remove_new_line_before_tags = br
|
||||
ij_html_space_after_tag_name = false
|
||||
ij_html_space_around_equality_in_attribute = false
|
||||
ij_html_space_inside_empty_tag = true
|
||||
ij_html_self_closing_tag = true
|
||||
ij_html_text_wrap = normal
|
||||
|
||||
[{*.http,*.rest}]
|
||||
ij_continuation_indent_size = 4
|
||||
ij_visual_guides =
|
||||
ij_http-request_call_parameters_wrap = normal
|
||||
ij_http-request_method_parameters_wrap = split_into_lines
|
||||
ij_http-request_space_before_comma = true
|
||||
ij_http-request_spaces_around_assignment_operators = true
|
||||
|
||||
[{*.markdown,*.md}]
|
||||
ij_visual_guides =
|
||||
ij_markdown_force_one_space_after_blockquote_symbol = true
|
||||
ij_markdown_force_one_space_after_header_symbol = true
|
||||
ij_markdown_force_one_space_after_list_bullet = true
|
||||
ij_markdown_force_one_space_between_words = true
|
||||
ij_markdown_format_tables = true
|
||||
ij_markdown_insert_quote_arrows_on_wrap = true
|
||||
ij_markdown_keep_indents_on_empty_lines = false
|
||||
ij_markdown_keep_line_breaks_inside_text_blocks = true
|
||||
ij_markdown_max_lines_around_block_elements = 1
|
||||
ij_markdown_max_lines_around_header = 1
|
||||
ij_markdown_max_lines_between_paragraphs = 1
|
||||
ij_markdown_min_lines_around_block_elements = 1
|
||||
ij_markdown_min_lines_around_header = 1
|
||||
ij_markdown_min_lines_between_paragraphs = 1
|
||||
ij_markdown_wrap_text_if_long = true
|
||||
ij_markdown_wrap_text_inside_blockquotes = true
|
||||
|
||||
[{*.mk,GNUmakefile,GNUmakefile.inc,makefile,makefile.inc}]
|
||||
ij_visual_guides =
|
||||
|
||||
[{*.yaml,*.yml}]
|
||||
indent_size = 2
|
||||
ij_visual_guides =
|
||||
ij_yaml_align_values_properties = do_not_align
|
||||
ij_yaml_autoinsert_sequence_marker = true
|
||||
ij_yaml_block_mapping_on_new_line = false
|
||||
ij_yaml_indent_sequence_value = true
|
||||
ij_yaml_keep_indents_on_empty_lines = false
|
||||
ij_yaml_keep_line_breaks = true
|
||||
ij_yaml_line_comment_add_space = false
|
||||
ij_yaml_line_comment_add_space_on_reformat = false
|
||||
ij_yaml_line_comment_at_first_column = true
|
||||
ij_yaml_sequence_on_new_line = false
|
||||
ij_yaml_space_before_colon = false
|
||||
ij_yaml_spaces_within_braces = true
|
||||
ij_yaml_spaces_within_brackets = true
|
||||
59
.env.dist
Normal file
59
.env.dist
Normal file
@@ -0,0 +1,59 @@
|
||||
# This file is a "template" of which env vars need to be defined for your application
|
||||
# Copy this file to .env file for development, create environment variables when deploying to production
|
||||
# https://symfony.com/doc/current/best_practices/configuration.html#infrastructure-related-configuration
|
||||
|
||||
###> symfony/framework-bundle ###
|
||||
APP_ENV=dev
|
||||
APP_SECRET=changethis
|
||||
APP_NAME=mineseeker
|
||||
# APP_PUBLIC_HOSTNAME: The public hostname for your application (used for generating absolute URLs in emails)
|
||||
# For production, set this to your domain (e.g., mineseeker.com)
|
||||
APP_PUBLIC_HOSTNAME=localhost
|
||||
# TRUSTED_PROXIES: Only needed for bare-metal dev behind a reverse proxy
|
||||
# For Docker development, this is set in compose.override.yaml
|
||||
# For production, set in PROD_ENV_FILE Gitea secret (use 172.18.0.0/16 initially)
|
||||
#TRUSTED_PROXIES=127.0.0.1,127.0.0.2
|
||||
#TRUSTED_HOSTS=localhost,example.com
|
||||
###< symfony/framework-bundle ###
|
||||
|
||||
###> doctrine/doctrine-bundle ###
|
||||
# Docker PostgreSQL is exposed on port 15432 — use that for bare-metal dev.
|
||||
# Replace POSTGRES_USER / POSTGRES_PASSWORD / POSTGRES_DB with the values from your .env.
|
||||
DATABASE_URL=postgresql://POSTGRES_USER:POSTGRES_PASSWORD@127.0.0.1:15432/POSTGRES_DB?serverVersion=18&charset=utf8
|
||||
###< doctrine/doctrine-bundle ###
|
||||
|
||||
###> google/recaptcha ###
|
||||
RECAPTCHA_SITE_KEY=changethis
|
||||
RECAPTCHA_SECRET_KEY=changethis
|
||||
###< google/recaptcha ###
|
||||
|
||||
###> minio/minio ###
|
||||
MINIO_ROOT_USER=changethis
|
||||
MINIO_ROOT_PASSWORD=changethis
|
||||
MINIO_ENDPOINT=http://localhost:9000
|
||||
MINIO_PUBLIC_URL=https://your-minio-subdomain.example.com
|
||||
###< minio/minio ###
|
||||
|
||||
###> symfony/mailer ###
|
||||
MAILER_DSN=smtp://localhost:1025
|
||||
MAIL_DOMAIN=localhost
|
||||
###< symfony/mailer ###
|
||||
|
||||
###> symfony/mercure-bundle ###
|
||||
# See https://symfony.com/doc/current/mercure.html#configuration
|
||||
# The URL of the Mercure hub, used by the app to publish updates (can be a local URL)
|
||||
MERCURE_URL=https://mine.local/.well-known/mercure
|
||||
# The public URL of the Mercure hub, used by the browser to connect
|
||||
MERCURE_PUBLIC_URL=https://mine.local/.well-known/mercure
|
||||
# The secret used to sign the JWTs
|
||||
MERCURE_JWT_SECRET="!ChangeThisMercureHubJWTSecretKey!"
|
||||
# Publisher JWT (must match publisher_jwt in your Caddyfile)
|
||||
MERCURE_JWT_TOKEN=changethis
|
||||
# Subscriber JWT sent to the browser so it can authenticate its EventSource connection
|
||||
MERCURE_SUBSCRIBER_JWT=changethis
|
||||
###< symfony/mercure-bundle ###
|
||||
|
||||
###> web-auth/webauthn-framework ###
|
||||
WEBAUTHN_RP_ID=mine.local
|
||||
WEBAUTHN_ORIGIN=https://mine.local
|
||||
###< web-auth/webauthn-framework ###
|
||||
3
.env.test
Normal file
3
.env.test
Normal file
@@ -0,0 +1,3 @@
|
||||
# define your env variables for the test env here
|
||||
KERNEL_CLASS='App\Kernel'
|
||||
APP_SECRET='$ecretf0rt3st'
|
||||
32
.gitchangelog.rc
Normal file
32
.gitchangelog.rc
Normal file
@@ -0,0 +1,32 @@
|
||||
ignore_regexps = [
|
||||
r'@minor', r'!minor',
|
||||
r'@cosmetic', r'!cosmetic',
|
||||
r'@refactor', r'!refactor',
|
||||
r'@wip', r'!wip',
|
||||
r'^(.{3,3}\s*:)?\s*[fF]irst commit.?\s*$',
|
||||
r'^$', ## ignore commits with empty messages
|
||||
r'@skipChangelog', r'!skipChangelog', r'skipChangeLog', r'!skipChangeLog',
|
||||
r'Merge branch', r'Merge remote-tracking branch', r'!deploy',
|
||||
]
|
||||
section_regexps = [
|
||||
('New', [
|
||||
r'^[nN]ew\s*:\s*((dev|use?r|pkg|test|doc)\s*:\s*)?([^\n]*)$',
|
||||
]),
|
||||
('Changes', [
|
||||
r'^[cC]hg\s*:\s*((dev|use?r|pkg|test|doc)\s*:\s*)?([^\n]*)$',
|
||||
]),
|
||||
('Fix', [
|
||||
r'^[fF]ix\s*:\s*((dev|use?r|pkg|test|doc)\s*:\s*)?([^\n]*)$',
|
||||
]),
|
||||
('Other', None ## Match all lines
|
||||
),
|
||||
]
|
||||
body_process = ReSub(r'((^|\n)[A-Z]\w+(-\w+)*: .*(\n\s+.*)*)+$', r'') | strip
|
||||
subject_process = (strip |
|
||||
ReSub(r'^([cC]hg|[fF]ix|[nN]ew)\s*:\s*((dev|use?r|pkg|test|doc)\s*:\s*)?([^\n@]*)(@[a-z]+\s+)*$', r'\4') |
|
||||
SetIfEmpty("No commit message.") | ucfirst | final_dot)
|
||||
tag_filter_regexp = r'^(v)?[0-9]+\.[0-9]+(\.[0-9]+)?(\-[0-9]+)?$'
|
||||
unreleased_version_label = "(unreleased)"
|
||||
output_engine = mustache("markdown")
|
||||
include_merge = True
|
||||
revs = []
|
||||
142
.gitea/workflows/ci.yml-bak
Normal file
142
.gitea/workflows/ci.yml-bak
Normal file
@@ -0,0 +1,142 @@
|
||||
name: CI - Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- develop
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
- develop
|
||||
|
||||
jobs:
|
||||
tests:
|
||||
runs-on: splendid-bear
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:18-alpine
|
||||
env:
|
||||
POSTGRES_USER: mineseeker_test
|
||||
POSTGRES_PASSWORD: test_password
|
||||
POSTGRES_DB: mineseeker_test
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
ports:
|
||||
- 5432:5432
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup PHP 8.3
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: '8.3'
|
||||
extensions: pdo_pgsql, gd, intl, zip, sodium
|
||||
coverage: none
|
||||
tools: composer:v2
|
||||
|
||||
- name: Validate composer.json
|
||||
run: composer validate --strict
|
||||
|
||||
- name: Cache Composer dependencies
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: vendor
|
||||
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-composer-
|
||||
|
||||
- name: Install PHP dependencies
|
||||
run: composer install --prefer-dist --no-progress --no-interaction
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
- name: Cache node modules
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: node_modules
|
||||
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-node-
|
||||
|
||||
- name: Install Node dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Build assets
|
||||
run: npm run build
|
||||
|
||||
- name: Create .env.test file
|
||||
run: |
|
||||
cat > .env.test << 'ENVEOF'
|
||||
APP_ENV=test
|
||||
APP_SECRET=test-secret-key-for-ci-testing-only
|
||||
DATABASE_URL="postgresql://mineseeker_test:test_password@localhost:5432/mineseeker_test?serverVersion=18&charset=utf8"
|
||||
KERNEL_CLASS='App\Kernel'
|
||||
SYMFONY_DEPRECATIONS_HELPER=999999
|
||||
PANTHER_APP_ENV=panther
|
||||
PANTHER_ERROR_SCREENSHOT_DIR=./var/error-screenshots
|
||||
ENVEOF
|
||||
|
||||
- name: Setup test database
|
||||
run: make test-db-setup
|
||||
|
||||
- name: Run PHPUnit tests
|
||||
run: vendor/bin/phpunit --testdox --colors=always
|
||||
|
||||
- name: Run PHPUnit tests with coverage (optional)
|
||||
if: github.event_name == 'pull_request'
|
||||
run: |
|
||||
XDEBUG_MODE=coverage vendor/bin/phpunit --coverage-text --coverage-clover=coverage.xml
|
||||
|
||||
- name: Upload coverage reports (optional)
|
||||
if: github.event_name == 'pull_request'
|
||||
uses: codecov/codecov-action@v4
|
||||
with:
|
||||
file: ./coverage.xml
|
||||
flags: unittests
|
||||
name: phpunit-coverage
|
||||
fail_ci_if_error: false
|
||||
|
||||
lint:
|
||||
runs-on: splendid-bear
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup PHP 8.3
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: '8.3'
|
||||
tools: composer:v2, phpstan
|
||||
|
||||
- name: Install PHP dependencies
|
||||
run: composer install --prefer-dist --no-progress --no-interaction
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
- name: Install Node dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Run ESLint
|
||||
run: npm run lint || true
|
||||
|
||||
- name: Check code style (PHP)
|
||||
run: |
|
||||
if [ -f "vendor/bin/php-cs-fixer" ]; then
|
||||
vendor/bin/php-cs-fixer fix --dry-run --diff
|
||||
else
|
||||
echo "PHP-CS-Fixer not installed, skipping..."
|
||||
fi
|
||||
39
.gitea/workflows/deploy.yml
Normal file
39
.gitea/workflows/deploy.yml
Normal file
@@ -0,0 +1,39 @@
|
||||
name: Deploy to Production
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: splendid-bear
|
||||
steps:
|
||||
- name: Checkout tag
|
||||
env:
|
||||
GITEA_TOKEN: ${{ gitea.token }}
|
||||
run: |
|
||||
set -e
|
||||
export HOME=/tmp
|
||||
git config --global credential.helper '!f() { echo "username=oauth2"; echo "password=$GITEA_TOKEN"; }; f'
|
||||
git config --global --add safe.directory "${{ vars.PROD_APP_DIR }}"
|
||||
cd "${{ vars.PROD_APP_DIR }}"
|
||||
git remote set-url origin "${{ gitea.server_url }}/${{ gitea.repository }}.git"
|
||||
git fetch --tags --force
|
||||
git checkout "${{ gitea.ref_name }}"
|
||||
|
||||
- name: Write .env
|
||||
env:
|
||||
PROD_ENV_FILE: ${{ secrets.PROD_ENV_FILE }}
|
||||
run: |
|
||||
printf '%s' "$PROD_ENV_FILE" > "${{ vars.PROD_APP_DIR }}/.env"
|
||||
|
||||
- name: Build image
|
||||
run: |
|
||||
cd "${{ vars.PROD_APP_DIR }}"
|
||||
docker compose build
|
||||
|
||||
- name: Start services
|
||||
run: |
|
||||
cd "${{ vars.PROD_APP_DIR }}"
|
||||
docker compose up -d
|
||||
126
.gitea/workflows/deploy.yml-bak
Normal file
126
.gitea/workflows/deploy.yml-bak
Normal file
@@ -0,0 +1,126 @@
|
||||
name: Deploy to Production
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: splendid-bear
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:18-alpine
|
||||
env:
|
||||
POSTGRES_USER: mineseeker_test
|
||||
POSTGRES_PASSWORD: test_password
|
||||
POSTGRES_DB: mineseeker_test
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
ports:
|
||||
- 5432:5432
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup PHP 8.3
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: '8.3'
|
||||
extensions: pdo_pgsql, gd, intl, zip, sodium
|
||||
coverage: none
|
||||
tools: composer:v2
|
||||
|
||||
- name: Install PHP dependencies
|
||||
run: composer install --prefer-dist --no-progress --no-interaction
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
- name: Install Node dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Build assets
|
||||
run: npm run build
|
||||
|
||||
- name: Create .env.test file
|
||||
run: |
|
||||
cat > .env.test << 'ENVEOF'
|
||||
APP_ENV=test
|
||||
APP_SECRET=test-secret-key-for-ci-testing-only
|
||||
DATABASE_URL="postgresql://mineseeker_test:test_password@localhost:5432/mineseeker_test?serverVersion=18&charset=utf8"
|
||||
KERNEL_CLASS='App\Kernel'
|
||||
SYMFONY_DEPRECATIONS_HELPER=999999
|
||||
ENVEOF
|
||||
|
||||
- name: Setup test database
|
||||
run: make test-db-setup
|
||||
|
||||
- name: Run PHPUnit tests
|
||||
run: vendor/bin/phpunit --testdox --colors=always --stop-on-failure
|
||||
|
||||
deploy:
|
||||
needs: test
|
||||
runs-on: splendid-bear
|
||||
steps:
|
||||
- name: Checkout tag
|
||||
env:
|
||||
GITEA_TOKEN: ${{ gitea.token }}
|
||||
run: |
|
||||
set -e
|
||||
export HOME=/tmp
|
||||
git config --global credential.helper '!f() { echo "username=oauth2"; echo "password=$GITEA_TOKEN"; }; f'
|
||||
git config --global --add safe.directory "${{ vars.PROD_APP_DIR }}"
|
||||
cd "${{ vars.PROD_APP_DIR }}"
|
||||
git remote set-url origin "${{ gitea.server_url }}/${{ gitea.repository }}.git"
|
||||
git fetch --tags --force
|
||||
git checkout "${{ gitea.ref_name }}"
|
||||
|
||||
- name: Write .env
|
||||
env:
|
||||
PROD_ENV_FILE: ${{ secrets.PROD_ENV_FILE }}
|
||||
run: |
|
||||
printf '%s' "$PROD_ENV_FILE" > "${{ vars.PROD_APP_DIR }}/.env"
|
||||
|
||||
- name: Build image
|
||||
run: |
|
||||
cd "${{ vars.PROD_APP_DIR }}"
|
||||
docker compose build
|
||||
|
||||
- name: Run database migrations
|
||||
run: |
|
||||
cd "${{ vars.PROD_APP_DIR }}"
|
||||
docker compose run --rm app php bin/console doctrine:migrations:migrate --no-interaction
|
||||
|
||||
- name: Clear cache
|
||||
run: |
|
||||
cd "${{ vars.PROD_APP_DIR }}"
|
||||
docker compose run --rm app php bin/console cache:clear --env=prod
|
||||
|
||||
- name: Start services
|
||||
run: |
|
||||
cd "${{ vars.PROD_APP_DIR }}"
|
||||
docker compose up -d
|
||||
|
||||
- name: Health check
|
||||
run: |
|
||||
sleep 5
|
||||
curl -f http://localhost:10080/ || exit 1
|
||||
|
||||
- name: Notify deployment success
|
||||
if: success()
|
||||
run: |
|
||||
echo "✅ Deployment successful for tag ${{ gitea.ref_name }}"
|
||||
|
||||
- name: Notify deployment failure
|
||||
if: failure()
|
||||
run: |
|
||||
echo "❌ Deployment failed for tag ${{ gitea.ref_name }}"
|
||||
exit 1
|
||||
59
.gitignore
vendored
59
.gitignore
vendored
@@ -1,40 +1,21 @@
|
||||
/app/config/parameters.yml
|
||||
/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 ###
|
||||
|
||||
###> phpunit/phpunit ###
|
||||
/phpunit.xml
|
||||
/.phpunit.cache/
|
||||
###< phpunit/phpunit ###
|
||||
|
||||
464
AGENTS.md
Normal file
464
AGENTS.md
Normal file
@@ -0,0 +1,464 @@
|
||||
# AI Agent Guidelines for MineSeeker
|
||||
|
||||
This document provides guidelines and context for AI coding agents working on the MineSeeker project.
|
||||
|
||||
## Project Overview
|
||||
|
||||
**MineSeeker** is a real-time multiplayer 1v1 minesweeper game built with Symfony (PHP) and React. Players compete to claim mines on a shared 16×16 grid, with the first to reach 26 mines winning.
|
||||
|
||||
### Tech Stack
|
||||
|
||||
- **Backend:** Symfony 7.2 (PHP 8.3)
|
||||
- **Frontend:** React 18, Vite 6
|
||||
- **Database:** PostgreSQL 17 with materialized views
|
||||
- **Storage:** MinIO (S3-compatible)
|
||||
- **Real-time:** Mercure (Server-Sent Events)
|
||||
- **Styling:** SCSS, MUI (Material-UI), Emotion
|
||||
- **Fonts:** @fontsource packages (web), Carlito-Bold.ttf (server-side images)
|
||||
|
||||
### Key Features
|
||||
|
||||
**Core Gameplay:**
|
||||
- **Multiplayer-focused:** 1v1 competitive gameplay where players race to claim more mines than their opponent
|
||||
- **Win condition:** First player to claim 26 out of 51 mines wins
|
||||
- **Real-time updates:** WebSocket-like gameplay using Mercure (Server-Sent Events)
|
||||
- **Game restoration:** Players can resume unfinished games from where they left off
|
||||
- **Bonus points system:** Rewards skilled play (blind hits, chain combos, edge mines, endgame mines, safe cell reveals)
|
||||
|
||||
**User Features:**
|
||||
- **Authentication:** Password + optional TOTP + optional WebAuthn passkeys
|
||||
- **Anonymous play:** Guest players can play without creating an account
|
||||
- **Profile statistics:** Detailed stats including wins, losses, draws, win rate, average score, total mines hit, and bonus points
|
||||
- **Battle history:** View and replay past games move-by-move
|
||||
|
||||
**Sharing & Social:**
|
||||
- **Battle reports:** Shareable public pages for each completed game (`/battle/{uuid}`)
|
||||
- **OG image generation:** Automatic creation of 1200×630 PNG images for social media sharing (using PHP GD)
|
||||
- **Open Graph tags:** Battle share pages include rich preview cards with player names, avatars, scores, and bonus points
|
||||
|
||||
---
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
### Backend (Symfony)
|
||||
|
||||
```
|
||||
src/
|
||||
├── Controller/ # HTTP endpoints (game, profile, battle sharing)
|
||||
├── Entity/ # Doctrine ORM entities
|
||||
├── Repository/ # Database queries (uses QueryBuilder, not raw SQL)
|
||||
├── Service/ # Business logic (BattleCardGenerator, WebAuthn, Email)
|
||||
├── Dto/ # Data Transfer Objects (immutable, readonly)
|
||||
├── Util/ # Game logic (TopicManager for Mercure)
|
||||
└── Migrations/ # Database schema changes
|
||||
```
|
||||
|
||||
**Important patterns:**
|
||||
- Use Doctrine ORM QueryBuilder (not raw SQL) in repositories
|
||||
- DTOs are `final readonly` classes with constructor property promotion
|
||||
- Services use dependency injection via `config/services.yaml`
|
||||
- Materialized views for performance (auto-refreshed via triggers)
|
||||
|
||||
### Frontend (React)
|
||||
|
||||
```
|
||||
assets/
|
||||
├── js/
|
||||
│ ├── mine-seeker/ # Main game bundle (self-contained)
|
||||
│ │ ├── MineSeeker.jsx # Root component, wraps GameProvider + QueryClientProvider
|
||||
│ │ ├── components/ # Game-specific components
|
||||
│ │ │ ├── GameBoard.jsx # Main game board grid
|
||||
│ │ │ ├── GameTimer.jsx # Game timer display
|
||||
│ │ │ ├── BonusBox.jsx # Bonus points indicator
|
||||
│ │ │ ├── BonusStatsDialog.jsx # Bonus statistics modal
|
||||
│ │ │ ├── CaptchaOverlay.jsx # Captcha challenge overlay
|
||||
│ │ │ ├── ChallengeCountdown.jsx # Challenge timer
|
||||
│ │ │ ├── OnlinePlayersDialog.jsx # Online players list
|
||||
│ │ │ ├── WaitingOverlayContent.jsx # Waiting for opponent
|
||||
│ │ │ ├── grid/ # Grid-related components (cells, mines)
|
||||
│ │ │ ├── profile/ # In-game profile components (PlayerColumn)
|
||||
│ │ │ ├── timer/ # Timer-related components
|
||||
│ │ │ └── user/ # User-related components
|
||||
│ │ ├── contexts/ # React Context API
|
||||
│ │ │ ├── GameContext.jsx # Game state context
|
||||
│ │ │ └── GameProvider.jsx # Context provider with state logic
|
||||
│ │ ├── hooks/ # Custom React hooks
|
||||
│ │ │ ├── useGameDataProvider.js # React Query data provider
|
||||
│ │ │ ├── useGameRefs.jsx # Refs for DOM elements
|
||||
│ │ │ ├── useGameState.jsx # Game state management
|
||||
│ │ │ ├── useServerCommunication.jsx # Mercure SSE connection
|
||||
│ │ │ └── useStepTimer.jsx # Step-by-step timer
|
||||
│ │ └── utils/ # Game-specific utilities
|
||||
│ │ └── constants.jsx # Game constants, colors, defaults
|
||||
│ ├── components/ # Shared UI components
|
||||
│ ├── utils/ # Shared utilities
|
||||
│ ├── profile.jsx # Profile page entry
|
||||
│ ├── passkey.jsx # Passkey management entry
|
||||
│ └── contact.jsx # Contact form entry
|
||||
├── css/
|
||||
│ └── homepage/ # SCSS partials (imported by style.homepage.scss)
|
||||
└── fonts/
|
||||
└── Carlito-Bold.ttf # TTF font for PHP GD image generation
|
||||
```
|
||||
|
||||
**Important patterns:**
|
||||
- Vite aliases: `@mine-components`, `@mine-contexts`, `@mine-hooks`, `@mine-utils`, `@global-components`, `@global-utils`
|
||||
- React Query only available inside `mine-seeker` bundle
|
||||
- Avoid circular dependencies (e.g., don't import from `@global-components` inside `components/` directory)
|
||||
- PropTypes required on all components
|
||||
|
||||
---
|
||||
|
||||
## Common Tasks
|
||||
|
||||
### Adding a New Feature
|
||||
|
||||
1. **Backend:**
|
||||
- Create migration for schema changes
|
||||
- Add/update entities and repositories
|
||||
- Create DTOs for data transfer
|
||||
- Add controller endpoints
|
||||
- Update service configuration if needed
|
||||
|
||||
2. **Frontend:**
|
||||
- Create components in appropriate bundle
|
||||
- Add PropTypes to all components
|
||||
- Use existing hooks and utilities
|
||||
- Follow styled-components pattern for MUI customization
|
||||
|
||||
3. **Documentation:**
|
||||
- Update relevant docs in `docs/` folder
|
||||
- Add examples if introducing new patterns
|
||||
|
||||
### Database Changes
|
||||
|
||||
- Always create migrations: `bin/console make:migration`
|
||||
- Use Doctrine QueryBuilder in repositories (not raw SQL)
|
||||
- For PostgreSQL-specific features (materialized views, triggers), use raw SQL in migrations only
|
||||
- Materialized views should auto-refresh via triggers
|
||||
|
||||
### Styling
|
||||
|
||||
- **Web fonts:** Use `@fontsource` packages (WOFF/WOFF2)
|
||||
- **Server-side images:** Use TTF fonts in `assets/fonts/` (PHP GD requires TTF)
|
||||
- **CSS:** Create SCSS partials in `assets/css/homepage/`, import in main file
|
||||
- **Components:** Use Emotion styled-components or CSS classes
|
||||
|
||||
### File Headers
|
||||
|
||||
All PHP and JS/JSX files should have this header:
|
||||
|
||||
```php
|
||||
<?php declare(strict_types=1);
|
||||
/*
|
||||
* This file is part of the SplendidBear Websites' projects.
|
||||
*
|
||||
* Copyright (c) 2026 @ www.splendidbear.org
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
```
|
||||
|
||||
```javascript
|
||||
/**
|
||||
* This file is part of the SplendidBear Websites' projects.
|
||||
*
|
||||
* Copyright (c) 2026 @ www.splendidbear.org
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Coding Standards
|
||||
|
||||
### PHP
|
||||
|
||||
- **PSR Standards:** Follow PSR-1, PSR-12 coding standards
|
||||
- **Type declarations:** Use strict types (`declare(strict_types=1)`)
|
||||
- **Property promotion:** Use constructor property promotion for DTOs and services
|
||||
- **Readonly:** Use `readonly` for immutable properties
|
||||
- **Final:** Mark DTOs as `final`
|
||||
- **Doctrine:** Use QueryBuilder with `expr()` methods, not string concatenation
|
||||
- **Null safety:** Use null coalescing `??` and null-safe operator `?->`
|
||||
- **Formatting:** 4-space indentation, opening braces on same line for methods/classes
|
||||
|
||||
**Example DTO:**
|
||||
|
||||
```php
|
||||
final readonly class ProfileGameDto implements JsonSerializable
|
||||
{
|
||||
public function __construct(
|
||||
public ?int $id,
|
||||
public string $redName,
|
||||
public string $blueName,
|
||||
public bool $bothRegistered,
|
||||
) {}
|
||||
}
|
||||
```
|
||||
|
||||
### JavaScript/React
|
||||
|
||||
- **Components:** Functional components with hooks
|
||||
- **PropTypes:** Required on all components
|
||||
- **Imports:** Use aliases (`@global-components`, `@mine-hooks`, etc.)
|
||||
- **State:** Use `useState`, `useEffect`, `useCallback`, `useMemo` appropriately
|
||||
- **Avoid:** Circular dependencies, especially with barrel exports
|
||||
|
||||
**Example component:**
|
||||
|
||||
```javascript
|
||||
import React, { useState } from 'react';
|
||||
import { string, number } from 'prop-types';
|
||||
|
||||
export const MyComponent = ({ title, count }) => {
|
||||
const [value, setValue] = useState(0);
|
||||
|
||||
return <div>{title}: {count + value}</div>;
|
||||
};
|
||||
|
||||
MyComponent.propTypes = {
|
||||
title: string.isRequired,
|
||||
count: number.isRequired,
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Important Files & Locations
|
||||
|
||||
### Configuration
|
||||
|
||||
- `config/services.yaml` - Service definitions and parameters
|
||||
- `vite.config.js` - Vite build config, aliases
|
||||
- `.env` - Environment variables (not in git)
|
||||
- `composer.json` - PHP dependencies
|
||||
- `package.json` - Node dependencies
|
||||
|
||||
### Key Services
|
||||
|
||||
- `BattleCardGenerator` - Generates 1200×630 PNG OG images using PHP GD
|
||||
- `TopicManager` - Mercure topic management and game logic
|
||||
- `WebAuthnService` - Passkey authentication
|
||||
- `Email services` - Various email senders in `src/Service/Email/`
|
||||
|
||||
### Important Entities
|
||||
|
||||
- `User` - Registered users
|
||||
- `PlayedGame` - Game records with moves, grid, scores
|
||||
- `RecentBattle` - Read-only entity from materialized view
|
||||
- `UserStats` - Read-only entity from materialized view
|
||||
|
||||
### Documentation
|
||||
|
||||
- `docs/game-mechanics/BONUS_POINTS_SYSTEM.md` - Bonus points reference
|
||||
- `docs/FONTS.md` - Font usage and management
|
||||
- `AGENTS.md` - This file
|
||||
- `CHANGELOG.md` - Project changelog
|
||||
|
||||
---
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
### ❌ Don't Do
|
||||
|
||||
- **Don't use raw SQL in repositories** (use Doctrine QueryBuilder)
|
||||
- **Don't create circular imports** (e.g., importing `@global-components` from within `components/`)
|
||||
- **Don't use system fonts directly** (bundle TTF in `assets/fonts/`)
|
||||
- **Don't skip PropTypes** on React components
|
||||
- **Don't use `var`** in JavaScript (use `const` or `let`)
|
||||
- **Don't define variables after using them** (hoisting issues with `const`)
|
||||
|
||||
### ✅ Do
|
||||
|
||||
- **Use Doctrine QueryBuilder** with `expr()` methods
|
||||
- **Import components directly** to avoid circular dependencies
|
||||
- **Bundle fonts in project** for portability
|
||||
- **Add PropTypes** to all components
|
||||
- **Use `const` for immutable values**, `let` for mutable
|
||||
- **Define variables before using them**
|
||||
|
||||
---
|
||||
|
||||
## Keeping Components in Sync
|
||||
|
||||
### Battle Report Display
|
||||
|
||||
The battle report/statistics appear in **two places** that must be kept synchronized:
|
||||
|
||||
#### 1. BattleDialog Component (React)
|
||||
**File:** `assets/js/components/BattleDialog.jsx`
|
||||
- React dialog component shown on profile page
|
||||
- Uses Material-UI and styled-components
|
||||
- Displays game stats, bonus points, winner information
|
||||
|
||||
#### 2. Battle Share Page (Twig)
|
||||
**File:** `templates/Game/battle_share.html.twig`
|
||||
- Public battle share page (accessible via `/battle/{uuid}`)
|
||||
- Server-rendered HTML with SCSS styling
|
||||
- Shows same information as BattleDialog
|
||||
|
||||
### Synchronization Rules
|
||||
|
||||
**When updating BattleDialog.jsx, also update battle_share.html.twig:**
|
||||
|
||||
✅ Adding new stats or fields
|
||||
✅ Changing display logic (winner calculation, formatting)
|
||||
✅ Modifying bonus points display
|
||||
✅ Updating labels or text
|
||||
|
||||
**Keep in sync:**
|
||||
- Game outcome logic (win/loss/draw/abandoned)
|
||||
- Bonus points formatting
|
||||
- Player name display
|
||||
- Score display
|
||||
- Stats and metadata shown
|
||||
|
||||
**Example:** If you add a new stat to BattleDialog showing "fastest mine claim time", you must also add it to battle_share.html.twig so both displays show the same information.
|
||||
|
||||
---
|
||||
|
||||
## Testing & Building
|
||||
|
||||
### Backend
|
||||
|
||||
```bash
|
||||
# Run migrations
|
||||
bin/console doctrine:migrations:migrate
|
||||
|
||||
# Clear cache
|
||||
bin/console cache:clear
|
||||
|
||||
# Refresh materialized views
|
||||
bin/console dbal:run-sql "REFRESH MATERIALIZED VIEW CONCURRENTLY recent_battles"
|
||||
```
|
||||
|
||||
### Frontend
|
||||
|
||||
```bash
|
||||
# Development build with watch
|
||||
npm run dev
|
||||
|
||||
# Production build
|
||||
npm run build
|
||||
|
||||
# After changing fonts
|
||||
rm -rf var/og-cache/*
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Git Workflow
|
||||
|
||||
### Commit Messages
|
||||
|
||||
Follow conventional commits:
|
||||
|
||||
- `feat: add bonus points to battle cards`
|
||||
- `fix: resolve circular dependency in BattleDialog`
|
||||
- `refactor: use Doctrine QueryBuilder in RecentBattleRepository`
|
||||
- `docs: add AGENTS.md for AI coding agents`
|
||||
- `chore: update dependencies`
|
||||
|
||||
### Creating Pull Requests
|
||||
|
||||
When creating PRs, include:
|
||||
|
||||
1. **Summary** - What was changed and why
|
||||
2. **Changes** - List of modified files/components
|
||||
3. **Testing** - How to verify the changes
|
||||
4. **Screenshots** - For UI changes
|
||||
|
||||
---
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
### Database
|
||||
|
||||
- Use materialized views for expensive queries (profile stats, recent battles)
|
||||
- Auto-refresh materialized views via triggers on source table changes
|
||||
- Index frequently queried columns
|
||||
- Use `COALESCE()` for nullable aggregates
|
||||
|
||||
### Frontend
|
||||
|
||||
- Lazy load large components
|
||||
- Use React Query for data fetching and caching
|
||||
- Avoid unnecessary re-renders (use `useMemo`, `useCallback`)
|
||||
- Bundle code per entry point (profile, passkey, contact, game)
|
||||
|
||||
### Images
|
||||
|
||||
- Battle card images are cached in `var/og-cache/`
|
||||
- Images regenerated when games change (via deterministic UUID)
|
||||
- Use appropriate image sizes (1200×630 for OG images)
|
||||
|
||||
---
|
||||
|
||||
## Security
|
||||
|
||||
### Authentication
|
||||
|
||||
- Password + TOTP (optional) + WebAuthn passkeys (optional)
|
||||
- Backup codes for TOTP recovery
|
||||
- Session-based authentication with `IS_AUTHENTICATED_REMEMBERED`
|
||||
|
||||
### Data Access
|
||||
|
||||
- Controllers check `denyAccessUnlessGranted()`
|
||||
- Materialized views filter by `user_id`
|
||||
- Guest players use separate `Gamer` entity (no `User` account)
|
||||
|
||||
### Input Validation
|
||||
|
||||
- Symfony forms for user input
|
||||
- File upload validation (size, MIME type)
|
||||
- WebAuthn challenge validation
|
||||
|
||||
---
|
||||
|
||||
## Useful Commands
|
||||
|
||||
```bash
|
||||
# Symfony
|
||||
bin/console debug:container ServiceName # Inspect service configuration
|
||||
bin/console debug:router # List all routes
|
||||
bin/console make:migration # Create new migration
|
||||
bin/console doctrine:migrations:list # List migrations
|
||||
|
||||
# Database
|
||||
bin/console dbal:run-sql "SELECT * FROM ..." # Run SQL query
|
||||
|
||||
# Assets
|
||||
npm run build # Build production assets
|
||||
npm run dev # Build with watch mode
|
||||
|
||||
# Git
|
||||
git log --oneline --graph # View commit history
|
||||
git diff origin/main...HEAD # See changes since main
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Need Help?
|
||||
|
||||
- **Bonus Points:** See `docs/game-mechanics/BONUS_POINTS_SYSTEM.md`
|
||||
- **Fonts:** See `docs/FONTS.md`
|
||||
- **Symfony Docs:** https://symfony.com/doc/current/
|
||||
- **React Docs:** https://react.dev/
|
||||
- **Doctrine ORM:** https://www.doctrine-project.org/projects/doctrine-orm/en/current/
|
||||
|
||||
---
|
||||
|
||||
## Version History
|
||||
|
||||
- **2026-04-21:** Initial AGENTS.md created
|
||||
- Document common patterns, pitfalls, and project structure
|
||||
- Include coding standards and examples
|
||||
|
||||
---
|
||||
|
||||
**Happy coding! 🚀**
|
||||
531
CHANGELOG.md
Normal file
531
CHANGELOG.md
Normal file
@@ -0,0 +1,531 @@
|
||||
# Changelog
|
||||
|
||||
|
||||
## v2026.2.8-0 (2026-04-21)
|
||||
|
||||
### New
|
||||
|
||||
* Add CI/CD improvements - add new CI workflow - & improve the deployment w/ tests #10. [Lang]
|
||||
|
||||
* Add test cases to back-end w/ real database connection in it #10. [Lang]
|
||||
|
||||
### Changes
|
||||
|
||||
* The original CI/CD workflow is restored - the work with tests is postponed #10. [Lang]
|
||||
|
||||
* Add sound when the game start #10. [Lang]
|
||||
|
||||
* Create AGENTS.md file for future maintenance #9. [Lang]
|
||||
|
||||
* Remove the wrongly implemented font installation in docker - & replace it with static font on BattleCardGenerator (it solves the shareable image problem on bare-metal too) #8. [Lang]
|
||||
|
||||
|
||||
## v2026.2.7-1 (2026-04-21)
|
||||
|
||||
### Changes
|
||||
|
||||
* Fine-tune the recent battle list #8. [Lang]
|
||||
|
||||
|
||||
## v2026.2.7-0 (2026-04-21)
|
||||
|
||||
### Changes
|
||||
|
||||
* Small changes on docs - and improve text on homepage #8. [Lang]
|
||||
|
||||
* Massive refactor on front-end for unification and readiness #8. [Lang]
|
||||
|
||||
* Update all doc blocks on back-end #8. [Lang]
|
||||
|
||||
* Small refactors on back-end #8. [Lang]
|
||||
|
||||
* Add RecentBattle entity that is a Materialized View to speed up the view - and further refactor on ProfileController #8. [Lang]
|
||||
|
||||
* Create the UserStats entity what is a Materialized View to store Profile stats for every user - & massive ProfileController refactor #8. [Lang]
|
||||
|
||||
* Refactor the SecurityController #7. [Lang]
|
||||
|
||||
* Refactor the code - there was unnecessary codes and wrongly formatted or designed code that are related to Repositories #7. [Lang]
|
||||
|
||||
* Upgrade the doctrine related back-end pkgs to the latest available version #7. [Lang]
|
||||
|
||||
* Add filter to the Profile page's recent plays and an infite list too #7. [Lang]
|
||||
|
||||
* Upgrade to the latest doctrine pkg on back-end #7. [Lang]
|
||||
|
||||
### Fix
|
||||
|
||||
* Do not hide the end-game overlay ever #8. [Lang]
|
||||
|
||||
* The username was not recognized properly #7. [Lang]
|
||||
|
||||
|
||||
## v2026.2.6-1 (2026-04-19)
|
||||
|
||||
### Fix
|
||||
|
||||
* The PostgreSQL logo was horrible #7. [Lang]
|
||||
|
||||
|
||||
## v2026.2.6-0 (2026-04-19)
|
||||
|
||||
### Changes
|
||||
|
||||
* Add ReCaptcha overlay again to protect the game #7. [Lang]
|
||||
|
||||
* Upgrade all front-end packages to the latest available version - and fix all eslint warnings & errors #7. [Lang]
|
||||
|
||||
* Upgrade fe packages #7. [Lang]
|
||||
|
||||
* Massive refactor on fetches - create centralized dataProvider #7. [Lang]
|
||||
|
||||
|
||||
## v2026.2.5-0 (2026-04-19)
|
||||
|
||||
### New
|
||||
|
||||
* Add Firebase deps to back-end #7. [Lang]
|
||||
|
||||
* Add missing buttons for overlays #7. [Lang]
|
||||
|
||||
* A new feature came up - the abandoned plays can be restored, if both users are registered users #7. [Lang]
|
||||
|
||||
### Changes
|
||||
|
||||
* Fix the '0' in Battle reports #6. [Lang]
|
||||
|
||||
* Fix missing icons on "Battle report" #6. [Lang]
|
||||
|
||||
### Fix
|
||||
|
||||
* The bomb using was not recorded correctly - the old data will be corrupted #6. [Lang]
|
||||
|
||||
|
||||
## v2026.2.4-0 (2026-04-18)
|
||||
|
||||
### New
|
||||
|
||||
* Add new profile charts and stats - & add new logo to the tech stack #5. [Lang]
|
||||
|
||||
### Changes
|
||||
|
||||
* Upgrade fe deps #5. [Lang]
|
||||
|
||||
* Improve the Battle reports to change unnecessary data with interesting data #5. [Lang]
|
||||
|
||||
### Fix
|
||||
|
||||
* The react is crashing on some cases #5. [Lang]
|
||||
|
||||
|
||||
## v2026.2.3-0 (2026-04-18)
|
||||
|
||||
### New
|
||||
|
||||
* Add initialization bonus points' system to the gameplay #5. [Lang]
|
||||
|
||||
### Changes
|
||||
|
||||
* Add extended data to battle reports and sharing image to make viewable bonus points #5. [Lang]
|
||||
|
||||
|
||||
## v2026.2.2-9 (2026-04-18)
|
||||
|
||||
### New
|
||||
|
||||
* Add rules page #4. [Lang]
|
||||
|
||||
### Fix
|
||||
|
||||
* The font-awesome simplifying to work on bare-metal - & fix all warnings at build time #4. [Lang]
|
||||
|
||||
* The css problem had been solved on reponsive gfx on homepage #4. [Lang]
|
||||
|
||||
|
||||
## v2026.2.1-8 (2026-04-18)
|
||||
|
||||
### Fix
|
||||
|
||||
* Quickfix for https-only login - & add user data when the user is not logged in #4. [Lang]
|
||||
|
||||
|
||||
## v2026.2.1-7 (2026-04-16)
|
||||
|
||||
### Changes
|
||||
|
||||
* Add consent checkbox to user's registration - and fix the sharing pics #4. [Lang]
|
||||
|
||||
* Add correct version numbering and CHANGELOG - and add the LICENSE #4. [Lang]
|
||||
|
||||
|
||||
## v2026.2.1-6 (2026-04-16)
|
||||
|
||||
### Changes
|
||||
|
||||
* Update all texts on all pages - extend them with the game specific things #4. [Lang]
|
||||
|
||||
|
||||
## v2026.2.1-5 (2026-04-16)
|
||||
|
||||
### Fix
|
||||
|
||||
* The meta tags does not have https scheme - nothing worked in configuration #4. [Lang]
|
||||
|
||||
|
||||
## v2026.2.1-4 (2026-04-15)
|
||||
|
||||
### New
|
||||
|
||||
* Add notification email when a user is registered #4. [Lang]
|
||||
|
||||
### Changes
|
||||
|
||||
* Add notification on activation too #4. [Lang]
|
||||
|
||||
|
||||
## v2026.2.1-2 (2026-04-15)
|
||||
|
||||
### Fix
|
||||
|
||||
* Another attempt to fix the email assets #4. [Lang]
|
||||
|
||||
|
||||
## v2026.2.1-1 (2026-04-15)
|
||||
|
||||
### Fix
|
||||
|
||||
* The images does not shows in emails #4. [Lang]
|
||||
|
||||
|
||||
## v2026.2.1-0 (2026-04-15)
|
||||
|
||||
### New
|
||||
|
||||
* Add Contact page with email sending behaviour #4. [Lang]
|
||||
|
||||
### Changes
|
||||
|
||||
* Add missing .env variable and increase the version number and add missing data from front-end and back-end deps descriptor #4. [Lang]
|
||||
|
||||
* Change the shareable battle - add avatars to it - even on the og tags #4. [Lang]
|
||||
|
||||
* Change text #4. [Lang]
|
||||
|
||||
### Fix
|
||||
|
||||
* The mailhog is crashed on development env #4. [Lang]
|
||||
|
||||
* The og tags did not have proper http schema - they should have https #4. [Lang]
|
||||
|
||||
|
||||
## v2026.2.0-5 (2026-04-14)
|
||||
|
||||
### Changes
|
||||
|
||||
* Add donation button #4. [Lang]
|
||||
|
||||
|
||||
## v2026.2.0-4 (2026-04-14)
|
||||
|
||||
### New
|
||||
|
||||
* Add timer for the acceptance of the challenge #4. [Lang]
|
||||
|
||||
### Changes
|
||||
|
||||
* Protect the gameplay with recaptcha #4. [Lang]
|
||||
|
||||
* The waiting dialog is uncloseable until the time is up #4. [Lang]
|
||||
|
||||
* Add share button to the overlay when the game ends #4. [Lang]
|
||||
|
||||
* Make fancy og tags - and create a special one for battle sharing #4. [Lang]
|
||||
|
||||
### Fix
|
||||
|
||||
* Missing font-awesome icons on bare-metal environment #4. [Lang]
|
||||
|
||||
|
||||
## v2026.2.0-3 (2026-04-14)
|
||||
|
||||
### Changes
|
||||
|
||||
* The user's avatar will be saved as a uuid.extension #4. [Lang]
|
||||
|
||||
|
||||
## v2026.2.0-1 (2026-04-14)
|
||||
|
||||
### Fix
|
||||
|
||||
* Quickfix for email sending #4. [Lang]
|
||||
|
||||
|
||||
## v2026.2.0-0 (2026-04-14)
|
||||
|
||||
### New
|
||||
|
||||
* Registered users have avatars next to the timer #4. [Lang]
|
||||
|
||||
* Add opportunity to use profile picture. #4. [Lang]
|
||||
|
||||
* Add more stats and a dialog for the recent battle that can be shareable #4. [Lang]
|
||||
|
||||
* Implement the 2FA authentication (TOTP and backup codes) #4. [Lang]
|
||||
|
||||
* Add beta logo to the corner #3. [Lang]
|
||||
|
||||
* Add mineseeker game to the symfony 4 project #3. [Lang]
|
||||
|
||||
* Upgrade to the latest symfony v4 #3. [Lang]
|
||||
|
||||
### Changes
|
||||
|
||||
* Implement CD script to Gitea and add docs to the process #4. [Lang]
|
||||
|
||||
* Remove unnecessary cdn based fonts #4. [Lang]
|
||||
|
||||
* Update docs #4. [Lang]
|
||||
|
||||
* Add JWT generation script to make Mercure safe #4. [Lang]
|
||||
|
||||
* Fix missing favicon #4. [Lang]
|
||||
|
||||
* Make compatible the whole project with bare metal AND with docker #4. [Lang]
|
||||
|
||||
* Add modern Webauthn authentication #4. [Lang]
|
||||
|
||||
* Refactor all forms to have Symfony Form Types & Validation Constrainsts - & implement Google ReCapthca v3 #4. [Lang]
|
||||
|
||||
* Add forgot password functionality #4. [Lang]
|
||||
|
||||
* Increase the minimum PHP version to the latest major - and massive refactor on back-end, like Controllers and Repositories #4. [Lang]
|
||||
|
||||
* Redesign the resign dialog #4. [Lang]
|
||||
|
||||
* Re-implement the waiting for opponent dialog - refactor its gfx - & add online user selection dialog #4. [Lang]
|
||||
|
||||
* Improve the gfx on homepage - implement login/register and activation for authentication - and add the first version of profile page #4. [Lang]
|
||||
|
||||
* Refactor and redesign the gfx on front-end #4. [Lang]
|
||||
|
||||
* Upgrade to the latest LTS Symfony package and backend #4. [Lang]
|
||||
|
||||
* Add timers to each player - renew the whole migration #4. [Lang]
|
||||
|
||||
* Update the vite related stuff because CORS and React errors - reinit the miration #4. [Lang]
|
||||
|
||||
* Use namespaces for front-end #4. [Lang]
|
||||
|
||||
* Replace webpack w/ vite & remove old, legacy jQuery from the code #4. [Lang]
|
||||
|
||||
* More, massive refactor for front-end #4. [Lang]
|
||||
|
||||
* Massive refactor on front-end - and remove unnecessary deps #4. [Lang]
|
||||
|
||||
* Change the code style to fit the current standard #4. [Lang]
|
||||
|
||||
* Refactor to use Attributes instead of yaml markdown #4. [Lang]
|
||||
|
||||
* Outsource the Grid generation and interactions to the backend #4. [Lang]
|
||||
|
||||
* Remove unnecessary variables and prune the Facebook registration method #4. [Lang]
|
||||
|
||||
* Replace the legacy gos/web-socket-bundle & replace it with Mercure protocol #4. [Lang]
|
||||
|
||||
* Make a massive refactor to the backend and remove all unnecessary deps - and make small refactors for the frontend too #4. [Lang]
|
||||
|
||||
* Created the first working solution since 7 yrs #4. [Lang]
|
||||
|
||||
* Add some changes on BE - add eslint and editorconfig - and add some deps #4. [Lang]
|
||||
|
||||
* Make the first working version - the stepping is broken due to the algorythm structure #4. [Lang]
|
||||
|
||||
* Change the composer default php minimum environment #3. [Lang]
|
||||
|
||||
* Change the default url to wss on frontend #3. [Lang]
|
||||
|
||||
* Refactor Rpc and Topic classes #3. [Lang]
|
||||
|
||||
* Refactor classes and reformat some layout #3. [Lang]
|
||||
|
||||
* Remove deprecated files #3. [Lang]
|
||||
|
||||
* Doc in README.md #3. [Lang]
|
||||
|
||||
* Gitignore a js.map file #2. [Lang]
|
||||
|
||||
### Other
|
||||
|
||||
* Pkg: usr: solve the not-working mailing on dev env under docker #4. [Lang]
|
||||
|
||||
|
||||
## 1.1.0 (2019-10-26)
|
||||
|
||||
### Changes
|
||||
|
||||
* Reinit project - disable redis module and make the project compatible w/ PHP7.3 #2. [Lang]
|
||||
|
||||
|
||||
## 0.4.0 (2019-10-26)
|
||||
|
||||
### Other
|
||||
|
||||
* Change session driver to REDIS. [Lang]
|
||||
|
||||
* Add created, updated field to db && improve graph design. [Lang]
|
||||
|
||||
* Cache setup && optimalize for google pagespeed && optimalize all images. [Lang]
|
||||
|
||||
* Improve graph design on homepage && add footer and techs && add official pages. [Lang]
|
||||
|
||||
* Bugfix mine websocket periodic mysql calling. [Lang]
|
||||
|
||||
* Bugfix hwioauth remember me && centralize hwioauth and facebook settings. [Lang]
|
||||
|
||||
* Centralize jquery && bugfix mysql auto-termination problem w/ user auth. [Lang]
|
||||
|
||||
* Release beta4. [Lang]
|
||||
|
||||
* Gitignore npm debug log. [Lang]
|
||||
|
||||
* Add english lang everywhere && add snowfall && add centralized version nbr && improve stylesheet && slack integration. [Lang]
|
||||
|
||||
* Bugfix #30 && random bg in game. [Lang]
|
||||
|
||||
* Add google analytics and facebook scripts && improve url share method w/ fb && enforce https in prod. [Lang]
|
||||
|
||||
* Reg and login buttons on index && remove list method && facebook centralize. [Lang]
|
||||
|
||||
* Redesign user frontend. [Lang]
|
||||
|
||||
* Mods for performance; one js.min file on prod. [Lang]
|
||||
|
||||
* Improve webpack config for prod compile #23. [Lang]
|
||||
|
||||
* Ssl handling #22 && reconnection issues #20, #21. [Lang]
|
||||
|
||||
* Facebook prod settings w/ app; hwi/HWIOAuthBundle. [Lang]
|
||||
|
||||
* Refact && game reconnection and restore w/o refresh #3 && bugfix bomb explosion on opponent mines #19. [Lang]
|
||||
|
||||
* Typo in rpc. [Lang]
|
||||
|
||||
* Handle prod mysql timeout && graphics improve. [Lang]
|
||||
|
||||
* Gitignore webpacked index.js. [Lang]
|
||||
|
||||
* Add production mods. [Lang]
|
||||
|
||||
* Bugfix points saving and exploded bombs to db && you can resign #6. [Lang]
|
||||
|
||||
* Bugfix resign button existence #11. [Lang]
|
||||
|
||||
* Bugfix opponent bomb btn buzz on hover #10. [Lang]
|
||||
|
||||
* Bugfix points problem in the end #16. [Lang]
|
||||
|
||||
* Add desc to every user #9. [Lang]
|
||||
|
||||
* Clipboard - not working #8. [Lang]
|
||||
|
||||
* Random player on start #5. [Lang]
|
||||
|
||||
* Show left mines after end #2 && reduce network traffic && better active field checking method. [Lang]
|
||||
|
||||
* Some refactor #13. [Lang]
|
||||
|
||||
* Bugfix grid field render #12. [Lang]
|
||||
|
||||
* Game ends after x mines. [Lang]
|
||||
|
||||
* Add new sounds && refactor && new bg images && form redesigns. [Lang]
|
||||
|
||||
* Bugfix entities gridrow, grid && improve graph design on homepage. [Lang]
|
||||
|
||||
* Some refactor && prod settings. [Lang]
|
||||
|
||||
* Improve graphics design in game. [Lang]
|
||||
|
||||
* Bugfix grid row in entity. [Lang]
|
||||
|
||||
* Bugfix changePlayer after bomb explosion. [Lang]
|
||||
|
||||
* Improve game graph design. [Lang]
|
||||
|
||||
* Login and register form more design. [Lang]
|
||||
|
||||
* Add basic design to userbundle && refactor. [Lang]
|
||||
|
||||
* Add font-awesome. [Lang]
|
||||
|
||||
* Working user authentication w/ fb and plain login. [Lang]
|
||||
|
||||
* Add facebook login module, hwi/HWIOAuthBundle. [Lang]
|
||||
|
||||
* Login && register form overrided. [Lang]
|
||||
|
||||
* Js and config refactor. [Lang]
|
||||
|
||||
* Replace gridcol object to json array in db. [Lang]
|
||||
|
||||
* Refactor. [Lang]
|
||||
|
||||
* Save steps and point info to db. [Lang]
|
||||
|
||||
* Save the step data to db. [Lang]
|
||||
|
||||
* Renamed the acme to mineseeker && handle when the user connection has been lost. [Lang]
|
||||
|
||||
* Add player names to UI. [Lang]
|
||||
|
||||
* Add overlay && game do not start until the opponent came. [Lang]
|
||||
|
||||
* Add base64 encryption to grid when it has been sended to server. [Lang]
|
||||
|
||||
* On click opponents bomb, you cannot target && refactor. [Lang]
|
||||
|
||||
* Warning when player has been found more than 20 mines. [Lang]
|
||||
|
||||
* Bugfix center mine counter animation. [Lang]
|
||||
|
||||
* The opponent is the next when bomb is exploded. [Lang]
|
||||
|
||||
* Current username checked && refactor && remove players in channel when they are more than 2. [Lang]
|
||||
|
||||
* Send bomb info and use it on opponent. [Lang]
|
||||
|
||||
* Add sounds w/ howler. [Lang]
|
||||
|
||||
* Bugfix multiple empty fields w/ one click on opponent view. [Lang]
|
||||
|
||||
* Refact && remove sound and logging && bugfix BIGBUG - handleGridField and showAppropriateFields sort order... [Lang]
|
||||
|
||||
* Create first working communication. [Lang]
|
||||
|
||||
* Create entities and repositories. [Lang]
|
||||
|
||||
* Changed websocket default port && debug RPC. [Lang]
|
||||
|
||||
* Created working session and client handler w/ websocket. [Lang]
|
||||
|
||||
* Working websocket client and server w/o session handling and storage. [Lang]
|
||||
|
||||
* Composer update. [Lang]
|
||||
|
||||
* Improve game && start sound creating. [Lang]
|
||||
|
||||
* Refactor grid control and grid field. [Lang]
|
||||
|
||||
* Created basic game w/ table and animations. [Lang]
|
||||
|
||||
* Websocket basic setup FE & BE && working basic game w/ react && webpack & babel config. [Lang]
|
||||
|
||||
* Gitignore node_modules && add symlink to node_modules (just for install) && basic react. [Lang]
|
||||
|
||||
* Add react hello world. [Lang]
|
||||
|
||||
* Rename project in config. [Lang]
|
||||
|
||||
* Initial commit && create project in symfony3. [Lang]
|
||||
|
||||
|
||||
29
Caddyfile
Normal file
29
Caddyfile
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
{$CADDY_GLOBAL_OPTIONS}
|
||||
|
||||
frankenphp {
|
||||
{$FRANKENPHP_CONFIG}
|
||||
}
|
||||
}
|
||||
|
||||
{$SERVER_NAME:localhost} {
|
||||
log
|
||||
|
||||
root * /app/public
|
||||
|
||||
encode zstd br gzip
|
||||
|
||||
# Forward scheme information to the PHP application
|
||||
header X-Forwarded-Proto {scheme}
|
||||
header X-Forwarded-Host {host}
|
||||
|
||||
mercure {
|
||||
transport_url {$MERCURE_TRANSPORT_URL:bolt:///data/mercure.db}
|
||||
publisher_jwt {$MERCURE_JWT_SECRET} HS256
|
||||
subscriber_jwt {$MERCURE_JWT_SECRET} HS256
|
||||
anonymous
|
||||
subscriptions
|
||||
}
|
||||
|
||||
php_server
|
||||
}
|
||||
52
Dockerfile
Normal file
52
Dockerfile
Normal file
@@ -0,0 +1,52 @@
|
||||
FROM oven/bun:1-alpine AS assets
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json bun.lock ./
|
||||
RUN bun install --frozen-lockfile
|
||||
|
||||
COPY vite.config.js ./
|
||||
COPY assets/ assets/
|
||||
COPY public/ public/
|
||||
|
||||
RUN bun run build
|
||||
|
||||
FROM dunglas/frankenphp:latest
|
||||
|
||||
RUN install-php-extensions \
|
||||
pdo_pgsql \
|
||||
gd \
|
||||
intl \
|
||||
zip \
|
||||
opcache \
|
||||
apcu \
|
||||
sodium
|
||||
|
||||
RUN mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini"
|
||||
RUN printf '[opcache]\nopcache.enable=1\nopcache.memory_consumption=256\nopcache.max_accelerated_files=20000\nopcache.validate_timestamps=0\n' \
|
||||
> "$PHP_INI_DIR/conf.d/opcache.ini"
|
||||
|
||||
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
|
||||
|
||||
ENV APP_ENV=prod
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN composer install \
|
||||
--no-dev \
|
||||
--no-interaction \
|
||||
--no-scripts \
|
||||
--optimize-autoloader
|
||||
|
||||
COPY --from=assets /app/public/build ./public/build
|
||||
|
||||
RUN mkdir -p var/cache var/log var/sessions && \
|
||||
chown -R www-data:www-data var/
|
||||
|
||||
COPY Caddyfile /etc/caddy/Caddyfile
|
||||
COPY docker/entrypoint.sh /entrypoint.sh
|
||||
RUN chmod +x /entrypoint.sh
|
||||
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
||||
124
LICENSE
Normal file
124
LICENSE
Normal file
@@ -0,0 +1,124 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2026 SplendidBear (https://www.splendidbear.org)
|
||||
|
||||
MineSeeker is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
MineSeeker is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
---
|
||||
|
||||
TERMS AND CONDITIONS:
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of works.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this License.
|
||||
|
||||
"You" refers to each licensee.
|
||||
|
||||
"Legal entities" means the union of the acting entity and all other entities
|
||||
that control, are controlled by, or are under common control with that entity.
|
||||
|
||||
"Modify" means to copy from or adapt all or part of the work in a fashion
|
||||
requiring copyright permission, other than the making of an exact copy.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work for making
|
||||
modifications to it. "Object code" means any non-source form of a work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of copyright
|
||||
on the Program. You are granted all permissions necessary to run, modify and
|
||||
propagate covered works by this License.
|
||||
|
||||
3. Copyleft - Derivative Works.
|
||||
|
||||
If you modify the Program, your modified version must:
|
||||
|
||||
- Carry prominent notices stating that you have modified it
|
||||
- License the entire work under this License or a compatible license
|
||||
- Make the source code available to recipients
|
||||
- Preserve all notices of previous licensing
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you receive it,
|
||||
provided that you:
|
||||
|
||||
- Keep intact all notices of authorship and licensing
|
||||
- Give recipients access to the source code along with this License
|
||||
- Do not modify anything except the License itself
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program under this License provided that:
|
||||
|
||||
- The work must be licensed as a whole under this License
|
||||
- You must give prominent notice of any modifications
|
||||
- You must provide access to the Corresponding Source code
|
||||
- You preserve all licensing notices
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
If you convey object code or compiled versions, you must also provide:
|
||||
|
||||
- The Corresponding Source code (in machine-readable form)
|
||||
- A notice of the terms under which it is licensed
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
No additional restrictions may be placed on the exercise of the rights
|
||||
granted or affirmed under this License.
|
||||
|
||||
8. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
9. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING
|
||||
ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF
|
||||
THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS
|
||||
OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR
|
||||
THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
---
|
||||
|
||||
For the complete GPL-3.0-or-later license text, visit:
|
||||
https://www.gnu.org/licenses/gpl-3.0.html
|
||||
|
||||
For more information about GNU GPL, visit:
|
||||
https://www.gnu.org/licenses/
|
||||
|
||||
MineSeeker is a multiplayer minesweeper game inspired by MSN Messenger's game.
|
||||
Project: https://www.mineseeker.hu
|
||||
Author: SplendidBear (https://www.splendidbear.org)
|
||||
|
||||
|
||||
105
Makefile
Normal file
105
Makefile
Normal file
@@ -0,0 +1,105 @@
|
||||
.PHONY: help start start-build stop build down ps logs prune-everything db-reset mercure-jwt cache-clear og-cache-clear test-db-setup test-db-reset test
|
||||
|
||||
.DEFAULT_GOAL := help
|
||||
|
||||
help:
|
||||
@echo "Available commands:"
|
||||
@echo " make start - Start services without building (uses existing images)"
|
||||
@echo " make start-build - Start services and build images if needed"
|
||||
@echo " make stop - Stop running services"
|
||||
@echo " make build - Build Docker images only"
|
||||
@echo " make down - Stop and remove containers/networks"
|
||||
@echo " make prune-everything - Prune volumes, networks and images (DANGEROUS!)"
|
||||
@echo " make db-reset - Reset the database (drop, create, migrate) (DANGEROUS!)"
|
||||
@echo " make ccp - Clear the production cache"
|
||||
@echo " make cache-clear - Clear all caches (Vite, Symfony, node_modules)"
|
||||
@echo " make og-cache-clear - Clear Open Graph cache only"
|
||||
@echo " make test-db-setup - One-time setup: Create test database and run migrations"
|
||||
@echo " make test-db-reset - Reset test database (drop, create, migrate)"
|
||||
@echo " make test - Run PHPUnit tests"
|
||||
|
||||
start:
|
||||
docker compose up -d
|
||||
|
||||
start-build:
|
||||
composer i
|
||||
bun run build
|
||||
docker compose up -d --build
|
||||
|
||||
stop:
|
||||
docker compose stop
|
||||
|
||||
build:
|
||||
docker compose build
|
||||
|
||||
down:
|
||||
docker compose down
|
||||
|
||||
prune-everything:
|
||||
@echo "WARNING: This will remove ALL containers, volumes, networks and images!"
|
||||
@read -p "Type 'yes' to confirm: " confirm; \
|
||||
if [ "$$confirm" != "yes" ]; then \
|
||||
echo "Aborted."; \
|
||||
exit 1; \
|
||||
fi
|
||||
docker compose down -v --rmi all --remove-orphans
|
||||
|
||||
mercure-jwt:
|
||||
@php bin/generate-mercure-jwt.php
|
||||
|
||||
db-reset:
|
||||
@echo "WARNING: This will DROP and RECREATE the database!"
|
||||
@read -p "Type 'yes' to confirm: " confirm; \
|
||||
if [ "$$confirm" != "yes" ]; then \
|
||||
echo "Aborted."; \
|
||||
exit 1; \
|
||||
fi
|
||||
bin/console doctrine:database:drop --force --if-exists --no-interaction
|
||||
bin/console doctrine:database:create --if-not-exists --no-interaction
|
||||
bin/console doctrine:migrations:migrate --no-interaction
|
||||
|
||||
ccp:
|
||||
bin/console cache:clear --no-warmup --env=prod
|
||||
|
||||
cache-clear:
|
||||
@echo "Clearing all caches..."
|
||||
@rm -rf node_modules/.vite
|
||||
@rm -rf .vite
|
||||
@rm -rf var/og-cache
|
||||
@php bin/console cache:clear --no-warmup
|
||||
@echo "✓ Vite cache cleared"
|
||||
@echo "✓ OG cache cleared"
|
||||
@echo "✓ Symfony cache cleared"
|
||||
@echo ""
|
||||
@echo "Rebuilding assets..."
|
||||
@bun run build
|
||||
@echo ""
|
||||
@echo "✓ All caches cleared and assets rebuilt!"
|
||||
@echo " Next step: Refresh browser with Ctrl+Shift+R"
|
||||
|
||||
og-cache-clear:
|
||||
@echo "Clearing Open Graph cache..."
|
||||
@rm -rf var/og-cache
|
||||
@echo "✓ OG cache cleared!"
|
||||
@echo " Battle card images will be regenerated on next access"
|
||||
|
||||
test-db-setup:
|
||||
@echo "Setting up test database..."
|
||||
@bin/console dbal:run-sql "SELECT 1 FROM pg_database WHERE datname='mineseeker_test'" 2>/dev/null | grep -q 1 || \
|
||||
(bin/console dbal:run-sql "CREATE DATABASE mineseeker_test" && echo "✓ Database 'mineseeker_test' created")
|
||||
@bin/console doctrine:migrations:migrate --env=test --no-interaction --allow-no-migration 2>&1 | grep -v "WARNING" || true
|
||||
@echo "✓ Test database setup complete!"
|
||||
@echo " Database: mineseeker_test"
|
||||
@echo " Run tests with: make test"
|
||||
|
||||
test-db-reset:
|
||||
@echo "Resetting test database..."
|
||||
@bin/console dbal:run-sql "DROP DATABASE IF EXISTS mineseeker_test" --quiet
|
||||
@bin/console dbal:run-sql "CREATE DATABASE mineseeker_test" --quiet
|
||||
@bin/console doctrine:migrations:migrate --env=test --no-interaction --quiet
|
||||
@echo "✓ Test database reset complete!"
|
||||
@echo " Database: mineseeker_test"
|
||||
@echo " Run tests with: make test"
|
||||
|
||||
test:
|
||||
@php -d memory_limit=512M bin/phpunit --testdox --colors=always
|
||||
382
README.md
382
README.md
@@ -1,35 +1,379 @@
|
||||
This is a Symfony 3 project w/ React JS in standalone mode and w/ WebSocket.
|
||||
# MineSeeker
|
||||
|
||||
0.) Must installed modules w/ npm are in package.json + to global:
|
||||
A real-time **1v1 multiplayer minesweeper** game played in the browser.
|
||||
Two players race on the same hidden minefield — uncover safe cells to score points, but hit a mine and you hand the advantage to your opponent.
|
||||
Games are live and synchronised instantly via a Mercure hub; no page reloads, no polling.
|
||||
|
||||
$ npm install webpack -g
|
||||
Created by [SplendidBear](https://www.splendidbear.org).
|
||||
|
||||
You will need a
|
||||
.babelrc file w/ the presets
|
||||
webpack.config.js - https://webpack.github.io/docs/webpack-for-browserify-users.html
|
||||
same as dir where the package.json!!
|
||||
---
|
||||
|
||||
(!) Tutorial: https://egghead.io/lessons/react-introduction-to-properties
|
||||
## Features
|
||||
|
||||
- **Real-time 1v1 gameplay** — moves broadcast instantly over Mercure (server-sent events)
|
||||
- **Guest & registered play** — jump in anonymously or create an account for stats and history
|
||||
- **Full authentication stack** — email/password, passkeys (WebAuthn), TOTP 2FA with backup codes
|
||||
- **Player profiles** — win/loss/draw stats, per-month charts, recent battle history with shareable replay links
|
||||
- **Profile pictures** — uploaded to MinIO object storage, thumbnails generated on-the-fly by LiipImagine
|
||||
- **Battle replay sharing** — share a direct link to any finished game
|
||||
- **Docker-ready** — single `make start-build` brings up the full production-like stack
|
||||
|
||||
---
|
||||
|
||||
1.) Backend WebSocket server start as daemon - GeniusesOfSymfony/WebSocketBundle
|
||||
## Tech stack
|
||||
|
||||
$ nohup bin/console gos:websocket:server &
|
||||
| Layer | Technology |
|
||||
|---|---|
|
||||
| Backend | PHP 8.5, Symfony 7.4, Doctrine ORM |
|
||||
| Frontend | React 19, Vite, MUI, SCSS |
|
||||
| Database | PostgreSQL 18 |
|
||||
| Real-time | Mercure (built into FrankenPHP / Caddy) |
|
||||
| File storage | MinIO (S3-compatible) |
|
||||
| Image processing | LiipImagine + Flysystem |
|
||||
| Server | FrankenPHP (Caddy + PHP in one binary) |
|
||||
| Auth | Symfony Security, Scheb 2FA, web-auth/webauthn-framework |
|
||||
|
||||
2.) React JS WebPack watch generator w/ babel presets: es2015, react
|
||||
---
|
||||
|
||||
$ webpack -p --config=webpack-prod.config.js
|
||||
## Requirements
|
||||
|
||||
PROD
|
||||
### Bare-metal development
|
||||
- PHP >= 8.5 with extensions: `pdo_pgsql`, `gd`, `intl`, `zip`, `sodium`
|
||||
- Composer 2
|
||||
- Node.js 22 + Bun
|
||||
- PostgreSQL 18
|
||||
- Caddy with FrankenPHP and the Mercure module
|
||||
- MailHog (or any SMTP server on port 1025)
|
||||
- MinIO (listening on port 9000)
|
||||
|
||||
$ webpack --progress --colors --watch -d
|
||||
### Docker
|
||||
- Docker Engine 24+
|
||||
- Docker Compose v2
|
||||
- Bun (for building frontend assets before `make start-build`)
|
||||
- Composer (for `make start-build`)
|
||||
|
||||
DEV
|
||||
---
|
||||
|
||||
-d --> Debugger; If you write this line somewhere: debugger;
|
||||
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
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Documentation
|
||||
|
||||
For detailed information about game mechanics, bonus systems, fonts, testing, and other technical details, see the [docs](./docs/) directory:
|
||||
|
||||
- **[AI Agent Guidelines](./AGENTS.md)** — Comprehensive guide for AI coding agents working on this project
|
||||
- **[Bonus Points System](./docs/game-mechanics/BONUS_POINTS_SYSTEM.md)** — Complete reference for all bonus point types, calculation rules, and implementation details
|
||||
- **[Fonts](./docs/FONTS.md)** — TrueType fonts used for server-side image generation
|
||||
- **[Testing Guide](./docs/testing/TESTING.md)** — Complete testing setup with Foundry factories, database isolation, and best practices
|
||||
- **[Factory Documentation](./docs/testing/FACTORIES.md)** — Detailed API reference for all test data factories
|
||||
|
||||
---
|
||||
|
||||
## License
|
||||
|
||||
LGPL-3.0 — see [LICENSE](LICENSE) for details.
|
||||
|
||||
© 2026 [SplendidBear](https://www.splendidbear.org)
|
||||
|
||||
---
|
||||
|
||||
## Testing & CI/CD
|
||||
|
||||
MineSeeker has a comprehensive test suite with **71 automated tests** and continuous integration/deployment pipelines.
|
||||
|
||||
### Quick Start
|
||||
|
||||
```bash
|
||||
# Setup test database (first time only)
|
||||
make test-db-setup
|
||||
|
||||
# Run all tests
|
||||
make test
|
||||
|
||||
# Run with documentation output
|
||||
vendor/bin/phpunit --testdox
|
||||
```
|
||||
|
||||
### Test Suite
|
||||
|
||||
- **71 tests** with **227 assertions**
|
||||
- **Controller tests** - HTTP endpoints, authentication, routing
|
||||
- **DTO tests** - Data serialization and calculations
|
||||
- **Entity tests** - Domain logic and defaults
|
||||
- **Service tests** - Business logic and external APIs
|
||||
- **Integration tests** - Foundry factories and database isolation
|
||||
|
||||
**Test execution time:** ~6-8 seconds
|
||||
|
||||
### Continuous Integration
|
||||
|
||||
**Automated testing** runs on every push/pull request:
|
||||
|
||||
```yaml
|
||||
# .gitea/workflows/ci.yml-bak
|
||||
✓ PHP 8.3 setup with all extensions
|
||||
✓ PostgreSQL 18 service container
|
||||
✓ Composer and npm dependency installation
|
||||
✓ Asset building with Vite
|
||||
✓ Database migrations
|
||||
✓ Full test suite execution
|
||||
✓ Code linting (ESLint, PHP-CS-Fixer)
|
||||
```
|
||||
|
||||
### Continuous Deployment
|
||||
|
||||
**Automated deployment** on version tags (e.g., `v1.2.3`):
|
||||
|
||||
```yaml
|
||||
# .gitea/workflows/deploy.yml
|
||||
1. Run full test suite (blocks deployment if fails)
|
||||
2. Checkout tagged version
|
||||
3. Build Docker image
|
||||
4. Run database migrations
|
||||
5. Restart services
|
||||
6. Health check verification
|
||||
```
|
||||
|
||||
**Deploy to production:**
|
||||
```bash
|
||||
git tag -a v1.2.3 -m "Release version 1.2.3"
|
||||
git push origin v1.2.3
|
||||
```
|
||||
|
||||
### Documentation
|
||||
|
||||
- **[Testing Guide](docs/testing/TESTING.md)** - Comprehensive testing documentation
|
||||
- **[Factory Reference](docs/testing/FACTORIES.md)** - Foundry factory API
|
||||
- **[CI/CD Guide](docs/CI_CD.md)** - Pipeline configuration and workflows
|
||||
|
||||
---
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
<IfModule mod_authz_core.c>
|
||||
Require all denied
|
||||
</IfModule>
|
||||
<IfModule !mod_authz_core.c>
|
||||
Order deny,allow
|
||||
Deny from all
|
||||
</IfModule>
|
||||
@@ -1,7 +0,0 @@
|
||||
<?php
|
||||
|
||||
use Symfony\Bundle\FrameworkBundle\HttpCache\HttpCache;
|
||||
|
||||
class AppCache extends HttpCache
|
||||
{
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
<?php
|
||||
|
||||
use Symfony\Component\HttpKernel\Kernel;
|
||||
use Symfony\Component\Config\Loader\LoaderInterface;
|
||||
|
||||
class AppKernel extends Kernel
|
||||
{
|
||||
public function registerBundles()
|
||||
{
|
||||
$bundles = [
|
||||
new Symfony\Bundle\FrameworkBundle\FrameworkBundle(),
|
||||
new Symfony\Bundle\SecurityBundle\SecurityBundle(),
|
||||
new Symfony\Bundle\TwigBundle\TwigBundle(),
|
||||
new Symfony\Bundle\MonologBundle\MonologBundle(),
|
||||
new Symfony\Bundle\SwiftmailerBundle\SwiftmailerBundle(),
|
||||
new Doctrine\Bundle\DoctrineBundle\DoctrineBundle(),
|
||||
new Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle(),
|
||||
|
||||
new Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle(),
|
||||
new Symfony\Bundle\AsseticBundle\AsseticBundle(),
|
||||
new FOS\UserBundle\FOSUserBundle(),
|
||||
new HWI\Bundle\OAuthBundle\HWIOAuthBundle(),
|
||||
new Gos\Bundle\WebSocketBundle\GosWebSocketBundle(),
|
||||
new Gos\Bundle\PubSubRouterBundle\GosPubSubRouterBundle(),
|
||||
new Doctrine\Bundle\DoctrineCacheBundle\DoctrineCacheBundle(),
|
||||
new Snc\RedisBundle\SncRedisBundle(),
|
||||
new CL\Bundle\SlackBundle\CLSlackBundle(),
|
||||
|
||||
new Jotunheimr\AdminBundle\JotunheimrAdminBundle(),
|
||||
new Jotunheimr\UserBundle\JotunheimrUserBundle(),
|
||||
new Mine\SeekerBundle\MineSeekerBundle(),
|
||||
];
|
||||
|
||||
if (in_array($this->getEnvironment(), ['dev', 'test'], true)) {
|
||||
$bundles[] = new Symfony\Bundle\DebugBundle\DebugBundle();
|
||||
$bundles[] = new Symfony\Bundle\WebProfilerBundle\WebProfilerBundle();
|
||||
$bundles[] = new Sensio\Bundle\DistributionBundle\SensioDistributionBundle();
|
||||
$bundles[] = new Sensio\Bundle\GeneratorBundle\SensioGeneratorBundle();
|
||||
}
|
||||
|
||||
return $bundles;
|
||||
}
|
||||
|
||||
public function getRootDir()
|
||||
{
|
||||
return __DIR__;
|
||||
}
|
||||
|
||||
public function getCacheDir()
|
||||
{
|
||||
return dirname(__DIR__).'/var/cache/'.$this->getEnvironment();
|
||||
}
|
||||
|
||||
public function getLogDir()
|
||||
{
|
||||
return dirname(__DIR__).'/var/logs';
|
||||
}
|
||||
|
||||
public function registerContainerConfiguration(LoaderInterface $loader)
|
||||
{
|
||||
$loader->load($this->getRootDir().'/config/config_'.$this->getEnvironment().'.yml');
|
||||
}
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="Cache-control" content="max-age=1209600;public">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="keywords" content="game,mineseeker,mine,seeker,laszlolang.com">
|
||||
<meta name="robots" content="index,follow">
|
||||
<meta name="revisit-after" content="2 days">
|
||||
<meta name="resource-type" content="document">
|
||||
<meta name="country" content="Hungary">
|
||||
<meta name="description" content="This is a new minesweeper, multiplayer game.">
|
||||
<meta name="content-language" content="hu,hun,hungarian">
|
||||
{% include '@MineSeeker/Recent/favicon.html.twig' %}
|
||||
<meta property="fb:app_id" content="{{ facebook_api }}">
|
||||
{% block metas %}{% endblock %}
|
||||
<title>MineSeeker{% block title %}{% endblock %}</title>
|
||||
{% block stylesheets %}{% endblock %}
|
||||
{% include '@MineSeeker/Recent/google-analytics.html.twig' %}
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="fb-root"></div>
|
||||
|
||||
{% block bodyTop %}{% endblock %}
|
||||
|
||||
<header>
|
||||
{% block header %}{% endblock %}
|
||||
</header>
|
||||
<main>
|
||||
{% block body %}{% endblock %}
|
||||
</main>
|
||||
<footer>
|
||||
{% block footer %}{% endblock %}
|
||||
</footer>
|
||||
|
||||
{% block javascripts %}
|
||||
{% javascripts filter='?uglifyjs2'
|
||||
'@JotunheimrAdminBundle/Resources/public/js/vendor/plugins/jQuery/jquery-3.0.0.min.js'
|
||||
'@JotunheimrAdminBundle/Resources/public/js/vendor/plugins/jQuery/jquery-migrate-3.0.0.min.js'
|
||||
'@JotunheimrAdminBundle/Resources/public/js/vendor/bootstrap/js/bootstrap.min.js' %}
|
||||
<script type="text/javascript" src="{{ asset_url }}"></script>
|
||||
{% endjavascripts %}
|
||||
|
||||
{% include '@MineSeeker/Recent/facebook.html.twig' %}
|
||||
{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,13 +0,0 @@
|
||||
<?php
|
||||
|
||||
use Doctrine\Common\Annotations\AnnotationRegistry;
|
||||
use Composer\Autoload\ClassLoader;
|
||||
|
||||
/**
|
||||
* @var ClassLoader $loader
|
||||
*/
|
||||
$loader = require __DIR__.'/../vendor/autoload.php';
|
||||
|
||||
AnnotationRegistry::registerLoader([$loader, 'loadClass']);
|
||||
|
||||
return $loader;
|
||||
@@ -1,107 +0,0 @@
|
||||
imports:
|
||||
- { resource: security.yml }
|
||||
- { resource: services.yml }
|
||||
- { resource: "@JotunheimrAdminBundle/Resources/config/config.yml" }
|
||||
- { resource: "@JotunheimrUserBundle/Resources/config/config.yml" }
|
||||
- { resource: "@MineSeekerBundle/Resources/config/config.yml" }
|
||||
- { resource: "@MineSeekerBundle/Resources/config/services.yml" }
|
||||
|
||||
# Put parameters here that don't need to change on each machine where the app is deployed
|
||||
# http://symfony.com/doc/current/best_practices/configuration.html#application-related-configuration
|
||||
parameters:
|
||||
locale: en
|
||||
|
||||
framework:
|
||||
#esi: ~
|
||||
translator: { fallbacks: ["%locale%"] }
|
||||
secret: "%secret%"
|
||||
router:
|
||||
resource: "%kernel.root_dir%/config/routing.yml"
|
||||
strict_requirements: ~
|
||||
form: ~
|
||||
csrf_protection: ~
|
||||
validation: { enable_annotations: true }
|
||||
#serializer: { enable_annotations: true }
|
||||
templating:
|
||||
engines: ['twig']
|
||||
default_locale: "%locale%"
|
||||
trusted_hosts: ~
|
||||
trusted_proxies: ~
|
||||
session:
|
||||
# http://symfony.com/doc/current/reference/configuration/framework.html#handler-id
|
||||
# handler_id: session.handler.native_file
|
||||
# save_path: "%kernel.root_dir%/../var/sessions/%kernel.environment%"
|
||||
handler_id: session.handler.pdo
|
||||
fragments: ~
|
||||
http_method_override: true
|
||||
assets: ~
|
||||
|
||||
# Twig Configuration
|
||||
twig:
|
||||
debug: "%kernel.debug%"
|
||||
strict_variables: "%kernel.debug%"
|
||||
globals:
|
||||
version: "0.37.18 (beta7)"
|
||||
facebook_api: "%facebook.api%"
|
||||
facebook_scope: "%facebook.scope%"
|
||||
facebook_api_version: "%facebook.version%"
|
||||
|
||||
# Doctrine Configuration
|
||||
doctrine:
|
||||
dbal:
|
||||
driver: pdo_mysql
|
||||
host: "%database_host%"
|
||||
port: "%database_port%"
|
||||
dbname: "%database_name%"
|
||||
user: "%database_user%"
|
||||
password: "%database_password%"
|
||||
charset: UTF8
|
||||
# if using pdo_sqlite as your database driver:
|
||||
# 1. add the path in parameters.yml
|
||||
# e.g. database_path: "%kernel.root_dir%/data/data.db3"
|
||||
# 2. Uncomment database_path in parameters.yml.dist
|
||||
# 3. Uncomment next line:
|
||||
# path: "%database_path%"
|
||||
|
||||
orm:
|
||||
auto_generate_proxy_classes: "%kernel.debug%"
|
||||
naming_strategy: doctrine.orm.naming_strategy.underscore
|
||||
auto_mapping: true
|
||||
|
||||
# Assetic Configuration
|
||||
assetic:
|
||||
debug: "%kernel.debug%"
|
||||
use_controller: "%kernel.debug%"
|
||||
bundles: ~
|
||||
node: /usr/bin/nodejs
|
||||
filters:
|
||||
cssrewrite: ~
|
||||
uglifyjs2:
|
||||
bin: "%kernel.root_dir%/../node_modules/uglify-js/bin/uglifyjs"
|
||||
no_copyright: true
|
||||
uglifycss:
|
||||
bin: "%kernel.root_dir%/../node_modules/uglifycss/uglifycss"
|
||||
|
||||
# FOS User Configuration
|
||||
fos_user:
|
||||
db_driver: orm # other valid values are 'mongodb', 'couchdb' and 'propel'
|
||||
firewall_name: secured_area
|
||||
user_class: Jotunheimr\UserBundle\Entity\User
|
||||
|
||||
# Facebook OAuth
|
||||
hwi_oauth:
|
||||
firewall_names: [secured_area]
|
||||
resource_owners:
|
||||
facebook:
|
||||
type: facebook
|
||||
client_id: "%facebook.api%"
|
||||
client_secret: "%facebook.api-secret%"
|
||||
scope: "%facebook.scope%"
|
||||
options:
|
||||
display: popup
|
||||
auth_type: rerequest
|
||||
csrf: true
|
||||
|
||||
# Slack integration
|
||||
cl_slack:
|
||||
api_token: xoxp-107639806167-107029084564-115427085733-cccaa4f96c89c87ce680c7f22acfd001
|
||||
@@ -1,40 +0,0 @@
|
||||
imports:
|
||||
- { resource: parameters_dev.yml }
|
||||
- { resource: config.yml }
|
||||
|
||||
framework:
|
||||
router:
|
||||
resource: "%kernel.root_dir%/config/routing_dev.yml"
|
||||
strict_requirements: true
|
||||
profiler: { only_exceptions: false }
|
||||
|
||||
web_profiler:
|
||||
toolbar: true
|
||||
intercept_redirects: false
|
||||
|
||||
|
||||
# Swiftmailer Configuration
|
||||
swiftmailer:
|
||||
transport: "%mailer_transport%"
|
||||
host: "%mailer_host%"
|
||||
username: "%mailer_user%"
|
||||
password: "%mailer_password%"
|
||||
spool: { type: memory }
|
||||
|
||||
monolog:
|
||||
handlers:
|
||||
main:
|
||||
type: stream
|
||||
path: "%kernel.logs_dir%/%kernel.environment%.log"
|
||||
level: debug
|
||||
channels: [!event]
|
||||
console:
|
||||
type: console
|
||||
channels: [!event, !doctrine]
|
||||
|
||||
parameters:
|
||||
facebook.api: 320599508311862
|
||||
facebook.api-secret: 18d4f48cdd274bccee2678e5eff3f557
|
||||
facebook.version: 'v2.8'
|
||||
facebook.scope: 'public_profile,email,user_friends'
|
||||
mineseeker.websocket: 6450
|
||||
@@ -1,31 +0,0 @@
|
||||
imports:
|
||||
- { resource: parameters_prod.yml }
|
||||
- { resource: config.yml }
|
||||
|
||||
# Swiftmailer Configuration
|
||||
swiftmailer:
|
||||
transport: "%mailer_transport%"
|
||||
host: "%mailer_host%"
|
||||
username: "%mailer_user%"
|
||||
password: "%mailer_password%"
|
||||
spool: { type: memory }
|
||||
|
||||
monolog:
|
||||
handlers:
|
||||
main:
|
||||
type: fingers_crossed
|
||||
action_level: error
|
||||
handler: nested
|
||||
nested:
|
||||
type: stream
|
||||
path: "%kernel.logs_dir%/%kernel.environment%.log"
|
||||
level: debug
|
||||
console:
|
||||
type: console
|
||||
|
||||
parameters:
|
||||
facebook.api: 320597498312063
|
||||
facebook.api-secret: c751bec8a3c5313ff2e5a83769bf1109
|
||||
facebook.version: 'v2.8'
|
||||
facebook.scope: 'public_profile,email,user_friends'
|
||||
mineseeker.websocket: 8080
|
||||
@@ -1,16 +0,0 @@
|
||||
imports:
|
||||
- { resource: config_dev.yml }
|
||||
|
||||
framework:
|
||||
test: ~
|
||||
session:
|
||||
storage_id: session.storage.mock_file
|
||||
profiler:
|
||||
collect: false
|
||||
|
||||
web_profiler:
|
||||
toolbar: false
|
||||
intercept_redirects: false
|
||||
|
||||
swiftmailer:
|
||||
disable_delivery: true
|
||||
@@ -1,19 +0,0 @@
|
||||
# This file is a "template" of what your parameters.yml file should look like
|
||||
# Set parameters here that may be different on each deployment target of the app, e.g. development, staging, production.
|
||||
# http://symfony.com/doc/current/best_practices/configuration.html#infrastructure-related-configuration
|
||||
parameters:
|
||||
database_host: 127.0.0.1
|
||||
database_port: ~
|
||||
database_name: symfony
|
||||
database_user: root
|
||||
database_password: ~
|
||||
# You should uncomment this if you want use pdo_sqlite
|
||||
# database_path: "%kernel.root_dir%/data.db3"
|
||||
|
||||
mailer_transport: smtp
|
||||
mailer_host: 127.0.0.1
|
||||
mailer_user: ~
|
||||
mailer_password: ~
|
||||
|
||||
# A secret key that's used to generate certain security-related tokens
|
||||
secret: ThisTokenIsNotSoSecretChangeIt
|
||||
@@ -1,12 +0,0 @@
|
||||
# This file is auto-generated during the composer install
|
||||
parameters:
|
||||
database_host: 127.0.0.1
|
||||
database_port: null
|
||||
database_name: mine
|
||||
database_user: root
|
||||
database_password: bazmeg
|
||||
mailer_transport: smtp
|
||||
mailer_host: 127.0.0.1
|
||||
mailer_user: null
|
||||
mailer_password: null
|
||||
secret: bbcd5df99fc340558fb3995c198a9b4764db72ba
|
||||
@@ -1,15 +0,0 @@
|
||||
# This file is auto-generated during the composer install
|
||||
parameters:
|
||||
database_host: 127.0.0.1
|
||||
database_port: null
|
||||
# database_name: xxsvci_mineseeker
|
||||
# database_user: xxsvci_mine
|
||||
# database_password: "XTw#8qC$faa*"
|
||||
database_name: mine
|
||||
database_user: root
|
||||
database_password: "bazmeg"
|
||||
mailer_transport: smtp
|
||||
mailer_host: 127.0.0.1
|
||||
mailer_user: null
|
||||
mailer_password: null
|
||||
secret: e25d036bb9c7ece0f2049984a1fa2f0cab295aaa
|
||||
@@ -1,14 +0,0 @@
|
||||
fos_user:
|
||||
resource: "@FOSUserBundle/Resources/config/routing/all.xml"
|
||||
|
||||
JotunheimrUserBundle:
|
||||
resource: "@JotunheimrUserBundle/Resources/config/routing.yml"
|
||||
prefix: /
|
||||
|
||||
JotunheimrAdminBundle:
|
||||
resource: "@JotunheimrAdminBundle/Resources/config/routing.yml"
|
||||
prefix: /
|
||||
|
||||
MineSeekerBundle:
|
||||
resource: "@MineSeekerBundle/Resources/config/routing.yml"
|
||||
prefix: /
|
||||
@@ -1,15 +0,0 @@
|
||||
_wdt:
|
||||
resource: "@WebProfilerBundle/Resources/config/routing/wdt.xml"
|
||||
prefix: /_wdt
|
||||
|
||||
_profiler:
|
||||
resource: "@WebProfilerBundle/Resources/config/routing/profiler.xml"
|
||||
prefix: /_profiler
|
||||
|
||||
_errors:
|
||||
resource: "@TwigBundle/Resources/config/routing/errors.xml"
|
||||
prefix: /_error
|
||||
|
||||
_main:
|
||||
resource: routing.yml
|
||||
schemes: [http]
|
||||
@@ -1,46 +0,0 @@
|
||||
security:
|
||||
encoders:
|
||||
FOS\UserBundle\Model\UserInterface: bcrypt
|
||||
|
||||
role_hierarchy:
|
||||
ROLE_ADMIN: ROLE_USER
|
||||
ROLE_SUPER_ADMIN: ROLE_ADMIN
|
||||
|
||||
providers:
|
||||
fos_userbundle:
|
||||
id: fos_user.user_provider.username_email
|
||||
|
||||
firewalls:
|
||||
secured_area:
|
||||
anonymous: ~
|
||||
oauth:
|
||||
resource_owners:
|
||||
facebook: /login/check-facebook
|
||||
google: /login/check-google
|
||||
my_github: /login/check-github
|
||||
login_path: /login
|
||||
failure_path: /login
|
||||
use_forward: false
|
||||
oauth_user_provider:
|
||||
service: jotun.user_provider
|
||||
remember_me:
|
||||
secret: "%secret%"
|
||||
lifetime: 604800
|
||||
path: /
|
||||
domain: ~
|
||||
user_provider: fos_userbundle
|
||||
form_login:
|
||||
provider: fos_userbundle
|
||||
csrf_token_generator: security.csrf.token_manager
|
||||
default_target_path: /
|
||||
remember_me: true
|
||||
logout:
|
||||
path: /logout
|
||||
target: /
|
||||
|
||||
access_control:
|
||||
- { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
|
||||
- { path: ^/register, role: IS_AUTHENTICATED_ANONYMOUSLY }
|
||||
- { path: ^/resetting, role: IS_AUTHENTICATED_ANONYMOUSLY }
|
||||
- { path: ^/play, role: IS_AUTHENTICATED_ANONYMOUSLY }
|
||||
- { path: ^/admin, role: ROLE_SUPER_ADMIN }
|
||||
@@ -1,9 +0,0 @@
|
||||
# Learn more about services, parameters and containers at
|
||||
# http://symfony.com/doc/current/book/service_container.html
|
||||
parameters:
|
||||
# parameter_name: value
|
||||
|
||||
services:
|
||||
# service_name:
|
||||
# class: AppBundle\Directory\ClassName
|
||||
# arguments: ["@another_service_name", "plain_value", "%parameter_name%"]
|
||||
10
assets/css/_fontawesome-config.scss
Normal file
10
assets/css/_fontawesome-config.scss
Normal file
@@ -0,0 +1,10 @@
|
||||
/*!*
|
||||
* This file is part of the SplendidBear Websites' projects.
|
||||
*
|
||||
* Copyright (c) 2026 @ www.splendidbear.org
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
@import '@fortawesome/fontawesome-free/css/all.min.css';
|
||||
12
assets/css/_fonts-config.scss
Normal file
12
assets/css/_fonts-config.scss
Normal file
@@ -0,0 +1,12 @@
|
||||
/*!*
|
||||
* This file is part of the SplendidBear Websites' projects.
|
||||
*
|
||||
* Copyright (c) 2026 @ www.splendidbear.org
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
@import '@fontsource/rajdhani';
|
||||
@import '@fontsource/changa-one';
|
||||
@import '@fontsource/open-sans/700';
|
||||
18
assets/css/homepage/_animations.scss
Normal file
18
assets/css/homepage/_animations.scss
Normal file
@@ -0,0 +1,18 @@
|
||||
/*!*
|
||||
* This file is part of the SplendidBear Websites' projects.
|
||||
*
|
||||
* Copyright (c) 2026 @ www.splendidbear.org
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
@keyframes appear {
|
||||
from { opacity: 0; transform: scale(0.94); }
|
||||
to { opacity: 1; transform: scale(1); }
|
||||
}
|
||||
|
||||
@keyframes rise {
|
||||
from { opacity: 0; transform: translateY(16px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
128
assets/css/homepage/_auth-bar.scss
Normal file
128
assets/css/homepage/_auth-bar.scss
Normal file
@@ -0,0 +1,128 @@
|
||||
/*!*
|
||||
* This file is part of the SplendidBear Websites' projects.
|
||||
*
|
||||
* Copyright (c) 2026 @ www.splendidbear.org
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
#hero-auth {
|
||||
padding: 20px;
|
||||
|
||||
.hero-auth {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.hero-auth-user {
|
||||
font: 600 13px 'Rajdhani', sans-serif;
|
||||
color: rgba(149, 207, 245, 0.75);
|
||||
letter-spacing: 0.5px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
|
||||
i { font-size: 15px; }
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1100px) {
|
||||
.hero-auth {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.hero-auth-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font: 600 12px 'Rajdhani', sans-serif;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1.5px;
|
||||
text-decoration: none;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
border-radius: 4px;
|
||||
padding: 7px 14px;
|
||||
cursor: pointer;
|
||||
transition: all 200ms ease;
|
||||
|
||||
&:hover {
|
||||
color: #fff;
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
border-color: rgba(255, 255, 255, 0.25);
|
||||
}
|
||||
|
||||
&--register {
|
||||
color: rgba(149, 207, 245, 0.8);
|
||||
border-color: rgba(35, 111, 135, 0.4);
|
||||
background: rgba(35, 111, 135, 0.12);
|
||||
|
||||
&:hover {
|
||||
color: #fff;
|
||||
background: rgba(35, 111, 135, 0.28);
|
||||
border-color: rgba(149, 207, 245, 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
&--security {
|
||||
color: rgba(149, 207, 245, 0.55);
|
||||
border-color: rgba(35, 111, 135, 0.22);
|
||||
background: transparent;
|
||||
|
||||
&:hover {
|
||||
color: rgba(149, 207, 245, 0.9);
|
||||
background: rgba(35, 111, 135, 0.14);
|
||||
border-color: rgba(35, 111, 135, 0.45);
|
||||
}
|
||||
}
|
||||
|
||||
&--out {
|
||||
background: transparent;
|
||||
border-color: rgba(173, 10, 5, 0.3);
|
||||
color: rgba(246, 125, 82, 0.7);
|
||||
|
||||
&:hover {
|
||||
background: rgba(173, 10, 5, 0.15);
|
||||
border-color: rgba(246, 125, 82, 0.5);
|
||||
color: #f67d52;
|
||||
}
|
||||
}
|
||||
|
||||
&--profile {
|
||||
color: rgba(149, 207, 245, 0.8);
|
||||
border-color: rgba(35, 111, 135, 0.35);
|
||||
background: rgba(35, 111, 135, 0.08);
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
color: #fff;
|
||||
background: rgba(35, 111, 135, 0.22);
|
||||
border-color: rgba(149, 207, 245, 0.5);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.hero-auth-avatar {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
flex-shrink: 0;
|
||||
border: 1px solid rgba(35, 111, 135, 0.5);
|
||||
|
||||
&--initials {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, rgba(35, 111, 135, 0.45) 0%, rgba(173, 10, 5, 0.3) 100%);
|
||||
font: 800 9px 'Rajdhani', sans-serif;
|
||||
color: rgba(149, 207, 245, 0.9);
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
}
|
||||
474
assets/css/homepage/_auth.scss
Normal file
474
assets/css/homepage/_auth.scss
Normal file
@@ -0,0 +1,474 @@
|
||||
/*!*
|
||||
* This file is part of the SplendidBear Websites' projects.
|
||||
*
|
||||
* Copyright (c) 2026 @ www.splendidbear.org
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
.auth-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 40px 20px 80px;
|
||||
}
|
||||
|
||||
.auth-flash {
|
||||
width: 100%;
|
||||
max-width: 420px;
|
||||
padding: 12px 18px;
|
||||
border-radius: 5px;
|
||||
font: 600 14px 'Rajdhani', sans-serif;
|
||||
letter-spacing: 0.5px;
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
&--success {
|
||||
background: rgba(26, 104, 68, 0.25);
|
||||
border: 1px solid rgba(42, 158, 96, 0.4);
|
||||
color: #a0f0c0;
|
||||
}
|
||||
|
||||
&--error {
|
||||
background: rgba(173, 10, 5, 0.18);
|
||||
border: 1px solid rgba(173, 10, 5, 0.4);
|
||||
color: #f6a090;
|
||||
}
|
||||
}
|
||||
|
||||
// "Check your inbox" confirmation card
|
||||
.auth-card--sent {
|
||||
text-align: center;
|
||||
padding: 48px 40px;
|
||||
}
|
||||
|
||||
.auth-sent-icon {
|
||||
font-size: 52px;
|
||||
color: rgba(149, 207, 245, 0.6);
|
||||
margin-bottom: 20px;
|
||||
filter: drop-shadow(0 0 16px rgba(35, 111, 135, 0.4));
|
||||
}
|
||||
|
||||
.auth-sent-email {
|
||||
font: 700 16px 'Rajdhani', sans-serif;
|
||||
color: #95cff5;
|
||||
letter-spacing: 0.5px;
|
||||
margin: 0 0 20px;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.auth-sent-note {
|
||||
font: 400 14px 'Rajdhani', sans-serif;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
line-height: 1.7;
|
||||
margin-bottom: 0;
|
||||
|
||||
strong { color: rgba(255, 255, 255, 0.75); }
|
||||
}
|
||||
|
||||
.auth-card {
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
border: 1px solid rgba(35, 111, 135, 0.2);
|
||||
border-radius: 10px;
|
||||
padding: 44px 48px 40px;
|
||||
width: 100%;
|
||||
max-width: 420px;
|
||||
backdrop-filter: blur(4px);
|
||||
box-shadow: 0 8px 48px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.auth-title {
|
||||
font: 800 30px 'Rajdhani', sans-serif;
|
||||
color: #ffffff;
|
||||
letter-spacing: 1px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.auth-sub {
|
||||
font: 400 14px 'Rajdhani', sans-serif;
|
||||
color: rgba(149, 207, 245, 0.6);
|
||||
letter-spacing: 0.5px;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.auth-error {
|
||||
background: rgba(173, 10, 5, 0.18);
|
||||
border: 1px solid rgba(173, 10, 5, 0.4);
|
||||
border-radius: 5px;
|
||||
padding: 10px 14px;
|
||||
font: 600 13px 'Rajdhani', sans-serif;
|
||||
color: #f6a090;
|
||||
margin-bottom: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.auth-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.auth-field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 7px;
|
||||
}
|
||||
|
||||
.auth-label {
|
||||
font: 700 11px 'Rajdhani', sans-serif;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 2px;
|
||||
color: rgba(255, 255, 255, 0.45);
|
||||
}
|
||||
|
||||
.auth-input-wrap {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.auth-input-icon {
|
||||
position: absolute;
|
||||
left: 11px;
|
||||
color: rgba(149, 207, 245, 0.4);
|
||||
font-size: 13px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.auth-input {
|
||||
width: 100%;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid rgba(35, 111, 135, 0.3);
|
||||
border-radius: 5px;
|
||||
padding: 11px 14px 11px 34px;
|
||||
font: 500 15px 'Rajdhani', sans-serif;
|
||||
color: #ffffff;
|
||||
letter-spacing: 0.5px;
|
||||
transition: border-color 200ms ease, background 200ms ease;
|
||||
|
||||
&::placeholder { color: rgba(255, 255, 255, 0.2); }
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
background: rgba(35, 111, 135, 0.1);
|
||||
border-color: rgba(149, 207, 245, 0.5);
|
||||
}
|
||||
|
||||
&--error {
|
||||
border-color: rgba(173, 10, 5, 0.6) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.auth-field-error {
|
||||
font: 500 12px 'Rajdhani', sans-serif;
|
||||
color: #f6a090;
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
|
||||
.auth-below-password {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.auth-remember {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font: 500 13px 'Rajdhani', sans-serif;
|
||||
color: rgba(255, 255, 255, 0.45);
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
margin-top: -4px;
|
||||
|
||||
input[type="checkbox"] { accent-color: #236f87; }
|
||||
}
|
||||
|
||||
.auth-checkbox {
|
||||
accent-color: #236f87;
|
||||
cursor: pointer;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.auth-checkbox-label {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
font: 400 14px 'Rajdhani', sans-serif;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
line-height: 1.5;
|
||||
|
||||
a {
|
||||
color: #95cff5;
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
transition: color 180ms;
|
||||
|
||||
&:hover { color: #c5e8ff; }
|
||||
}
|
||||
}
|
||||
|
||||
textarea.auth-input {
|
||||
padding: 11px 14px;
|
||||
min-height: 120px;
|
||||
resize: vertical;
|
||||
font-family: 'Rajdhani', sans-serif;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.auth-submit {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
background: linear-gradient(to bottom, #ad0a05 0%, #d4401a 55%, #f67d52 100%);
|
||||
border: 1px solid rgba(246, 125, 82, 0.3);
|
||||
border-radius: 5px;
|
||||
color: #ffffff;
|
||||
font: 700 16px 'Rajdhani', sans-serif;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 3px;
|
||||
padding: 14px;
|
||||
cursor: pointer;
|
||||
margin-top: 6px;
|
||||
box-shadow: 0 4px 20px rgba(173, 10, 5, 0.35);
|
||||
transition: all 220ms ease;
|
||||
|
||||
&:hover {
|
||||
background: linear-gradient(to bottom, #c91008 0%, #e5521e 55%, #ff8c61 100%);
|
||||
box-shadow: 0 6px 28px rgba(173, 10, 5, 0.6);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
&:active { transform: translateY(0); }
|
||||
}
|
||||
|
||||
.auth-cancel {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
background: linear-gradient(to bottom, #1a1a1a 0%, #2d2d2d 55%, #404040 100%);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: 5px;
|
||||
color: #ffffff;
|
||||
font: 700 16px 'Rajdhani', sans-serif;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 3px;
|
||||
padding: 14px;
|
||||
cursor: pointer;
|
||||
margin-top: 6px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.35);
|
||||
transition: all 220ms ease;
|
||||
|
||||
&:hover {
|
||||
background: linear-gradient(to bottom, #2d2d2d 0%, #3d3d3d 55%, #505050 100%);
|
||||
box-shadow: 0 6px 28px rgba(0, 0, 0, 0.6);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
&:active { transform: translateY(0); }
|
||||
}
|
||||
|
||||
.auth-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
align-items: flex-start;
|
||||
|
||||
form {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
|
||||
&.auth-form {
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.auth-cancel-form {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
|
||||
.auth-cancel {
|
||||
flex: 1;
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.auth-cancel-standalone {
|
||||
margin-top: 12px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.auth-cancel--block {
|
||||
width: 100%;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.auth-switch {
|
||||
font: 400 13px 'Rajdhani', sans-serif;
|
||||
color: rgba(255, 255, 255, 0.35);
|
||||
text-align: center;
|
||||
margin-top: 24px;
|
||||
letter-spacing: 0.3px;
|
||||
|
||||
a {
|
||||
color: #95cff5;
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
transition: color 180ms;
|
||||
|
||||
&:hover { color: #c5e8ff; }
|
||||
}
|
||||
}
|
||||
|
||||
.auth-forgot-password {
|
||||
font: 400 13px 'Rajdhani', sans-serif;
|
||||
color: rgba(255, 255, 255, 0.35);
|
||||
text-align: center;
|
||||
letter-spacing: 0.3px;
|
||||
|
||||
a {
|
||||
color: #95cff5;
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
transition: color 180ms;
|
||||
|
||||
&:hover { color: #c5e8ff; }
|
||||
}
|
||||
}
|
||||
|
||||
.auth-divider {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 20px 0;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.auth-divider::before,
|
||||
.auth-divider::after {
|
||||
content: '';
|
||||
flex: 1;
|
||||
height: 1px;
|
||||
background: #ddd;
|
||||
}
|
||||
|
||||
.auth-divider span {
|
||||
margin: 0 10px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.auth-passkey-btn {
|
||||
width: 100%;
|
||||
padding: 12px 16px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.auth-passkey-btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 20px rgba(102, 126, 234, 0.3);
|
||||
}
|
||||
|
||||
.auth-passkey-btn:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.auth-input--code {
|
||||
font: 700 22px 'Courier New', monospace;
|
||||
letter-spacing: 6px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.auth-field-hint {
|
||||
font: 400 12px 'Rajdhani', sans-serif;
|
||||
color: rgba(149, 207, 245, 0.45);
|
||||
letter-spacing: 0.3px;
|
||||
margin: 6px 0 0;
|
||||
}
|
||||
|
||||
.auth-card--wide {
|
||||
max-width: 520px;
|
||||
}
|
||||
|
||||
.auth-back {
|
||||
margin-top: 20px;
|
||||
text-align: center;
|
||||
|
||||
a {
|
||||
font: 500 13px 'Rajdhani', sans-serif;
|
||||
color: rgba(149, 207, 245, 0.5);
|
||||
text-decoration: none;
|
||||
letter-spacing: 0.5px;
|
||||
transition: color 180ms;
|
||||
|
||||
&:hover { color: rgba(149, 207, 245, 0.9); }
|
||||
}
|
||||
}
|
||||
|
||||
.twofa-setup {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
margin-top: 20px;
|
||||
|
||||
&__qr {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
img {
|
||||
border-radius: 8px;
|
||||
border: 2px solid rgba(35, 111, 135, 0.3);
|
||||
background: #fff;
|
||||
padding: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
&__manual {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&__manual-label {
|
||||
font: 400 12px 'Rajdhani', sans-serif;
|
||||
color: rgba(149, 207, 245, 0.45);
|
||||
letter-spacing: 0.5px;
|
||||
margin: 0 0 8px;
|
||||
}
|
||||
|
||||
&__secret {
|
||||
display: inline-block;
|
||||
padding: 6px 14px;
|
||||
background: rgba(0, 0, 0, 0.35);
|
||||
border: 1px solid rgba(35, 111, 135, 0.3);
|
||||
border-radius: 4px;
|
||||
font: 700 14px 'Courier New', monospace;
|
||||
letter-spacing: 3px;
|
||||
color: #95cff5;
|
||||
word-break: break-all;
|
||||
user-select: all;
|
||||
}
|
||||
}
|
||||
200
assets/css/homepage/_battle-dialog.scss
Normal file
200
assets/css/homepage/_battle-dialog.scss
Normal file
@@ -0,0 +1,200 @@
|
||||
/*!*
|
||||
* This file is part of the SplendidBear Websites' projects.
|
||||
*
|
||||
* Copyright (c) 2026 @ www.splendidbear.org
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
// ── Avatar ───────────────────────────────────────────────────────────────────
|
||||
|
||||
.bd-avatar-wrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.bd-avatar-ring-wrap {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.bd-avatar-ring {
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
border-radius: 50%;
|
||||
background: var(--bd-avatar-gradient);
|
||||
border: 2px solid var(--bd-avatar-border);
|
||||
box-shadow: var(--bd-avatar-glow);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font: 800 24px 'Rajdhani', sans-serif;
|
||||
color: var(--bd-avatar-color);
|
||||
letter-spacing: 2px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.bd-avatar-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.bd-avatar-bonus {
|
||||
position: absolute;
|
||||
bottom: -6px;
|
||||
right: -6px;
|
||||
background: #ffd700;
|
||||
border-radius: 50%;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 0 12px rgba(255, 215, 0, 0.6), 0 0 0 2px rgba(7, 9, 13, 1);
|
||||
border: 2px solid rgba(0, 0, 0, 0.5);
|
||||
z-index: 10;
|
||||
|
||||
i {
|
||||
color: #000;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.bd-avatar-name {
|
||||
font: 700 15px 'Rajdhani', sans-serif;
|
||||
color: var(--bd-avatar-color);
|
||||
letter-spacing: 1px;
|
||||
max-width: 120px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.bd-avatar-side {
|
||||
font: 600 10px 'Rajdhani', sans-serif;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 2px;
|
||||
color: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
// ── StatRow ──────────────────────────────────────────────────────────────────
|
||||
|
||||
.bd-stat-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 9px 0;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
||||
|
||||
&__icon {
|
||||
width: 16px;
|
||||
color: rgba(149, 207, 245, 0.4);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
&__label {
|
||||
font: 500 13px 'Rajdhani', sans-serif;
|
||||
color: rgba(255, 255, 255, 0.45);
|
||||
flex: 1;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
&__value {
|
||||
font: 700 13px 'Rajdhani', sans-serif;
|
||||
color: var(--bd-stat-value-color, rgba(255, 255, 255, 0.75));
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
}
|
||||
|
||||
// ── BonusPoints ──────────────────────────────────────────────────────────────
|
||||
|
||||
.bd-bonus {
|
||||
padding: 16px 20px 0;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.08);
|
||||
margin: 16px 0;
|
||||
|
||||
&__grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
&__column {
|
||||
padding: 16px;
|
||||
border-radius: 6px;
|
||||
|
||||
&--red {
|
||||
border: 1px solid rgba(173, 10, 5, 0.2);
|
||||
background: rgba(173, 10, 5, 0.05);
|
||||
}
|
||||
|
||||
&--blue {
|
||||
border: 1px solid rgba(149, 207, 245, 0.2);
|
||||
background: rgba(149, 207, 245, 0.05);
|
||||
}
|
||||
}
|
||||
|
||||
&__heading {
|
||||
font: 700 12px 'Rajdhani', sans-serif;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 2px;
|
||||
color: #ffd700;
|
||||
display: block;
|
||||
margin-bottom: 12px;
|
||||
|
||||
i {
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
&__rows {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
// ── BattleDialog header actions & bonus score row ────────────────────────────
|
||||
|
||||
.bd-header-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.bd-bonus-score {
|
||||
margin-bottom: 8px;
|
||||
|
||||
&__red {
|
||||
font: 700 13px 'Rajdhani', sans-serif;
|
||||
color: #f67d52;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
|
||||
i {
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
|
||||
&__blue {
|
||||
font: 700 13px 'Rajdhani', sans-serif;
|
||||
color: #95cff5;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
|
||||
i {
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bd-result-badge {
|
||||
background: var(--bd-result-bg);
|
||||
border: 1px solid var(--bd-result-border);
|
||||
color: var(--bd-result-color);
|
||||
}
|
||||
52
assets/css/homepage/_content.scss
Normal file
52
assets/css/homepage/_content.scss
Normal file
@@ -0,0 +1,52 @@
|
||||
/*!*
|
||||
* This file is part of the SplendidBear Websites' projects.
|
||||
*
|
||||
* Copyright (c) 2026 @ www.splendidbear.org
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
main div.txt {
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
padding: 60px 40px 80px;
|
||||
}
|
||||
|
||||
main div.txt h2 {
|
||||
font: bold 28px 'Rajdhani', sans-serif;
|
||||
color: #ffffff;
|
||||
margin-bottom: 30px;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
main div.txt h3 {
|
||||
font: bold 17px 'Rajdhani', sans-serif;
|
||||
color: rgba(149, 207, 245, 0.9);
|
||||
margin: 28px 0 10px;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
main div.txt p,
|
||||
main div.txt li {
|
||||
font: 400 15px 'Rajdhani', sans-serif;
|
||||
color: rgba(255, 255, 255, 0.72);
|
||||
line-height: 1.75;
|
||||
}
|
||||
|
||||
main div.txt a {
|
||||
color: #95cff5;
|
||||
text-decoration: none;
|
||||
transition: color 180ms;
|
||||
|
||||
&:hover { color: #c5e8ff; }
|
||||
}
|
||||
|
||||
main div.txt img {
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
main div.txt .img-container {
|
||||
text-align: center;
|
||||
}
|
||||
84
assets/css/homepage/_cta.scss
Normal file
84
assets/css/homepage/_cta.scss
Normal file
@@ -0,0 +1,84 @@
|
||||
/*!*
|
||||
* This file is part of the SplendidBear Websites' projects.
|
||||
*
|
||||
* Copyright (c) 2026 @ www.splendidbear.org
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
.hero-cta {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
font: 800 28px 'Rajdhani', sans-serif;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 6px;
|
||||
text-decoration: none;
|
||||
color: #ffffff;
|
||||
padding: 22px 100px 20px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid rgba(246, 125, 82, 0.25);
|
||||
|
||||
background: linear-gradient(to bottom, #b30c06 0%, #d63d15 50%, #f67d52 100%);
|
||||
|
||||
box-shadow:
|
||||
0 0 0 1px rgba(173, 10, 5, 0.2),
|
||||
0 0 30px rgba(173, 10, 5, 0.35),
|
||||
0 6px 24px rgba(0, 0, 0, 0.5),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.12);
|
||||
|
||||
transition: transform 220ms ease, box-shadow 220ms ease, letter-spacing 220ms ease;
|
||||
animation: rise 0.8s 0.42s cubic-bezier(0.22, 1, 0.36, 1) both;
|
||||
}
|
||||
|
||||
// Outer glow layer (blurred duplicate, always visible)
|
||||
.hero-cta::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: -4px;
|
||||
border-radius: 7px;
|
||||
background: linear-gradient(to bottom, #ad0a05, #f67d52);
|
||||
filter: blur(18px);
|
||||
opacity: 0.3;
|
||||
z-index: -1;
|
||||
transition: opacity 220ms ease;
|
||||
}
|
||||
|
||||
.hero-cta:hover {
|
||||
transform: translateY(-4px);
|
||||
letter-spacing: 8px;
|
||||
box-shadow:
|
||||
0 0 0 1px rgba(246, 125, 82, 0.3),
|
||||
0 0 50px rgba(173, 10, 5, 0.65),
|
||||
0 10px 32px rgba(0, 0, 0, 0.45),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
|
||||
.hero-cta:hover::before {
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
.hero-cta:active {
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
// Version / copyright line
|
||||
.hero-meta {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
font: 400 12px 'Rajdhani', sans-serif;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
letter-spacing: 0.5px;
|
||||
margin-top: 58px;
|
||||
animation: rise 0.8s 0.55s cubic-bezier(0.22, 1, 0.36, 1) both;
|
||||
}
|
||||
|
||||
.hero-meta a {
|
||||
color: rgba(149, 207, 245, 0.65);
|
||||
text-decoration: none;
|
||||
transition: color 180ms;
|
||||
}
|
||||
|
||||
.hero-meta a:hover {
|
||||
color: #95cff5;
|
||||
}
|
||||
72
assets/css/homepage/_donate.scss
Normal file
72
assets/css/homepage/_donate.scss
Normal file
@@ -0,0 +1,72 @@
|
||||
/*!*
|
||||
* This file is part of the SplendidBear Websites' projects.
|
||||
*
|
||||
* Copyright (c) 2026 @ www.splendidbear.org
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
.hero-donate-text {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
font: 400 12px 'Rajdhani', sans-serif;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
letter-spacing: 0.5px;
|
||||
margin-top: 42px;
|
||||
margin-bottom: 8px;
|
||||
animation: rise 0.8s 0.5s cubic-bezier(0.22, 1, 0.36, 1) both;
|
||||
}
|
||||
|
||||
.hero-donate {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
font: 500 12px 'Rajdhani', sans-serif;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
text-decoration: none;
|
||||
color: rgba(255, 184, 82, 0.75);
|
||||
padding: 6px 14px;
|
||||
border-radius: 2px;
|
||||
border: 1px solid rgba(255, 184, 82, 0.25);
|
||||
|
||||
background: rgba(255, 140, 30, 0.05);
|
||||
|
||||
box-shadow:
|
||||
0 0 0 1px rgba(255, 140, 30, 0.1),
|
||||
0 0 8px rgba(255, 140, 30, 0.08);
|
||||
|
||||
transition: transform 200ms ease, box-shadow 200ms ease, color 200ms ease, background 200ms ease;
|
||||
animation: rise 0.8s 0.58s cubic-bezier(0.22, 1, 0.36, 1) both;
|
||||
}
|
||||
|
||||
.hero-donate::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: -2px;
|
||||
border-radius: 3px;
|
||||
background: rgba(255, 140, 30, 0.15);
|
||||
filter: blur(8px);
|
||||
opacity: 0.1;
|
||||
z-index: -1;
|
||||
transition: opacity 200ms ease;
|
||||
}
|
||||
|
||||
.hero-donate:hover {
|
||||
transform: translateY(-1px);
|
||||
color: rgba(255, 200, 100, 0.9);
|
||||
background: rgba(255, 140, 30, 0.1);
|
||||
box-shadow:
|
||||
0 0 0 1px rgba(255, 140, 30, 0.2),
|
||||
0 0 12px rgba(255, 140, 30, 0.15);
|
||||
}
|
||||
|
||||
.hero-donate:hover::before {
|
||||
opacity: 0.2;
|
||||
}
|
||||
|
||||
.hero-donate:active {
|
||||
transform: translateY(0px);
|
||||
}
|
||||
|
||||
|
||||
363
assets/css/homepage/_features.scss
Normal file
363
assets/css/homepage/_features.scss
Normal file
@@ -0,0 +1,363 @@
|
||||
/*!*
|
||||
* This file is part of the SplendidBear Websites' projects.
|
||||
*
|
||||
* Copyright (c) 2026 @ www.splendidbear.org
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
.feature-block {
|
||||
width: 100%;
|
||||
padding: 80px 40px;
|
||||
position: relative;
|
||||
|
||||
&:first-of-type {
|
||||
border-top: 1px solid rgba(35, 111, 135, 0.12);
|
||||
}
|
||||
|
||||
& + & {
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
}
|
||||
|
||||
.feature-block__inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 80px;
|
||||
max-width: 1100px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.feature-block--reverse .feature-block__inner {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
// Visual side
|
||||
.feature-block__visual {
|
||||
flex: 0 0 340px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
// Stats icons cluster
|
||||
.feature-block__visual--stats {
|
||||
height: 260px;
|
||||
gap: 0;
|
||||
|
||||
i {
|
||||
position: absolute;
|
||||
color: rgba(35, 111, 135, 0.5);
|
||||
transition: color 300ms ease;
|
||||
}
|
||||
|
||||
// Bar chart — large, centre
|
||||
i.fa-chart-bar {
|
||||
font-size: 160px;
|
||||
color: rgba(35, 111, 135, 0.5);
|
||||
filter: drop-shadow(0 0 40px rgba(35, 111, 135, 0.5));
|
||||
}
|
||||
|
||||
// Trophy — top right
|
||||
i.fa-trophy {
|
||||
font-size: 80px;
|
||||
top: 0px;
|
||||
right: 20px;
|
||||
color: rgba(246, 125, 82, 0.7);
|
||||
filter: drop-shadow(0 0 25px rgba(246, 125, 82, 0.4));
|
||||
}
|
||||
|
||||
// Clock history — bottom left
|
||||
i.fa-clock-rotate-left {
|
||||
font-size: 68px;
|
||||
bottom: 0px;
|
||||
left: 20px;
|
||||
color: rgba(149, 207, 245, 0.65);
|
||||
filter: drop-shadow(0 0 20px rgba(149, 207, 245, 0.35));
|
||||
}
|
||||
|
||||
&:hover i.fa-chart-bar { color: rgba(35, 111, 135, 0.8); filter: drop-shadow(0 0 50px rgba(35, 111, 135, 0.7)); }
|
||||
&:hover i.fa-trophy { color: rgba(246, 125, 82, 0.9); filter: drop-shadow(0 0 35px rgba(246, 125, 82, 0.6)); }
|
||||
&:hover i.fa-clock-rotate-left { color: rgba(149, 207, 245, 0.9); filter: drop-shadow(0 0 30px rgba(149, 207, 245, 0.5)); }
|
||||
}
|
||||
|
||||
// MSN visual
|
||||
.feature-block__visual--msn {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.msn-logo {
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
object-fit: contain;
|
||||
filter: drop-shadow(0 0 18px rgba(149, 207, 245, 0.3)) brightness(1.1);
|
||||
flex-shrink: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.msn-screenshot {
|
||||
width: 340px;
|
||||
max-width: 100%;
|
||||
border-radius: 8px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
box-shadow:
|
||||
0 8px 40px rgba(0, 0, 0, 0.6),
|
||||
0 0 0 1px rgba(35, 111, 135, 0.12);
|
||||
filter: saturate(0.85) brightness(0.9);
|
||||
transition: filter 300ms ease;
|
||||
|
||||
&:hover {
|
||||
filter: saturate(1) brightness(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Privacy visual
|
||||
.feature-block__visual--privacy {
|
||||
height: 260px;
|
||||
gap: 0;
|
||||
|
||||
i {
|
||||
position: absolute;
|
||||
color: rgba(35, 111, 135, 0.5);
|
||||
transition: color 300ms ease;
|
||||
}
|
||||
|
||||
// Shield — centre, large
|
||||
i.fa-shield {
|
||||
font-size: 140px;
|
||||
color: rgba(34, 197, 94, 0.3);
|
||||
filter: drop-shadow(0 0 30px rgba(34, 197, 94, 0.25));
|
||||
}
|
||||
|
||||
// Lock — top right
|
||||
i.fa-lock {
|
||||
font-size: 56px;
|
||||
top: 20px;
|
||||
right: 35px;
|
||||
color: rgba(168, 85, 247, 0.5);
|
||||
filter: drop-shadow(0 0 16px rgba(168, 85, 247, 0.2));
|
||||
}
|
||||
|
||||
// Eye slash — bottom left
|
||||
i.fa-eye-slash {
|
||||
font-size: 48px;
|
||||
bottom: 28px;
|
||||
left: 40px;
|
||||
color: rgba(59, 130, 246, 0.5);
|
||||
filter: drop-shadow(0 0 12px rgba(59, 130, 246, 0.15));
|
||||
}
|
||||
|
||||
&:hover i.fa-shield { color: rgba(34, 197, 94, 0.6); }
|
||||
&:hover i.fa-lock { color: rgba(168, 85, 247, 0.75); }
|
||||
&:hover i.fa-eye-slash { color: rgba(59, 130, 246, 0.7); }
|
||||
}
|
||||
|
||||
// Practice visual
|
||||
.feature-block__visual--practice {
|
||||
height: 260px;
|
||||
gap: 0;
|
||||
|
||||
i {
|
||||
position: absolute;
|
||||
color: rgba(35, 111, 135, 0.5);
|
||||
transition: color 300ms ease;
|
||||
}
|
||||
|
||||
// Laptop — centre, large
|
||||
i.fa-laptop {
|
||||
font-size: 140px;
|
||||
color: rgba(251, 146, 60, 0.3);
|
||||
filter: drop-shadow(0 0 30px rgba(251, 146, 60, 0.25));
|
||||
}
|
||||
|
||||
// Linux — top left
|
||||
i.fa-linux {
|
||||
font-size: 48px;
|
||||
top: 20px;
|
||||
left: 35px;
|
||||
color: rgba(245, 158, 11, 0.5);
|
||||
filter: drop-shadow(0 0 16px rgba(245, 158, 11, 0.2));
|
||||
}
|
||||
|
||||
// Apple — top right
|
||||
i.fa-apple {
|
||||
font-size: 56px;
|
||||
top: 20px;
|
||||
right: 35px;
|
||||
color: rgba(156, 163, 175, 0.5);
|
||||
filter: drop-shadow(0 0 16px rgba(156, 163, 175, 0.2));
|
||||
}
|
||||
|
||||
// Windows — bottom left
|
||||
i.fa-windows {
|
||||
font-size: 48px;
|
||||
bottom: 28px;
|
||||
left: 40px;
|
||||
color: rgba(59, 130, 246, 0.5);
|
||||
filter: drop-shadow(0 0 12px rgba(59, 130, 246, 0.15));
|
||||
}
|
||||
|
||||
&:hover i.fa-laptop { color: rgba(251, 146, 60, 0.6); }
|
||||
&:hover i.fa-linux { color: rgba(245, 158, 11, 0.75); }
|
||||
&:hover i.fa-apple { color: rgba(156, 163, 175, 0.75); }
|
||||
&:hover i.fa-windows { color: rgba(59, 130, 246, 0.7); }
|
||||
}
|
||||
|
||||
// Text side
|
||||
.feature-block__text {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.feature-block__label {
|
||||
font: 700 11px 'Rajdhani', sans-serif;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 4px;
|
||||
color: rgba(149, 207, 245, 0.55);
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.feature-block__title {
|
||||
font: 800 40px 'Rajdhani', sans-serif;
|
||||
color: #ffffff;
|
||||
line-height: 1.1;
|
||||
letter-spacing: 0.5px;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.feature-block__body {
|
||||
font: 400 16px 'Rajdhani', sans-serif;
|
||||
color: rgba(255, 255, 255, 0.62);
|
||||
line-height: 1.8;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.feature-block__cta {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-top: 28px;
|
||||
font: 700 14px 'Rajdhani', sans-serif;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 2px;
|
||||
text-decoration: none;
|
||||
color: rgba(149, 207, 245, 0.85);
|
||||
border: 1px solid rgba(35, 111, 135, 0.4);
|
||||
background: rgba(35, 111, 135, 0.1);
|
||||
padding: 11px 24px;
|
||||
border-radius: 4px;
|
||||
transition: all 200ms ease;
|
||||
|
||||
&:hover {
|
||||
background: rgba(35, 111, 135, 0.25);
|
||||
border-color: rgba(149, 207, 245, 0.55);
|
||||
color: #fff;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 16px rgba(35, 111, 135, 0.25);
|
||||
}
|
||||
}
|
||||
|
||||
.practice-links {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.practice-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
font: 700 13px 'Rajdhani', sans-serif;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
text-decoration: none;
|
||||
color: rgba(149, 207, 245, 0.85);
|
||||
border: 1px solid rgba(59, 130, 246, 0.3);
|
||||
background: rgba(59, 130, 246, 0.08);
|
||||
padding: 10px 18px;
|
||||
border-radius: 4px;
|
||||
transition: all 200ms ease;
|
||||
width: fit-content;
|
||||
|
||||
&:hover {
|
||||
background: rgba(59, 130, 246, 0.2);
|
||||
border-color: rgba(59, 130, 246, 0.6);
|
||||
color: #fff;
|
||||
transform: translateX(4px);
|
||||
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
.practice-link-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 8px;
|
||||
object-fit: contain;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
padding: 4px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
flex-shrink: 0;
|
||||
transition: all 200ms ease;
|
||||
}
|
||||
|
||||
.practice-link:hover .practice-link-icon {
|
||||
background: rgba(59, 130, 246, 0.15);
|
||||
border-color: rgba(59, 130, 246, 0.4);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
@media screen and (max-width: 900px) {
|
||||
.feature-block__inner,
|
||||
.feature-block--reverse .feature-block__inner {
|
||||
flex-direction: column;
|
||||
gap: 48px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.feature-block__visual {
|
||||
flex: none;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.feature-block__visual--stats {
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
.feature-block__visual--msn {
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.msn-screenshot {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.feature-block__label,
|
||||
.feature-block__cta {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.feature-block__title { font-size: 32px; }
|
||||
|
||||
.feature-block {
|
||||
padding: 60px 24px;
|
||||
}
|
||||
|
||||
.practice-links {
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.practice-link {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
121
assets/css/homepage/_footer.scss
Normal file
121
assets/css/homepage/_footer.scss
Normal file
@@ -0,0 +1,121 @@
|
||||
/*!*
|
||||
* This file is part of the SplendidBear Websites' projects.
|
||||
*
|
||||
* Copyright (c) 2026 @ www.splendidbear.org
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
footer {
|
||||
background: #040608;
|
||||
border-top: 1px solid rgba(35, 111, 135, 0.12);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.footer-inner {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
max-width: 1100px;
|
||||
margin: 0 auto;
|
||||
padding: 60px 60px 52px;
|
||||
gap: 40px;
|
||||
}
|
||||
|
||||
.footer-brand {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.footer-logo {
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
opacity: 0.55;
|
||||
filter:
|
||||
drop-shadow(0 0 12px rgba(35, 111, 135, 0.4))
|
||||
brightness(1.1);
|
||||
transition: opacity 250ms ease, filter 250ms ease;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.9;
|
||||
filter:
|
||||
drop-shadow(0 0 20px rgba(35, 111, 135, 0.65))
|
||||
brightness(1.2);
|
||||
}
|
||||
}
|
||||
|
||||
.footer-name {
|
||||
font: 700 22px 'Rajdhani', sans-serif;
|
||||
color: rgba(255, 255, 255, 0.75);
|
||||
letter-spacing: 2px;
|
||||
text-transform: uppercase;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.footer-tagline {
|
||||
font: 400 13px 'Rajdhani', sans-serif;
|
||||
color: rgba(149, 207, 245, 0.7);
|
||||
letter-spacing: 0.5px;
|
||||
max-width: 240px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.footer-nav-label {
|
||||
font: 700 11px 'Rajdhani', sans-serif;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 4px;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
margin-bottom: 18px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.footer-nav ul {
|
||||
list-style: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 6px;
|
||||
min-width: 180px;
|
||||
}
|
||||
|
||||
.footer-nav ul li a {
|
||||
display: block;
|
||||
font: 500 15px 'Rajdhani', sans-serif;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
text-decoration: none;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1.5px;
|
||||
white-space: nowrap;
|
||||
padding: 6px 0;
|
||||
transition: color 180ms ease, letter-spacing 180ms ease;
|
||||
|
||||
&:hover {
|
||||
color: #95cff5;
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.footer-copy {
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.05);
|
||||
padding: 16px 60px;
|
||||
max-width: 1100px;
|
||||
margin: 0 auto;
|
||||
|
||||
p {
|
||||
font: 400 11px 'Rajdhani', sans-serif;
|
||||
color: rgba(255, 255, 255, 0.45);
|
||||
letter-spacing: 0.5px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
a {
|
||||
color: rgba(149, 207, 245, 0.6);
|
||||
text-decoration: none;
|
||||
transition: color 180ms;
|
||||
|
||||
&:hover { color: #95cff5; }
|
||||
}
|
||||
}
|
||||
48
assets/css/homepage/_header.scss
Normal file
48
assets/css/homepage/_header.scss
Normal file
@@ -0,0 +1,48 @@
|
||||
/*!*
|
||||
* This file is part of the SplendidBear Websites' projects.
|
||||
*
|
||||
* Copyright (c) 2026 @ www.splendidbear.org
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
header {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
|
||||
// Minesweeper grid texture
|
||||
background-color: #07090d;
|
||||
background-image:
|
||||
linear-gradient(rgba(35, 111, 135, 0.1) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(35, 111, 135, 0.1) 1px, transparent 1px);
|
||||
background-size: 46px 46px;
|
||||
}
|
||||
|
||||
// Deep radial vignette – grid fades toward the centre
|
||||
header::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: radial-gradient(
|
||||
ellipse 85% 75% at 50% 50%,
|
||||
#07090d 10%,
|
||||
transparent 75%
|
||||
);
|
||||
z-index: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
// Smoke at the bottom so header bleeds into body
|
||||
header::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 160px;
|
||||
background: linear-gradient(to bottom, transparent, #07090d);
|
||||
z-index: 1;
|
||||
pointer-events: none;
|
||||
}
|
||||
58
assets/css/homepage/_hero-compact.scss
Normal file
58
assets/css/homepage/_hero-compact.scss
Normal file
@@ -0,0 +1,58 @@
|
||||
/*!*
|
||||
* This file is part of the SplendidBear Websites' projects.
|
||||
*
|
||||
* Copyright (c) 2026 @ www.splendidbear.org
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
.hero--compact {
|
||||
min-height: unset;
|
||||
padding: 36px 60px 48px;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: left;
|
||||
gap: 52px;
|
||||
|
||||
&::before, &::after { display: none; }
|
||||
|
||||
.hero-logo {
|
||||
flex-shrink: 0;
|
||||
margin-bottom: 0;
|
||||
|
||||
img { width: 180px; }
|
||||
}
|
||||
|
||||
.hero-body {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.hero-sub {
|
||||
font-size: 14px;
|
||||
letter-spacing: 2px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 26px;
|
||||
margin-bottom: 24px;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.hero-cta {
|
||||
padding: 12px 52px 10px;
|
||||
font-size: 18px;
|
||||
letter-spacing: 4px;
|
||||
}
|
||||
|
||||
.hero-meta {
|
||||
margin-top: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
// Also shrink the bottom fade on sub-pages
|
||||
header:has(.hero--compact)::after {
|
||||
height: 60px;
|
||||
}
|
||||
99
assets/css/homepage/_hero.scss
Normal file
99
assets/css/homepage/_hero.scss
Normal file
@@ -0,0 +1,99 @@
|
||||
/*!*
|
||||
* This file is part of the SplendidBear Websites' projects.
|
||||
*
|
||||
* Copyright (c) 2026 @ www.splendidbear.org
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
.hero {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
min-height: 100vh;
|
||||
padding: 80px 40px 160px;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
// Decorative glow blobs in opposite corners
|
||||
.hero::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -60px;
|
||||
left: -60px;
|
||||
width: 420px;
|
||||
height: 420px;
|
||||
border-radius: 50%;
|
||||
background: radial-gradient(circle, rgba(173, 10, 5, 0.09) 0%, transparent 65%);
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.hero::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 100px;
|
||||
right: -60px;
|
||||
width: 380px;
|
||||
height: 380px;
|
||||
border-radius: 50%;
|
||||
background: radial-gradient(circle, rgba(35, 111, 135, 0.1) 0%, transparent 65%);
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
// Logo
|
||||
.hero-logo {
|
||||
display: block;
|
||||
margin-bottom: 72px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
animation: appear 0.9s cubic-bezier(0.22, 1, 0.36, 1) both;
|
||||
}
|
||||
|
||||
.hero-logo img {
|
||||
width: 400px;
|
||||
max-width: 82vw;
|
||||
filter:
|
||||
drop-shadow(0 0 40px rgba(35, 111, 135, 0.35))
|
||||
drop-shadow(0 6px 20px rgba(0, 0, 0, 0.8));
|
||||
}
|
||||
|
||||
// Body text block
|
||||
.hero-body {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.hero-sub {
|
||||
font: 300 17px 'Rajdhani', sans-serif;
|
||||
color: rgba(149, 207, 245, 0.9);
|
||||
letter-spacing: 3px;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 18px;
|
||||
animation: rise 0.8s 0.15s cubic-bezier(0.22, 1, 0.36, 1) both;
|
||||
}
|
||||
|
||||
.hero-sub strong {
|
||||
font-weight: 600;
|
||||
color: #95cff5;
|
||||
}
|
||||
|
||||
.hero h1 {
|
||||
font: 800 58px 'Rajdhani', sans-serif;
|
||||
color: #ffffff;
|
||||
line-height: 1.1;
|
||||
letter-spacing: 1px;
|
||||
margin-bottom: 56px;
|
||||
text-shadow: 0 4px 30px rgba(0, 0, 0, 0.7);
|
||||
animation: rise 0.8s 0.28s cubic-bezier(0.22, 1, 0.36, 1) both;
|
||||
}
|
||||
1230
assets/css/homepage/_profile.scss
Normal file
1230
assets/css/homepage/_profile.scss
Normal file
File diff suppressed because it is too large
Load Diff
32
assets/css/homepage/_reset.scss
Normal file
32
assets/css/homepage/_reset.scss
Normal file
@@ -0,0 +1,32 @@
|
||||
/*!*
|
||||
* This file is part of the SplendidBear Websites' projects.
|
||||
*
|
||||
* Copyright (c) 2026 @ www.splendidbear.org
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
* {
|
||||
outline: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
// Grid lives on html so it tiles across all pages including content pages
|
||||
background-color: #07090d;
|
||||
background-image:
|
||||
linear-gradient(rgba(35, 111, 135, 0.1) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(35, 111, 135, 0.1) 1px, transparent 1px);
|
||||
background-size: 46px 46px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
background: transparent;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
113
assets/css/homepage/_responsive.scss
Normal file
113
assets/css/homepage/_responsive.scss
Normal file
@@ -0,0 +1,113 @@
|
||||
/*!*
|
||||
* This file is part of the SplendidBear Websites' projects.
|
||||
*
|
||||
* Copyright (c) 2026 @ www.splendidbear.org
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
@media screen and (max-width: 900px) {
|
||||
.hero h1 {
|
||||
font-size: 44px;
|
||||
}
|
||||
|
||||
.hero-cta {
|
||||
padding: 20px 72px 18px;
|
||||
font-size: 24px;
|
||||
letter-spacing: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
.hero--compact {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
padding: 36px 24px 44px;
|
||||
gap: 28px;
|
||||
|
||||
.hero-body { align-items: center; }
|
||||
}
|
||||
|
||||
.profile-stats {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.profile-charts {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.profile-header {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
padding: 28px 24px;
|
||||
}
|
||||
|
||||
.profile-email,
|
||||
.profile-role {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.profile-game {
|
||||
grid-template-columns: 26px 64px 18px 1fr 14px;
|
||||
|
||||
.profile-game__date { display: none; }
|
||||
}
|
||||
|
||||
.footer-inner {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
padding: 48px 30px 36px;
|
||||
}
|
||||
|
||||
.footer-brand {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.footer-tagline {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.footer-nav-label {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.footer-nav ul {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.footer-copy {
|
||||
padding: 16px 30px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 550px) {
|
||||
.hero {
|
||||
padding: 60px 24px 140px;
|
||||
}
|
||||
|
||||
.hero-logo img {
|
||||
width: 260px;
|
||||
}
|
||||
|
||||
.hero-logo {
|
||||
margin-bottom: 52px;
|
||||
}
|
||||
|
||||
.hero h1 {
|
||||
font-size: 32px;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.hero-sub {
|
||||
font-size: 14px;
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
|
||||
.hero-cta {
|
||||
padding: 18px 48px 16px;
|
||||
font-size: 20px;
|
||||
letter-spacing: 4px;
|
||||
}
|
||||
}
|
||||
69
assets/css/homepage/_tech.scss
Normal file
69
assets/css/homepage/_tech.scss
Normal file
@@ -0,0 +1,69 @@
|
||||
/*!*
|
||||
* This file is part of the SplendidBear Websites' projects.
|
||||
*
|
||||
* Copyright (c) 2026 @ www.splendidbear.org
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
main {
|
||||
background: #07090d;
|
||||
}
|
||||
|
||||
.tech-section {
|
||||
padding: 48px 20px 72px;
|
||||
text-align: center;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.04);
|
||||
}
|
||||
|
||||
.tech-label {
|
||||
font: 600 11px 'Rajdhani', sans-serif;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 6px;
|
||||
color: rgba(255, 255, 255, 0.14);
|
||||
margin-bottom: 28px;
|
||||
}
|
||||
|
||||
.tech-link {
|
||||
display: inline-block;
|
||||
line-height: 0;
|
||||
}
|
||||
|
||||
.tech-logos img {
|
||||
display: inline-block;
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
object-fit: contain;
|
||||
margin: 8px 24px;
|
||||
// Force all logos to white, then tint with the game's blue on hover
|
||||
filter: brightness(0) invert(1) opacity(0.35);
|
||||
transition: filter 220ms ease, transform 220ms ease;
|
||||
}
|
||||
|
||||
.tech-logos img:hover {
|
||||
filter:
|
||||
brightness(0) invert(1)
|
||||
sepia(1) saturate(3) hue-rotate(175deg) brightness(1.1)
|
||||
opacity(0.9);
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
|
||||
.tech-oss {
|
||||
margin-top: 36px;
|
||||
font: 400 15px 'Rajdhani', sans-serif;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
letter-spacing: 0.5px;
|
||||
max-width: 680px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
padding: 0 24px;
|
||||
line-height: 1.7;
|
||||
text-align: center;
|
||||
|
||||
i {
|
||||
color: rgba(220, 60, 50, 0.85);
|
||||
margin-right: 6px;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
33
assets/css/mineseeker/_back-button.scss
Normal file
33
assets/css/mineseeker/_back-button.scss
Normal file
@@ -0,0 +1,33 @@
|
||||
/*!*
|
||||
* This file is part of the SplendidBear Websites' projects.
|
||||
*
|
||||
* Copyright (c) 2026 @ www.splendidbear.org
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
.back-from-game {
|
||||
display: inline-block;
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
left: 20px;
|
||||
|
||||
-ms-transform: scale(1);
|
||||
-webkit-transform: scale(1);
|
||||
transform: scale(1);
|
||||
-webkit-transition: all 250ms cubic-bezier(.17, .67, .83, .67);
|
||||
transition: all 250ms cubic-bezier(.17, .67, .83, .67);
|
||||
}
|
||||
|
||||
.back-from-game img {
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
.back-from-game:hover {
|
||||
-ms-transform: scale(1.2);
|
||||
-webkit-transform: scale(1.2);
|
||||
transform: scale(1.2);
|
||||
-webkit-transition: all 250ms cubic-bezier(.17, .67, .83, .67);
|
||||
transition: all 250ms cubic-bezier(.17, .67, .83, .67);
|
||||
}
|
||||
65
assets/css/mineseeker/_base.scss
Normal file
65
assets/css/mineseeker/_base.scss
Normal file
@@ -0,0 +1,65 @@
|
||||
/*!*
|
||||
* This file is part of the SplendidBear Websites' projects.
|
||||
*
|
||||
* Copyright (c) 2026 @ www.splendidbear.org
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
html {
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
height: 100%;
|
||||
min-height: 100%;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
main {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.mine-container {
|
||||
background-size: cover;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.clear {
|
||||
clear: both
|
||||
}
|
||||
|
||||
#mine-wrapper,
|
||||
#mine-wrapper * {
|
||||
-webkit-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
#mine-wrapper {
|
||||
display: table;
|
||||
width: 842px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
#mine-wrapper .game-wrapper {
|
||||
background: #000;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
padding: 10px;
|
||||
|
||||
-webkit-border-radius: 10px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
234
assets/css/mineseeker/_bomb.scss
Normal file
234
assets/css/mineseeker/_bomb.scss
Normal file
@@ -0,0 +1,234 @@
|
||||
/*!*
|
||||
* This file is part of the SplendidBear Websites' projects.
|
||||
*
|
||||
* Copyright (c) 2026 @ www.splendidbear.org
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
#mine-wrapper .game-wrapper .users .user-container .user-control {
|
||||
background: -moz-linear-gradient(-45deg, rgba(255, 255, 255, 0.5) 0%, rgba(125, 185, 232, 0) 100%);
|
||||
background: -webkit-linear-gradient(-45deg, rgba(255, 255, 255, 0.5) 0%, rgba(125, 185, 232, 0) 100%);
|
||||
background: linear-gradient(135deg, rgba(255, 255, 255, 0.5) 0%, rgba(125, 185, 232, 0) 100%);
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffff', endColorstr='#007db9e8', GradientType=1);
|
||||
position: relative;
|
||||
padding: 5px;
|
||||
|
||||
-webkit-border-radius: 5px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
#mine-wrapper .game-wrapper .users .user-container .user-control > img {
|
||||
position: absolute;
|
||||
width: 55px;
|
||||
left: -5px;
|
||||
bottom: 10px;
|
||||
|
||||
-ms-transform: rotate(-15deg);
|
||||
-webkit-transform: rotate(-15deg);
|
||||
transform: rotate(-15deg);
|
||||
}
|
||||
|
||||
#mine-wrapper .game-wrapper .users .user-container .user-control .user-control-mines {
|
||||
display: inline-block;
|
||||
background: #FFFFFF;
|
||||
font-size: 25px;
|
||||
text-align: center;
|
||||
width: 45px;
|
||||
height: 35px;
|
||||
margin-left: 25px;
|
||||
margin-top: 5px;
|
||||
|
||||
-webkit-border-radius: 5px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
#mine-wrapper .game-wrapper .users .user-container.user-blue .user-control .user-control-mines {
|
||||
color: #1a3955;
|
||||
}
|
||||
|
||||
#mine-wrapper .game-wrapper .users .user-container.user-red .user-control .user-control-mines {
|
||||
color: #b10000;
|
||||
}
|
||||
|
||||
#mine-wrapper .game-wrapper .users .user-container .user-control .bomb-container {
|
||||
display: inline-block;
|
||||
float: right;
|
||||
width: 65px;
|
||||
height: 45px;
|
||||
border: 1px solid #000;
|
||||
|
||||
-webkit-border-radius: 7px;
|
||||
border-radius: 7px;
|
||||
-webkit-transform: translateZ(0);
|
||||
transform: translateZ(0);
|
||||
-webkit-backface-visibility: hidden;
|
||||
backface-visibility: hidden;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
box-shadow: 0 0 1px rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
#mine-wrapper .game-wrapper .users .user-container .user-control .bomb-container.buzz:hover {
|
||||
-webkit-animation-name: hvr-buzz-out;
|
||||
animation-name: hvr-buzz-out;
|
||||
-webkit-animation-duration: 0.75s;
|
||||
animation-duration: 0.75s;
|
||||
-webkit-animation-timing-function: linear;
|
||||
animation-timing-function: linear;
|
||||
-webkit-animation-iteration-count: 1;
|
||||
animation-iteration-count: 1;
|
||||
}
|
||||
|
||||
@-webkit-keyframes hvr-buzz-out {
|
||||
10% { -webkit-transform: translateX(3px) rotate(2deg); transform: translateX(3px) rotate(2deg); }
|
||||
20% { -webkit-transform: translateX(-3px) rotate(-2deg); transform: translateX(-3px) rotate(-2deg); }
|
||||
30% { -webkit-transform: translateX(3px) rotate(2deg); transform: translateX(3px) rotate(2deg); }
|
||||
40% { -webkit-transform: translateX(-3px) rotate(-2deg); transform: translateX(-3px) rotate(-2deg); }
|
||||
50% { -webkit-transform: translateX(2px) rotate(1deg); transform: translateX(2px) rotate(1deg); }
|
||||
60% { -webkit-transform: translateX(-2px) rotate(-1deg); transform: translateX(-2px) rotate(-1deg); }
|
||||
70% { -webkit-transform: translateX(2px) rotate(1deg); transform: translateX(2px) rotate(1deg); }
|
||||
80% { -webkit-transform: translateX(-2px) rotate(-1deg); transform: translateX(-2px) rotate(-1deg); }
|
||||
90% { -webkit-transform: translateX(1px) rotate(0); transform: translateX(1px) rotate(0); }
|
||||
100% { -webkit-transform: translateX(-1px) rotate(0); transform: translateX(-1px) rotate(0); }
|
||||
}
|
||||
|
||||
@keyframes hvr-buzz-out {
|
||||
10% { -webkit-transform: translateX(3px) rotate(2deg); transform: translateX(3px) rotate(2deg); }
|
||||
20% { -webkit-transform: translateX(-3px) rotate(-2deg); transform: translateX(-3px) rotate(-2deg); }
|
||||
30% { -webkit-transform: translateX(3px) rotate(2deg); transform: translateX(3px) rotate(2deg); }
|
||||
40% { -webkit-transform: translateX(-3px) rotate(-2deg); transform: translateX(-3px) rotate(-2deg); }
|
||||
50% { -webkit-transform: translateX(2px) rotate(1deg); transform: translateX(2px) rotate(1deg); }
|
||||
60% { -webkit-transform: translateX(-2px) rotate(-1deg); transform: translateX(-2px) rotate(-1deg); }
|
||||
70% { -webkit-transform: translateX(2px) rotate(1deg); transform: translateX(2px) rotate(1deg); }
|
||||
80% { -webkit-transform: translateX(-2px) rotate(-1deg); transform: translateX(-2px) rotate(-1deg); }
|
||||
90% { -webkit-transform: translateX(1px) rotate(0); transform: translateX(1px) rotate(0); }
|
||||
100% { -webkit-transform: translateX(-1px) rotate(0); transform: translateX(-1px) rotate(0); }
|
||||
}
|
||||
|
||||
#mine-wrapper .game-wrapper .users .user-container .user-control .bomb-container .bomb {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#mine-wrapper .game-wrapper .users .user-container .user-control .bomb-container .bomb img {
|
||||
display: inline-block;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#mine-wrapper .game-wrapper .users .user-container .user-control .bomb-container:hover .bomb img {
|
||||
-webkit-animation-name: hvr-buzz;
|
||||
animation-name: hvr-buzz;
|
||||
-webkit-animation-duration: 0.15s;
|
||||
animation-duration: 0.15s;
|
||||
-webkit-animation-timing-function: linear;
|
||||
animation-timing-function: linear;
|
||||
-webkit-animation-iteration-count: infinite;
|
||||
animation-iteration-count: infinite;
|
||||
}
|
||||
|
||||
#mine-wrapper .game-wrapper .users .user-container .user-control .bomb-container .bomb {
|
||||
-webkit-border-radius: 5px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
#mine-wrapper .game-wrapper .users .user-container.user-blue .user-control .bomb-container .bomb {
|
||||
background: rgb(131, 194, 245);
|
||||
background: -moz-linear-gradient(top, rgba(131, 194, 245, 1) 0%, rgba(108, 190, 230, 1) 39%, rgba(221, 255, 252, 1) 100%);
|
||||
background: -webkit-linear-gradient(top, rgba(131, 194, 245, 1) 0%, rgba(108, 190, 230, 1) 39%, rgba(221, 255, 252, 1) 100%);
|
||||
background: linear-gradient(to bottom, rgba(131, 194, 245, 1) 0%, rgba(108, 190, 230, 1) 39%, rgba(221, 255, 252, 1) 100%);
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#83c2f5', endColorstr='#ddfffc', GradientType=0);
|
||||
border: 3px solid #0b538e;
|
||||
}
|
||||
|
||||
#mine-wrapper .game-wrapper .users .user-container.user-red .user-control .bomb-container .bomb {
|
||||
background: rgb(255, 175, 159);
|
||||
background: -moz-linear-gradient(top, rgba(255, 175, 159, 1) 0%, rgba(231, 113, 7, 1) 54%, rgba(231, 113, 7, 1) 54%, rgba(237, 172, 16, 1) 100%);
|
||||
background: -webkit-linear-gradient(top, rgba(255, 175, 159, 1) 0%, rgba(231, 113, 7, 1) 54%, rgba(231, 113, 7, 1) 54%, rgba(237, 172, 16, 1) 100%);
|
||||
background: linear-gradient(to bottom, rgba(255, 175, 159, 1) 0%, rgba(231, 113, 7, 1) 54%, rgba(231, 113, 7, 1) 54%, rgba(237, 172, 16, 1) 100%);
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffaf9f', endColorstr='#edac10', GradientType=0);
|
||||
border: 3px solid #c9221c;
|
||||
}
|
||||
|
||||
// Resign button
|
||||
|
||||
#mine-wrapper .game-wrapper .users .resign {
|
||||
background: rgba(70, 73, 66, 1);
|
||||
background: -moz-linear-gradient(top, rgba(70, 73, 66, 1) 0%, rgba(140, 138, 139, 1) 69%, rgba(96, 89, 97, 1) 100%);
|
||||
background: -webkit-gradient(left top, left bottom, color-stop(0%, rgba(70, 73, 66, 1)), color-stop(69%, rgba(140, 138, 139, 1)), color-stop(100%, rgba(96, 89, 97, 1)));
|
||||
background: -webkit-linear-gradient(top, rgba(70, 73, 66, 1) 0%, rgba(140, 138, 139, 1) 69%, rgba(96, 89, 97, 1) 100%);
|
||||
background: -o-linear-gradient(top, rgba(70, 73, 66, 1) 0%, rgba(140, 138, 139, 1) 69%, rgba(96, 89, 97, 1) 100%);
|
||||
background: -ms-linear-gradient(top, rgba(70, 73, 66, 1) 0%, rgba(140, 138, 139, 1) 69%, rgba(96, 89, 97, 1) 100%);
|
||||
background: linear-gradient(to bottom, rgba(70, 73, 66, 1) 0%, rgba(140, 138, 139, 1) 69%, rgba(96, 89, 97, 1) 100%);
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#464942', endColorstr='#605961', GradientType=0);
|
||||
display: block;
|
||||
position: relative;
|
||||
width: 95%;
|
||||
height: 50px;
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
font-weight: bold;
|
||||
font-size: 22px;
|
||||
text-transform: uppercase;
|
||||
text-align: center;
|
||||
line-height: 40px;
|
||||
border: 3px solid #484742;
|
||||
color: #fff;
|
||||
margin: 10px auto 0 auto;
|
||||
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
|
||||
-webkit-border-radius: 5px;
|
||||
border-radius: 5px;
|
||||
-webkit-transition: all 250ms ease-in-out;
|
||||
-moz-transition: all 250ms ease-in-out;
|
||||
-o-transition: all 250ms ease-in-out;
|
||||
transition: all 250ms ease-in-out;
|
||||
}
|
||||
|
||||
#mine-wrapper .game-wrapper .users .resign:hover {
|
||||
background: rgba(70, 73, 66, 1);
|
||||
color: #FFFFFF;
|
||||
transition: all 250ms ease-in-out;
|
||||
}
|
||||
|
||||
#mine-wrapper .game-wrapper .users .resign.disabled {
|
||||
background: rgba(70, 73, 66, 1);
|
||||
color: #848484;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
#mine-wrapper .game-wrapper .users .resign.disabled:hover {
|
||||
background: rgba(70, 73, 66, 1);
|
||||
color: #848484;
|
||||
}
|
||||
|
||||
#mine-wrapper .game-wrapper .users .resign .resign-shine {
|
||||
background: rgba(255, 255, 255, 1);
|
||||
background: -moz-linear-gradient(top, rgba(255, 255, 255, 1) 0%, rgba(0, 0, 0, 0) 100%);
|
||||
background: -webkit-gradient(left top, left bottom, color-stop(0%, rgba(255, 255, 255, 1)), color-stop(100%, rgba(0, 0, 0, 0)));
|
||||
background: -webkit-linear-gradient(top, rgba(255, 255, 255, 1) 0%, rgba(0, 0, 0, 0) 100%);
|
||||
background: -o-linear-gradient(top, rgba(255, 255, 255, 1) 0%, rgba(0, 0, 0, 0) 100%);
|
||||
background: -ms-linear-gradient(top, rgba(255, 255, 255, 1) 0%, rgba(0, 0, 0, 0) 100%);
|
||||
background: linear-gradient(to bottom, rgba(255, 255, 255, 1) 0%, rgba(0, 0, 0, 0) 100%);
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffff', endColorstr='#000000', GradientType=0);
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 33%;
|
||||
|
||||
opacity: 0.7;
|
||||
|
||||
-webkit-border-radius: 5px;
|
||||
border-radius: 5px;
|
||||
transition: all 250ms ease-in-out;
|
||||
}
|
||||
|
||||
#mine-wrapper .game-wrapper .users .resign:hover .resign-shine,
|
||||
#mine-wrapper .game-wrapper .users .resign.disabled .resign-shine {
|
||||
display: none;
|
||||
transition: all 250ms ease-in-out;
|
||||
}
|
||||
250
assets/css/mineseeker/_bonus-box.scss
Normal file
250
assets/css/mineseeker/_bonus-box.scss
Normal file
@@ -0,0 +1,250 @@
|
||||
/*!*
|
||||
* This file is part of the SplendidBear Websites' projects.
|
||||
*
|
||||
* Copyright (c) 2026 @ www.splendidbear.org
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
#mine-wrapper .bonus-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
min-width: 58px;
|
||||
padding: 6px 12px;
|
||||
border-radius: 8px;
|
||||
border: 2px solid transparent;
|
||||
background: #07090d;
|
||||
font-family: 'Rajdhani', sans-serif;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
transition: all 0.25s ease;
|
||||
align-self: stretch;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-1px);
|
||||
filter: brightness(1.15);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
#mine-wrapper .bonus-box.red-bonus {
|
||||
background: linear-gradient(to bottom, #2a0502 0%, #4a1510 100%);
|
||||
border-color: rgba(246, 125, 82, 0.4);
|
||||
color: rgba(246, 125, 82, 0.85);
|
||||
|
||||
&:hover {
|
||||
border-color: rgba(246, 125, 82, 0.85);
|
||||
box-shadow: 0 0 12px rgba(173, 10, 5, 0.6);
|
||||
}
|
||||
}
|
||||
|
||||
#mine-wrapper .bonus-box.blue-bonus {
|
||||
background: linear-gradient(to bottom, #050f18 0%, #0f2838 100%);
|
||||
border-color: rgba(149, 207, 245, 0.4);
|
||||
color: rgba(149, 207, 245, 0.85);
|
||||
|
||||
&:hover {
|
||||
border-color: rgba(149, 207, 245, 0.85);
|
||||
box-shadow: 0 0 12px rgba(35, 111, 135, 0.6);
|
||||
}
|
||||
}
|
||||
|
||||
#mine-wrapper .bonus-box__icon {
|
||||
font-size: 13px;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
#mine-wrapper .bonus-box__value {
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 16px;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.bsd {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-family: 'Rajdhani', sans-serif;
|
||||
}
|
||||
|
||||
.bsd-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 18px 22px 14px;
|
||||
border-bottom: 1px solid rgba(35, 111, 135, 0.3);
|
||||
}
|
||||
|
||||
.bsd-header-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.bsd-label {
|
||||
font-size: 11px;
|
||||
letter-spacing: 2px;
|
||||
color: rgba(149, 207, 245, 0.7);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.bsd-title {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
color: #fff;
|
||||
|
||||
.fa {
|
||||
color: #f6d572;
|
||||
}
|
||||
}
|
||||
|
||||
.bsd-close {
|
||||
background: transparent;
|
||||
border: 1px solid rgba(35, 111, 135, 0.4);
|
||||
border-radius: 6px;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
color: #fff;
|
||||
border-color: rgba(149, 207, 245, 0.8);
|
||||
}
|
||||
}
|
||||
|
||||
.bsd-body {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 14px;
|
||||
padding: 18px 22px;
|
||||
}
|
||||
|
||||
.bsd-column {
|
||||
border-radius: 10px;
|
||||
padding: 14px;
|
||||
border: 1px solid transparent;
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
}
|
||||
|
||||
.bsd-column--red {
|
||||
border-color: rgba(246, 125, 82, 0.35);
|
||||
background: linear-gradient(to bottom, rgba(74, 6, 3, 0.35), rgba(107, 37, 21, 0.15));
|
||||
}
|
||||
|
||||
.bsd-column--blue {
|
||||
border-color: rgba(149, 207, 245, 0.35);
|
||||
background: linear-gradient(to bottom, rgba(11, 37, 48, 0.35), rgba(22, 61, 85, 0.15));
|
||||
}
|
||||
|
||||
.bsd-column-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 10px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.bsd-column-name {
|
||||
font-weight: 700;
|
||||
font-size: 16px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.bsd-column-total {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: #f6d572;
|
||||
|
||||
.fa {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.bsd-stats {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.bsd-stat {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
padding: 6px 2px;
|
||||
border-bottom: 1px dashed rgba(255, 255, 255, 0.06);
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.bsd-stat-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.bsd-stat-label {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: rgba(255, 255, 255, 0.92);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.bsd-stat-desc {
|
||||
font-size: 11px;
|
||||
color: rgba(255, 255, 255, 0.48);
|
||||
line-height: 1.25;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
.bsd-stat-value {
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
min-width: 24px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.bsd-note {
|
||||
margin: 0;
|
||||
padding: 12px 22px 18px;
|
||||
font-size: 12px;
|
||||
color: rgba(255, 255, 255, 0.45);
|
||||
text-align: center;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
@media (max-width: 520px) {
|
||||
.bsd-body {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
216
assets/css/mineseeker/_grid.scss
Normal file
216
assets/css/mineseeker/_grid.scss
Normal file
@@ -0,0 +1,216 @@
|
||||
/*!*
|
||||
* This file is part of the SplendidBear Websites' projects.
|
||||
*
|
||||
* Copyright (c) 2026 @ www.splendidbear.org
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
#mine-wrapper .grid {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 643px;
|
||||
border: 1px solid #cac3e5;
|
||||
|
||||
cursor: none;
|
||||
}
|
||||
|
||||
#mine-wrapper .grid-container {
|
||||
background: #4E4E4E;
|
||||
padding: 15px 10px;
|
||||
|
||||
-webkit-border-radius: 5px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
#mine-wrapper .grid .field-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#mine-wrapper .grid .field-wrapper > img.field-target {
|
||||
position: absolute;
|
||||
display: none;
|
||||
width: 45px;
|
||||
top: -2.5px;
|
||||
left: -2.5px;
|
||||
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
#mine-wrapper .grid .field-wrapper:hover > img.field-target {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#mine-wrapper .grid .field-wrapper > img.field-bomb-target {
|
||||
position: absolute;
|
||||
display: block;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
#mine-wrapper .grid .field-wrapper > img.field-blue-last,
|
||||
#mine-wrapper .grid .field-wrapper > img.field-red-last {
|
||||
position: absolute;
|
||||
display: none;
|
||||
width: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 99;
|
||||
}
|
||||
|
||||
#mine-wrapper .grid .field-wrapper > img.field-blue-last.last-clicked,
|
||||
#mine-wrapper .grid .field-wrapper > img.field-red-last.last-clicked {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#mine-wrapper .grid .field-wrapper .field {
|
||||
background: #61defa;
|
||||
background: -moz-linear-gradient(left, #61defa 0%, #119dec 100%);
|
||||
background: -webkit-linear-gradient(left, #61defa 0%, #119dec 100%);
|
||||
background: linear-gradient(to right, #61defa 0%, #119dec 100%);
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#61defa', endColorstr='#119dec', GradientType=1);
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 2px solid #51c2fe;
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
font-weight: bold;
|
||||
font-size: 35px;
|
||||
text-align: center;
|
||||
line-height: 35px;
|
||||
}
|
||||
|
||||
#mine-wrapper .grid .field-wrapper .field .field-corner {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#mine-wrapper .grid .field-wrapper .field.active {
|
||||
background: #fde717;
|
||||
background: -moz-linear-gradient(left, #fde717 0%, #f5b807 100%);
|
||||
background: -webkit-linear-gradient(left, #fde717 0%, #f5b807 100%);
|
||||
background: linear-gradient(to right, #fde717 0%, #f5b807 100%);
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fde717', endColorstr='#f5b807', GradientType=1);
|
||||
border: 2px solid #f6d762;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
#mine-wrapper .grid .field-wrapper .field.active .flag-number {
|
||||
-webkit-animation: bubbleNumber 500ms cubic-bezier(.36, .07, .19, .97) both;
|
||||
animation: bubbleNumber 500ms cubic-bezier(.36, .07, .19, .97) both;
|
||||
-webkit-transform: scale(1);
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
@keyframes bubbleNumber {
|
||||
0% {
|
||||
background: #61defa;
|
||||
background: -moz-linear-gradient(left, #61defa 0%, #119dec 100%);
|
||||
background: -webkit-linear-gradient(left, #61defa 0%, #119dec 100%);
|
||||
background: linear-gradient(to right, #61defa 0%, #119dec 100%);
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#61defa', endColorstr='#119dec', GradientType=1);
|
||||
|
||||
-webkit-border-radius: 50%;
|
||||
border-radius: 50%;
|
||||
-webkit-transform: scale(1);
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
-webkit-border-radius: 50%;
|
||||
border-radius: 50%;
|
||||
-webkit-transform: scale(0);
|
||||
transform: scale(0);
|
||||
}
|
||||
100% {
|
||||
-webkit-border-radius: 0;
|
||||
border-radius: 0;
|
||||
-webkit-transform: scale(1);
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
#mine-wrapper .grid .field-wrapper .field.active .flag-mine {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#mine-wrapper .grid .field-wrapper .field.active .flag-mine > img {
|
||||
width: 75%;
|
||||
margin-left: 15px;
|
||||
|
||||
-ms-transform: rotate(7deg);
|
||||
-webkit-transform: rotate(7deg);
|
||||
transform: rotate(7deg);
|
||||
-webkit-animation: mineFlagLoad 500ms cubic-bezier(.36, .07, .19, .97) both;
|
||||
animation: mineFlagLoad 500ms cubic-bezier(.36, .07, .19, .97) both;
|
||||
}
|
||||
|
||||
@keyframes mineFlagLoad {
|
||||
0% {
|
||||
margin-bottom: 0;
|
||||
margin-left: 15px;
|
||||
-ms-transform: rotate(9deg);
|
||||
-webkit-transform: rotate(9deg);
|
||||
transform: rotate(9deg);
|
||||
}
|
||||
50% {
|
||||
margin-bottom: -5px;
|
||||
margin-left: 7px;
|
||||
}
|
||||
100% {
|
||||
margin-bottom: 3px;
|
||||
margin-left: 0;
|
||||
-ms-transform: rotate(-9deg);
|
||||
-webkit-transform: rotate(-9deg);
|
||||
transform: rotate(-9deg);
|
||||
}
|
||||
}
|
||||
|
||||
#mine-wrapper .grid .field-wrapper .field.active .flag-mine .flag-mine-base {
|
||||
position: absolute;
|
||||
background: #000000;
|
||||
width: 25px;
|
||||
height: 22px;
|
||||
bottom: -12px;
|
||||
left: 50%;
|
||||
margin-left: -10.5px;
|
||||
|
||||
-webkit-border-radius: 50%;
|
||||
border-radius: 50%;
|
||||
-webkit-animation: mineBaseLoad 500ms cubic-bezier(.36, .07, .19, .97) both;
|
||||
animation: mineBaseLoad 500ms cubic-bezier(.36, .07, .19, .97) both;
|
||||
}
|
||||
|
||||
@keyframes mineBaseLoad {
|
||||
0% { margin-bottom: 0; }
|
||||
50% { margin-bottom: -5px; }
|
||||
100% { margin-bottom: 0; }
|
||||
}
|
||||
|
||||
#mine-wrapper .grid .field-wrapper .field.active.mine {
|
||||
background: #61defa;
|
||||
background: -moz-linear-gradient(left, #61defa 0%, #119dec 100%);
|
||||
background: -webkit-linear-gradient(left, #61defa 0%, #119dec 100%);
|
||||
background: linear-gradient(to right, #61defa 0%, #119dec 100%);
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#61defa', endColorstr='#119dec', GradientType=1);
|
||||
border: 2px solid #51c2fe;
|
||||
}
|
||||
|
||||
#mine-wrapper .grid .field-wrapper .field.active.color-1 { color: #0000ff; }
|
||||
#mine-wrapper .grid .field-wrapper .field.active.color-2 { color: #079433; }
|
||||
#mine-wrapper .grid .field-wrapper .field.active.color-3 { color: #fd1400; }
|
||||
#mine-wrapper .grid .field-wrapper .field.active.color-4 { color: #0c099e; }
|
||||
#mine-wrapper .grid .field-wrapper .field.active.color-5 { color: #7b4c01; }
|
||||
#mine-wrapper .grid .field-wrapper .field.active.color-6 { color: #008388; }
|
||||
#mine-wrapper .grid .field-wrapper .field.active.color-7 { color: #000000; }
|
||||
#mine-wrapper .grid .field-wrapper .field.active.color-8 { color: #ff0000; }
|
||||
|
||||
#mine-wrapper .grid .field-wrapper .field img {
|
||||
width: 80%;
|
||||
}
|
||||
99
assets/css/mineseeker/_mine-counter.scss
Normal file
99
assets/css/mineseeker/_mine-counter.scss
Normal file
@@ -0,0 +1,99 @@
|
||||
/*!*
|
||||
* This file is part of the SplendidBear Websites' projects.
|
||||
*
|
||||
* Copyright (c) 2026 @ www.splendidbear.org
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
#mine-wrapper .game-wrapper .users .active-mines-container {
|
||||
background: -moz-radial-gradient(center, ellipse cover, rgba(255, 252, 252, 1) 0%, rgba(255, 252, 252, 0.99) 1%, rgba(106, 106, 106, 0.39) 61%, rgba(106, 106, 106, 0) 100%);
|
||||
background: -webkit-radial-gradient(center, ellipse cover, rgba(255, 252, 252, 1) 0%, rgba(255, 252, 252, 0.99) 1%, rgba(106, 106, 106, 0.39) 61%, rgba(106, 106, 106, 0) 100%);
|
||||
background: radial-gradient(ellipse at center, rgba(255, 252, 252, 1) 0%, rgba(255, 252, 252, 0.99) 1%, rgba(106, 106, 106, 0.39) 61%, rgba(106, 106, 106, 0) 100%);
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcfc', endColorstr='#006a6a6a', GradientType=1);
|
||||
background-repeat: no-repeat;
|
||||
background-position: center center;
|
||||
background-size: 72% 179%;
|
||||
position: relative;
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
#mine-wrapper .game-wrapper .users .active-mines-container i {
|
||||
font-size: 27px;
|
||||
color: #b1b1b3;
|
||||
margin-top: 3px;
|
||||
|
||||
text-shadow: 0 0 3px #000000;
|
||||
}
|
||||
|
||||
#mine-wrapper .game-wrapper .users .active-mines-container i:first-child {
|
||||
float: left;
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
#mine-wrapper .game-wrapper .users .active-mines-container i:last-child {
|
||||
float: right;
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
#mine-wrapper .game-wrapper .users .active-mines-container .active-mines {
|
||||
background: -moz-linear-gradient(top, rgba(0, 0, 0, 1) 0%, rgba(135, 136, 131, 1) 100%);
|
||||
background: -webkit-linear-gradient(top, rgba(0, 0, 0, 1) 0%, rgba(135, 136, 131, 1) 100%);
|
||||
background: linear-gradient(to bottom, rgba(0, 0, 0, 1) 0%, rgba(135, 136, 131, 1) 100%);
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#3e3f41', endColorstr='#878883', GradientType=0);
|
||||
position: absolute;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
top: -7.5px;
|
||||
left: 50%;
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
font-size: 25px;
|
||||
line-height: 39px;
|
||||
text-align: center;
|
||||
color: #FFFFFF;
|
||||
border: 5px solid #000000;
|
||||
margin-left: -25px;
|
||||
|
||||
z-index: 100;
|
||||
|
||||
-webkit-border-radius: 50%;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
#mine-wrapper .game-wrapper .users .active-mines-container .active-mines.found-mine {
|
||||
-webkit-animation: bubbleLeftMine 750ms cubic-bezier(.36, .07, .19, .97) both;
|
||||
animation: bubbleLeftMine 750ms cubic-bezier(.36, .07, .19, .97) both;
|
||||
}
|
||||
|
||||
@keyframes bubbleLeftMine {
|
||||
0% { -webkit-transform: scale(1); transform: scale(1); }
|
||||
50% { border-color: #2e3337; -webkit-transform: scale(2); transform: scale(2); }
|
||||
100% { -webkit-transform: scale(1); transform: scale(1); }
|
||||
}
|
||||
|
||||
#mine-wrapper .game-wrapper .users .active-mines-container .active-mines .active-mines-shine {
|
||||
background: -moz-linear-gradient(top, rgba(213, 214, 216, 1) 0%, rgba(106, 106, 106, 1) 100%);
|
||||
background: -webkit-linear-gradient(top, rgba(213, 214, 216, 1) 0%, rgba(106, 106, 106, 1) 100%);
|
||||
background: linear-gradient(to bottom, rgba(213, 214, 216, 1) 0%, rgba(106, 106, 106, 1) 100%);
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#d5d6d8', endColorstr='#6a6a6a', GradientType=0);
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 50%;
|
||||
width: 30px;
|
||||
height: 20px;
|
||||
margin-left: -14.5px;
|
||||
|
||||
z-index: 101;
|
||||
|
||||
-webkit-border-radius: 50%;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
#mine-wrapper .game-wrapper .users .active-mines-container .active-mines .active-mines-nbr {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
|
||||
z-index: 102;
|
||||
}
|
||||
780
assets/css/mineseeker/_overlay.scss
Normal file
780
assets/css/mineseeker/_overlay.scss
Normal file
@@ -0,0 +1,780 @@
|
||||
/*!*
|
||||
* This file is part of the SplendidBear Websites' projects.
|
||||
*
|
||||
* Copyright (c) 2026 @ www.splendidbear.org
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
#mine-wrapper .game-wrapper .game-overlay {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
|
||||
z-index: 200;
|
||||
|
||||
-webkit-border-radius: 10px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
#mine-wrapper .game-wrapper .game-overlay.hide {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#mine-wrapper .game-wrapper .game-overlay .game-overlay-window {
|
||||
background: linear-gradient(135deg, rgba(7, 9, 13, 0.98) 0%, rgba(10, 20, 35, 0.98) 100%);
|
||||
border: 2px solid rgba(35, 111, 135, 0.4);
|
||||
backdrop-filter: blur(12px);
|
||||
font-family: 'Rajdhani', sans-serif;
|
||||
color: #fff;
|
||||
width: 100%;
|
||||
max-width: 680px;
|
||||
padding: 40px;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 25px 50px rgba(0, 0, 0, 0.7), 0 0 40px rgba(35, 111, 135, 0.15);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
animation: slideUp 0.5s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
overflow: hidden;
|
||||
max-height: 90vh;
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
#mine-wrapper .game-wrapper .game-overlay .game-overlay-window h1 {
|
||||
font-weight: 800;
|
||||
font-size: 32px;
|
||||
color: #fff;
|
||||
margin: 0 0 50px 0;
|
||||
letter-spacing: 1px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
#mine-wrapper .game-wrapper .game-overlay .game-overlay-window h2 {
|
||||
font-size: 14px;
|
||||
color: rgba(149, 207, 245, 0.6);
|
||||
margin: 0 0 30px 0;
|
||||
letter-spacing: 2px;
|
||||
text-transform: uppercase;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
#mine-wrapper .game-wrapper .game-overlay .game-overlay-window .resign {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
text-transform: none;
|
||||
letter-spacing: normal;
|
||||
font-size: inherit;
|
||||
|
||||
a {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
background: linear-gradient(to bottom, #236f87 0%, #1a5068 100%);
|
||||
border: 2px solid #2e7a9a;
|
||||
color: #e0f4ff;
|
||||
font: 800 13px 'Rajdhani', sans-serif;
|
||||
letter-spacing: 2px;
|
||||
text-transform: uppercase;
|
||||
padding: 12px 24px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 300ms cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
width: 100%;
|
||||
max-width: 260px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 12px rgba(35, 111, 135, 0.25);
|
||||
text-decoration: none;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
|
||||
transition: left 0.4s ease;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: linear-gradient(to bottom, #2d8aa8 0%, #236f87 100%);
|
||||
border-color: #5ba4d4;
|
||||
color: #fff;
|
||||
box-shadow: 0 8px 24px rgba(35, 111, 135, 0.4);
|
||||
transform: translateY(-2px);
|
||||
|
||||
&::before {
|
||||
left: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
background: linear-gradient(to bottom, #8a2323 0%, #681a1a 100%);
|
||||
border-color: #9a2e2e;
|
||||
color: #ffe0e0;
|
||||
box-shadow: 0 4px 12px rgba(135, 35, 35, 0.25);
|
||||
|
||||
&:hover {
|
||||
background: linear-gradient(to bottom, #a82d2d 0%, #872323 100%);
|
||||
border-color: #d45b5b;
|
||||
color: #fff;
|
||||
box-shadow: 0 8px 24px rgba(135, 35, 35, 0.4);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#mine-wrapper .game-wrapper .game-overlay .game-overlay-window:has(.resign) h1 {
|
||||
text-align: center;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
#mine-wrapper .game-wrapper .game-overlay .game-overlay-window:has(.resign) h2 {
|
||||
text-align: center;
|
||||
color: inherit;
|
||||
font-size: inherit;
|
||||
letter-spacing: normal;
|
||||
text-transform: none;
|
||||
font-weight: normal;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#mine-wrapper .game-wrapper .game-overlay .game-overlay-window h3 {
|
||||
font-size: 16px;
|
||||
color: #236f87;
|
||||
}
|
||||
|
||||
#mine-wrapper .game-wrapper .game-overlay .game-overlay-window .share-invite {
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
#mine-wrapper .game-wrapper .game-overlay .game-overlay-window .share-invite-label {
|
||||
font-size: 13px;
|
||||
color: #386e8c;
|
||||
margin: 0 0 8px;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.waiting-options {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto 1fr;
|
||||
grid-template-rows: auto;
|
||||
gap: 10px;
|
||||
align-items: stretch;
|
||||
width: 100%;
|
||||
animation: fadeInUp 0.6s ease-out 0.2s both;
|
||||
|
||||
&.waiting-options--invite-only {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.waiting-option {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
padding: 15px;
|
||||
background: linear-gradient(135deg, rgba(35, 111, 135, 0.08) 0%, rgba(26, 80, 104, 0.08) 100%);
|
||||
border: 2px solid rgba(35, 111, 135, 0.2);
|
||||
border-radius: 12px;
|
||||
transition: all 350ms cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
animation: scaleIn 0.5s ease-out;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, transparent, rgba(35, 111, 135, 0.15), transparent);
|
||||
transition: left 0.5s ease;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: rgba(35, 111, 135, 0.45);
|
||||
background: linear-gradient(135deg, rgba(35, 111, 135, 0.12) 0%, rgba(26, 80, 104, 0.12) 100%);
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 12px 30px rgba(35, 111, 135, 0.2);
|
||||
|
||||
&::before {
|
||||
left: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.waiting-option-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
font-weight: 800;
|
||||
font-size: 17px;
|
||||
color: #fff;
|
||||
letter-spacing: 1px;
|
||||
text-transform: uppercase;
|
||||
|
||||
i {
|
||||
font-size: 20px;
|
||||
color: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
background: linear-gradient(135deg, rgba(35, 111, 135, 0.4) 0%, rgba(35, 111, 135, 0.2) 100%);
|
||||
border: 2px solid rgba(35, 111, 135, 0.5);
|
||||
border-radius: 8px;
|
||||
transition: all 400ms cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&:hover i {
|
||||
background: linear-gradient(135deg, rgba(35, 111, 135, 0.6) 0%, rgba(35, 111, 135, 0.4) 100%);
|
||||
border-color: rgba(35, 111, 135, 0.8);
|
||||
transform: scale(1.15) rotate(-8deg);
|
||||
box-shadow: 0 0 20px rgba(35, 111, 135, 0.4);
|
||||
}
|
||||
}
|
||||
|
||||
.waiting-option-desc {
|
||||
font: 600 12px 'Rajdhani', sans-serif;
|
||||
color: rgba(149, 207, 245, 0.75);
|
||||
margin: 0;
|
||||
letter-spacing: 0.4px;
|
||||
line-height: 1.4;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
.waiting-divider {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
margin: 0;
|
||||
animation: slideIn 0.7s ease-out 0.4s both;
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
content: '';
|
||||
width: 2px;
|
||||
height: 20px;
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
rgba(35, 111, 135, 0.1),
|
||||
rgba(35, 111, 135, 0.4),
|
||||
rgba(35, 111, 135, 0.1)
|
||||
);
|
||||
}
|
||||
|
||||
&::after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
span {
|
||||
font: 700 11px 'Rajdhani', sans-serif;
|
||||
color: rgba(35, 111, 135, 0.6);
|
||||
letter-spacing: 2px;
|
||||
text-transform: uppercase;
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
flex-direction: row;
|
||||
margin: 8px 0;
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
content: '';
|
||||
flex: 1;
|
||||
width: auto;
|
||||
height: 1px;
|
||||
background: linear-gradient(
|
||||
to right,
|
||||
rgba(35, 111, 135, 0),
|
||||
rgba(35, 111, 135, 0.3),
|
||||
rgba(35, 111, 135, 0)
|
||||
);
|
||||
}
|
||||
|
||||
&::after {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#mine-wrapper .game-wrapper .game-overlay .game-overlay-window .share-invite {
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#mine-wrapper .game-wrapper .game-overlay .game-overlay-window .share-url-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: linear-gradient(135deg, #d0e8f5 0%, #c5dff0 100%);
|
||||
border: 2px solid #7ab8d8;
|
||||
border-radius: 8px;
|
||||
padding: 0 10px;
|
||||
cursor: text;
|
||||
transition: all 300ms ease;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
&:hover {
|
||||
border-color: #236f87;
|
||||
box-shadow: 0 4px 12px rgba(35, 111, 135, 0.2);
|
||||
}
|
||||
|
||||
&:focus-within {
|
||||
border-color: #236f87;
|
||||
box-shadow: 0 0 16px rgba(35, 111, 135, 0.35);
|
||||
}
|
||||
}
|
||||
|
||||
#mine-wrapper .game-wrapper .game-overlay .game-overlay-window .share-url-icon {
|
||||
color: #236f87;
|
||||
font-size: 13px;
|
||||
flex-shrink: 0;
|
||||
margin-right: 8px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
#mine-wrapper .game-wrapper .game-overlay .game-overlay-window .share-url-input {
|
||||
flex: 1;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
outline: 0;
|
||||
height: 40px;
|
||||
color: #1a4a6a;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
letter-spacing: 0.5px;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
cursor: text;
|
||||
min-width: 0;
|
||||
|
||||
&::selection {
|
||||
background: rgba(35, 111, 135, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes scaleIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.95);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
#mine-wrapper .game-wrapper .game-overlay .game-overlay-window .browse-players-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
background: linear-gradient(to bottom, #236f87 0%, #1a5068 100%);
|
||||
border: 2px solid #2e7a9a;
|
||||
color: #e0f4ff;
|
||||
font: 700 13px 'Rajdhani', sans-serif;
|
||||
letter-spacing: 2px;
|
||||
text-transform: uppercase;
|
||||
padding: 12px 24px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 300ms cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
width: 100%;
|
||||
font-weight: 800;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 12px rgba(35, 111, 135, 0.25);
|
||||
|
||||
i {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
|
||||
transition: left 0.4s ease;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: linear-gradient(to bottom, #2d8aa8 0%, #236f87 100%);
|
||||
border-color: #5ba4d4;
|
||||
color: #fff;
|
||||
box-shadow: 0 8px 24px rgba(35, 111, 135, 0.4);
|
||||
transform: translateY(-2px);
|
||||
|
||||
&::before {
|
||||
left: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
#mine-wrapper .game-wrapper .game-overlay .game-overlay-window .share-copy-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 9px;
|
||||
background: linear-gradient(to bottom, #236f87 0%, #1a5068 100%);
|
||||
border: 2px solid #2e7a9a;
|
||||
color: #e0f4ff;
|
||||
font-family: 'Rajdhani', sans-serif;
|
||||
font-size: 13px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 1px;
|
||||
text-transform: uppercase;
|
||||
padding: 12px 24px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 300ms cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
width: 100%;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 12px rgba(35, 111, 135, 0.25);
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
|
||||
transition: left 0.4s ease;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: linear-gradient(to bottom, #2d8aa8 0%, #236f87 100%);
|
||||
border-color: #5ba4d4;
|
||||
color: #fff;
|
||||
box-shadow: 0 8px 24px rgba(35, 111, 135, 0.4);
|
||||
transform: translateY(-2px);
|
||||
|
||||
&::before {
|
||||
left: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
&.copied {
|
||||
background: linear-gradient(to bottom, #1a6844 0%, #135233 100%);
|
||||
border-color: #2a9e60;
|
||||
color: #a0f0c0;
|
||||
box-shadow: 0 4px 12px rgba(26, 104, 68, 0.4);
|
||||
}
|
||||
}
|
||||
|
||||
#mine-wrapper .game-wrapper .game-overlay .game-overlay-window .game-overlay-actions {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
gap: 12px;
|
||||
margin-top: 20px;
|
||||
width: 100%;
|
||||
|
||||
> * {
|
||||
flex: 1 1 0;
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
#mine-wrapper .game-wrapper .game-overlay .game-overlay-window .game-overlay-share {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 9px;
|
||||
background: linear-gradient(to bottom, #236f87 0%, #1a5068 100%);
|
||||
border: 2px solid #2e7a9a;
|
||||
color: #e0f4ff;
|
||||
font-family: 'Rajdhani', sans-serif;
|
||||
font-size: 13px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 1px;
|
||||
text-transform: uppercase;
|
||||
padding: 12px 24px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 300ms cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
width: 100%;
|
||||
margin-top: 20px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 12px rgba(35, 111, 135, 0.25);
|
||||
z-index: 10;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
|
||||
transition: left 0.4s ease;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: linear-gradient(to bottom, #2d8aa8 0%, #236f87 100%);
|
||||
border-color: #5ba4d4;
|
||||
color: #fff;
|
||||
box-shadow: 0 8px 24px rgba(35, 111, 135, 0.4);
|
||||
transform: translateY(-2px);
|
||||
|
||||
&::before {
|
||||
left: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
&.copied {
|
||||
background: linear-gradient(to bottom, #1a6844 0%, #135233 100%);
|
||||
border-color: #2a9e60;
|
||||
color: #a0f0c0;
|
||||
box-shadow: 0 4px 12px rgba(26, 104, 68, 0.4);
|
||||
}
|
||||
|
||||
i {
|
||||
font-size: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
#mine-wrapper .game-wrapper .game-overlay .game-overlay-window .game-overlay-profile {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 9px;
|
||||
background: linear-gradient(to bottom, #1a6844 0%, #135233 100%);
|
||||
border: 2px solid #2a9e60;
|
||||
color: #d0ffe0;
|
||||
font-family: 'Rajdhani', sans-serif;
|
||||
font-size: 13px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 1px;
|
||||
text-transform: uppercase;
|
||||
padding: 12px 24px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 300ms cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
width: 100%;
|
||||
margin-top: 20px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 12px rgba(42, 158, 96, 0.25);
|
||||
text-decoration: none;
|
||||
z-index: 10;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
|
||||
transition: left 0.4s ease;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: linear-gradient(to bottom, #238f5c 0%, #1a6844 100%);
|
||||
border-color: #5ee89a;
|
||||
color: #fff;
|
||||
box-shadow: 0 8px 24px rgba(42, 158, 96, 0.4);
|
||||
transform: translateY(-2px);
|
||||
|
||||
&::before {
|
||||
left: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
i {
|
||||
font-size: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
// CaptchaOverlay Styles
|
||||
.captcha-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(7, 9, 13, 0.95);
|
||||
backdrop-filter: blur(8px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.captcha-content {
|
||||
text-align: center;
|
||||
color: #fff;
|
||||
max-width: 400px;
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
.captcha-icon {
|
||||
font-size: 64px;
|
||||
color: #236f87;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.captcha-title {
|
||||
font: 800 32px 'Rajdhani', sans-serif;
|
||||
margin: 0 0 16px;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.captcha-description {
|
||||
color: rgba(149, 207, 245, 0.7);
|
||||
font: 400 16px 'Rajdhani', sans-serif;
|
||||
margin: 0 0 32px;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.captcha-button {
|
||||
background: linear-gradient(#236f87 0%, #1a5068 100%);
|
||||
border: 2px solid #2e7a9a;
|
||||
border-radius: 8px;
|
||||
color: #e0f4ff;
|
||||
cursor: pointer;
|
||||
font: 800 18px 'Rajdhani', sans-serif;
|
||||
letter-spacing: 2px;
|
||||
padding: 16px 40px;
|
||||
text-transform: uppercase;
|
||||
transition: all 0.3s ease;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
opacity: 1;
|
||||
|
||||
i {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.7;
|
||||
cursor: wait;
|
||||
}
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: linear-gradient(#2d8aa8 0%, #236f87 100%);
|
||||
border-color: #5ba4d4;
|
||||
color: #fff;
|
||||
box-shadow: 0 8px 24px rgba(35, 111, 135, 0.4);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
&:active:not(:disabled) {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
&.captcha-button--error {
|
||||
background: linear-gradient(#8a2323 0%, #681a1a 100%);
|
||||
border-color: #9a2e2e;
|
||||
|
||||
&:hover {
|
||||
background: linear-gradient(#a82d2d 0%, #872323 100%);
|
||||
border-color: #d45b5b;
|
||||
box-shadow: 0 8px 24px rgba(135, 35, 35, 0.4);
|
||||
}
|
||||
}
|
||||
|
||||
&.captcha-button--loading {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
50
assets/css/mineseeker/_responsive.scss
Normal file
50
assets/css/mineseeker/_responsive.scss
Normal file
@@ -0,0 +1,50 @@
|
||||
/*!*
|
||||
* This file is part of the SplendidBear Websites' projects.
|
||||
*
|
||||
* Copyright (c) 2026 @ www.splendidbear.org
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
@media screen and (max-width: 900px) {
|
||||
#mine-wrapper .game-wrapper .users {
|
||||
visibility: hidden;
|
||||
display: none;
|
||||
}
|
||||
|
||||
#mine-wrapper {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#mine-wrapper .game-wrapper {
|
||||
width: 100%;
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
|
||||
#mine-wrapper .grid-container {
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#mine-wrapper .grid {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#mine-wrapper .grid .field-wrapper {
|
||||
width: 6.25%;
|
||||
aspect-ratio: 1;
|
||||
}
|
||||
|
||||
#mine-wrapper .grid .field-wrapper > img.field-target {
|
||||
width: 105%;
|
||||
top: -2.5%;
|
||||
left: -2.5%;
|
||||
}
|
||||
|
||||
#mine-wrapper .grid .field-wrapper .field {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
111
assets/css/mineseeker/_timer.scss
Normal file
111
assets/css/mineseeker/_timer.scss
Normal file
@@ -0,0 +1,111 @@
|
||||
/*!*
|
||||
* This file is part of the SplendidBear Websites' projects.
|
||||
*
|
||||
* Copyright (c) 2026 @ www.splendidbear.org
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
#mine-wrapper .game-timer-container {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
justify-content: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
#mine-wrapper .game-timer {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 115px;
|
||||
border-radius: 8px;
|
||||
padding: 8px 18px;
|
||||
font-family: 'Rajdhani', sans-serif;
|
||||
font-weight: bold;
|
||||
border: 2px solid transparent;
|
||||
transition: all 0.4s ease;
|
||||
}
|
||||
|
||||
// Red – waiting
|
||||
#mine-wrapper .game-timer.red-timer {
|
||||
background: linear-gradient(to bottom, #4a0603 0%, #6b2515 100%);
|
||||
border-color: #7a1e10;
|
||||
color: rgba(246, 125, 82, 0.55);
|
||||
}
|
||||
|
||||
// Red – active (thinking)
|
||||
#mine-wrapper .game-timer.red-timer.active {
|
||||
background: linear-gradient(to bottom, #ad0a05 0%, #f67d52 100%);
|
||||
border-color: #ff9b6b;
|
||||
color: #fff;
|
||||
box-shadow: 0 0 16px rgba(173, 10, 5, 0.75), 0 0 5px rgba(246, 125, 82, 0.5);
|
||||
}
|
||||
|
||||
// Blue – waiting
|
||||
#mine-wrapper .game-timer.blue-timer {
|
||||
background: linear-gradient(to bottom, #0b2530 0%, #163d55 100%);
|
||||
border-color: #173650;
|
||||
color: rgba(149, 207, 245, 0.55);
|
||||
}
|
||||
|
||||
// Blue – active (thinking)
|
||||
#mine-wrapper .game-timer.blue-timer.active {
|
||||
background: linear-gradient(to bottom, #236f87 0%, #95cff5 100%);
|
||||
border-color: #b8e5ff;
|
||||
color: #fff;
|
||||
box-shadow: 0 0 16px rgba(35, 111, 135, 0.75), 0 0 5px rgba(149, 207, 245, 0.5);
|
||||
}
|
||||
|
||||
#mine-wrapper .game-timer .timer-avatar {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 10px;
|
||||
font-weight: bold;
|
||||
font-family: 'Rajdhani', sans-serif;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
|
||||
#mine-wrapper .game-timer .timer-avatar__img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
#mine-wrapper .game-timer.red-timer .timer-avatar__initials {
|
||||
color: rgba(246, 125, 82, 0.85);
|
||||
}
|
||||
|
||||
#mine-wrapper .game-timer.blue-timer .timer-avatar__initials {
|
||||
color: rgba(149, 207, 245, 0.85);
|
||||
}
|
||||
|
||||
#mine-wrapper .game-timer .timer-icon {
|
||||
font-size: 15px;
|
||||
opacity: 0.7;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
#mine-wrapper .game-timer.active .timer-icon {
|
||||
opacity: 1;
|
||||
animation: timer-icon-pulse 1.6s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes timer-icon-pulse {
|
||||
0%, 100% { transform: scale(1); opacity: 0.85; }
|
||||
50% { transform: scale(1.2); opacity: 1; }
|
||||
}
|
||||
|
||||
#mine-wrapper .game-timer .timer-display {
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 20px;
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
171
assets/css/mineseeker/_users.scss
Normal file
171
assets/css/mineseeker/_users.scss
Normal file
@@ -0,0 +1,171 @@
|
||||
/*!*
|
||||
* This file is part of the SplendidBear Websites' projects.
|
||||
*
|
||||
* Copyright (c) 2026 @ www.splendidbear.org
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
#mine-wrapper .game-wrapper .users {
|
||||
width: 180px;
|
||||
padding: 0 10px 0 0;
|
||||
}
|
||||
|
||||
#mine-wrapper .game-wrapper .users .user-container {
|
||||
background: #FFFFFF;
|
||||
height: 40%;
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
padding: 5px;
|
||||
margin: 5px;
|
||||
|
||||
z-index: 99;
|
||||
|
||||
-webkit-border-radius: 10px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
#mine-wrapper .game-wrapper .users .user-container.user-blue {
|
||||
background: rgb(35, 111, 135);
|
||||
background: -moz-linear-gradient(top, rgba(35, 111, 135, 1) 0%, rgba(149, 207, 245, 1) 100%);
|
||||
background: -webkit-linear-gradient(top, rgba(35, 111, 135, 1) 0%, rgba(149, 207, 245, 1) 100%);
|
||||
background: linear-gradient(to bottom, rgba(35, 111, 135, 1) 0%, rgba(149, 207, 245, 1) 100%);
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#236f87', endColorstr='#95cff5', GradientType=0);
|
||||
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
#mine-wrapper .game-wrapper .users .user-container.user-red {
|
||||
background: rgb(173, 10, 5);
|
||||
background: -moz-linear-gradient(top, rgba(173, 10, 5, 1) 0%, rgba(246, 125, 82, 1) 100%);
|
||||
background: -webkit-linear-gradient(top, rgba(173, 10, 5, 1) 0%, rgba(246, 125, 82, 1) 100%);
|
||||
background: linear-gradient(to bottom, rgba(173, 10, 5, 1) 0%, rgba(246, 125, 82, 1) 100%);
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ad0a05', endColorstr='#f67d52', GradientType=0);
|
||||
}
|
||||
|
||||
#mine-wrapper .game-wrapper .users .user-container .user-header {
|
||||
background: -moz-linear-gradient(top, rgba(255, 255, 255, 0.7) 39%, rgba(255, 255, 255, 0.21) 87%, rgba(0, 0, 0, 0) 100%);
|
||||
background: -webkit-linear-gradient(top, rgba(255, 255, 255, 0.7) 39%, rgba(255, 255, 255, 0.21) 87%, rgba(0, 0, 0, 0) 100%);
|
||||
background: linear-gradient(to bottom, rgba(255, 255, 255, 0.7) 39%, rgba(255, 255, 255, 0.21) 87%, rgba(0, 0, 0, 0) 100%);
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffff', endColorstr='#00000000', GradientType=0);
|
||||
position: relative;
|
||||
font: bolder 25px 'Changa One', cursive;
|
||||
letter-spacing: 5px;
|
||||
text-transform: uppercase;
|
||||
text-align: center;
|
||||
padding: 6px 5px 20px 5px;
|
||||
margin-bottom: 40px;
|
||||
|
||||
-webkit-border-radius: 5px;
|
||||
border-radius: 5px;
|
||||
-webkit-text-shadow: 1px 1px 0 #FFF;
|
||||
text-shadow: 1px 1px 0 #FFF;
|
||||
}
|
||||
|
||||
#mine-wrapper .game-wrapper .users .user-container.user-blue .user-header {
|
||||
color: #236f87;
|
||||
}
|
||||
|
||||
#mine-wrapper .game-wrapper .users .user-container.user-red .user-header {
|
||||
color: #AD0A05;
|
||||
}
|
||||
|
||||
#mine-wrapper .game-wrapper .users .user-container .user-header > img {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
bottom: 0;
|
||||
width: 40%;
|
||||
margin-left: -20%;
|
||||
margin-bottom: -25%;
|
||||
}
|
||||
|
||||
#mine-wrapper .game-wrapper .users .user-container .user-header > img.user-cursor {
|
||||
display: block;
|
||||
width: 30%;
|
||||
top: 20px;
|
||||
left: 10px;
|
||||
margin-left: 0;
|
||||
|
||||
-webkit-animation: cursorJumping 1.2s cubic-bezier(.36, .07, .19, .97) infinite;
|
||||
animation: cursorJumping 1.2s cubic-bezier(.36, .07, .19, .97) infinite;
|
||||
}
|
||||
|
||||
#mine-wrapper .game-wrapper .users .user-container .user-header > img.user-cursor::after {
|
||||
content: '';
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
background: #1A6844;
|
||||
animation: animate .5s linear infinite;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
@keyframes cursorJumping {
|
||||
0% { top: 15px; }
|
||||
50% { top: 25px; }
|
||||
100% { top: 15px; }
|
||||
}
|
||||
|
||||
#mine-wrapper .game-wrapper .users .user-container .user-name {
|
||||
min-height: 30px;
|
||||
font-weight: normal;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
padding: 3px 5px;
|
||||
margin: 0;
|
||||
|
||||
overflow: hidden;
|
||||
word-break: break-word;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
#mine-wrapper .game-wrapper .users .user-container.user-blue .user-name {
|
||||
border-top: 1px dashed #0b3776;
|
||||
border-bottom: 1px dashed #0b3776;
|
||||
color: #0b3776;
|
||||
}
|
||||
|
||||
#mine-wrapper .game-wrapper .users .user-container.user-red .user-name {
|
||||
color: #fdf612;
|
||||
border-top: 1px dashed #fdf612;
|
||||
border-bottom: 1px dashed #fdf612;
|
||||
}
|
||||
|
||||
#mine-wrapper .game-wrapper .users .user-container .user-caret {
|
||||
height: 30px;
|
||||
font-size: 30px;
|
||||
text-align: center;
|
||||
line-height: 15px;
|
||||
}
|
||||
|
||||
#mine-wrapper .game-wrapper .users .user-container.user-blue .user-caret > i {
|
||||
color: #0b3776;
|
||||
}
|
||||
|
||||
#mine-wrapper .game-wrapper .users .user-container.user-red .user-caret > i {
|
||||
color: #fdf612;
|
||||
}
|
||||
|
||||
#mine-wrapper .game-wrapper .users .user-container .user-desc {
|
||||
height: 65px;
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
word-break: break-word;
|
||||
padding: 0 5px;
|
||||
}
|
||||
|
||||
#mine-wrapper .game-wrapper .users .user-container.user-blue .user-desc {
|
||||
color: #0b3776;
|
||||
}
|
||||
|
||||
#mine-wrapper .game-wrapper .users .user-container.user-red .user-desc {
|
||||
color: #fdf612;
|
||||
}
|
||||
356
assets/css/mineseeker/_waiting-dialog.scss
Normal file
356
assets/css/mineseeker/_waiting-dialog.scss
Normal file
@@ -0,0 +1,356 @@
|
||||
/*!*
|
||||
* This file is part of the SplendidBear Websites' projects.
|
||||
*
|
||||
* Copyright (c) 2026 @ www.splendidbear.org
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
.opd-paper {
|
||||
background: #07090d !important;
|
||||
background-image: linear-gradient(rgba(35, 111, 135, 0.08) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(35, 111, 135, 0.08) 1px, transparent 1px) !important;
|
||||
background-size: 46px 46px !important;
|
||||
border: 1px solid rgba(35, 111, 135, 0.4) !important;
|
||||
border-radius: 12px !important;
|
||||
box-shadow: 0 0 80px rgba(35, 111, 135, 0.15),
|
||||
0 32px 80px rgba(0, 0, 0, 0.9) !important;
|
||||
width: 500px;
|
||||
max-width: 94vw !important;
|
||||
overflow: hidden !important;
|
||||
}
|
||||
|
||||
.opd {
|
||||
padding: 28px 28px 22px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.opd-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 22px;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.opd-header-text {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.opd-header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
padding-top: 2px;
|
||||
}
|
||||
|
||||
.opd-label {
|
||||
display: block;
|
||||
font: 700 11px 'Rajdhani', sans-serif;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 4px;
|
||||
color: rgba(149, 207, 245, 0.55);
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.opd-title {
|
||||
font: 800 28px 'Rajdhani', sans-serif;
|
||||
color: #fff;
|
||||
letter-spacing: 0.5px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 11px;
|
||||
|
||||
i {
|
||||
color: rgba(35, 111, 135, 0.9);
|
||||
font-size: 22px;
|
||||
}
|
||||
}
|
||||
|
||||
.opd-refresh,
|
||||
.opd-close {
|
||||
background: transparent;
|
||||
border: 1px solid rgba(35, 111, 135, 0.3);
|
||||
border-radius: 6px;
|
||||
color: rgba(149, 207, 245, 0.55);
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: all 200ms ease;
|
||||
flex-shrink: 0;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
border-color: rgba(149, 207, 245, 0.5);
|
||||
color: #fff;
|
||||
background: rgba(35, 111, 135, 0.15);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.4;
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
|
||||
.opd-refresh--spin i {
|
||||
animation: opd-spin 0.7s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes opd-spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.opd-search-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: rgba(35, 111, 135, 0.07);
|
||||
border: 1px solid rgba(35, 111, 135, 0.28);
|
||||
border-radius: 8px;
|
||||
padding: 0 14px;
|
||||
margin-bottom: 16px;
|
||||
transition: border-color 200ms ease, background 200ms ease;
|
||||
|
||||
&:focus-within {
|
||||
border-color: rgba(35, 111, 135, 0.65);
|
||||
background: rgba(35, 111, 135, 0.12);
|
||||
}
|
||||
}
|
||||
|
||||
.opd-search-icon {
|
||||
color: rgba(149, 207, 245, 0.38);
|
||||
font-size: 13px;
|
||||
flex-shrink: 0;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.opd-search {
|
||||
flex: 1;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
outline: 0;
|
||||
height: 44px;
|
||||
font: 400 14px 'Rajdhani', sans-serif;
|
||||
letter-spacing: 0.3px;
|
||||
color: #fff;
|
||||
|
||||
&::placeholder {
|
||||
color: rgba(149, 207, 245, 0.32);
|
||||
}
|
||||
}
|
||||
|
||||
.opd-search-clear {
|
||||
background: transparent;
|
||||
border: 0;
|
||||
color: rgba(149, 207, 245, 0.4);
|
||||
cursor: pointer;
|
||||
padding: 0 0 0 8px;
|
||||
font-size: 12px;
|
||||
transition: color 150ms ease;
|
||||
|
||||
&:hover {
|
||||
color: rgba(149, 207, 245, 0.8);
|
||||
}
|
||||
}
|
||||
|
||||
.opd-list {
|
||||
min-height: 110px;
|
||||
}
|
||||
|
||||
.opd-empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 30px 0 22px;
|
||||
gap: 12px;
|
||||
|
||||
i {
|
||||
font-size: 34px;
|
||||
color: rgba(35, 111, 135, 0.35);
|
||||
}
|
||||
|
||||
p {
|
||||
font: 400 14px 'Rajdhani', sans-serif;
|
||||
color: rgba(255, 255, 255, 0.38);
|
||||
letter-spacing: 0.4px;
|
||||
}
|
||||
}
|
||||
|
||||
.opd-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
padding: 11px 12px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid transparent;
|
||||
transition: all 180ms ease;
|
||||
margin-bottom: 5px;
|
||||
|
||||
&:hover {
|
||||
background: rgba(35, 111, 135, 0.1);
|
||||
border-color: rgba(35, 111, 135, 0.28);
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.opd-avatar {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, rgba(35, 111, 135, 0.55) 0%, rgba(35, 111, 135, 0.28) 100%);
|
||||
border: 1px solid rgba(35, 111, 135, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font: 700 13px 'Rajdhani', sans-serif;
|
||||
color: rgba(149, 207, 245, 0.9);
|
||||
letter-spacing: 1px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.opd-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.opd-name {
|
||||
font: 700 15px 'Rajdhani', sans-serif;
|
||||
color: #fff;
|
||||
letter-spacing: 0.3px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.opd-since {
|
||||
font: 400 12px 'Rajdhani', sans-serif;
|
||||
color: rgba(149, 207, 245, 0.48);
|
||||
|
||||
i {
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.opd-join {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
background: linear-gradient(to bottom, rgba(35, 111, 135, 0.75) 0%, rgba(26, 80, 104, 0.9) 100%);
|
||||
border: 1px solid rgba(35, 111, 135, 0.55);
|
||||
color: rgba(149, 207, 245, 0.9);
|
||||
font: 700 12px 'Rajdhani', sans-serif;
|
||||
letter-spacing: 1.5px;
|
||||
text-transform: uppercase;
|
||||
text-decoration: none;
|
||||
padding: 7px 16px;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
transition: all 200ms ease;
|
||||
flex-shrink: 0;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: linear-gradient(to bottom, rgba(45, 138, 168, 0.9) 0%, rgba(35, 111, 135, 0.95) 100%);
|
||||
border-color: rgba(149, 207, 245, 0.5);
|
||||
color: #fff;
|
||||
box-shadow: 0 0 14px rgba(35, 111, 135, 0.5);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
cursor: default;
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
&.opd-join--waiting {
|
||||
background: linear-gradient(to bottom, rgba(26, 80, 104, 0.6) 0%, rgba(15, 50, 70, 0.7) 100%);
|
||||
border-color: rgba(35, 111, 135, 0.3);
|
||||
color: rgba(149, 207, 245, 0.6);
|
||||
opacity: 1;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
.opd-declined {
|
||||
font: 600 12px 'Rajdhani', sans-serif;
|
||||
color: rgba(255, 120, 120, 0.85);
|
||||
letter-spacing: 0.5px;
|
||||
padding: 8px 12px;
|
||||
margin-bottom: 8px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid rgba(180, 60, 60, 0.3);
|
||||
background: rgba(180, 60, 60, 0.08);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
|
||||
i {
|
||||
font-size: 14px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.opd-note {
|
||||
font: 400 11px 'Rajdhani', sans-serif;
|
||||
color: rgba(149, 207, 245, 0.32);
|
||||
text-align: center;
|
||||
letter-spacing: 0.5px;
|
||||
margin-top: 14px;
|
||||
padding-top: 14px;
|
||||
border-top: 1px solid rgba(35, 111, 135, 0.14);
|
||||
}
|
||||
|
||||
.opd-header-actions {
|
||||
.opd-refresh[disabled] {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.opd-close[disabled] {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.opd-waiting {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 14px 16px;
|
||||
margin-bottom: 16px;
|
||||
background: rgba(35, 111, 135, 0.07);
|
||||
border: 1px solid rgba(35, 111, 135, 0.28);
|
||||
border-radius: 8px;
|
||||
color: #95cff5;
|
||||
|
||||
i {
|
||||
font-size: 16px;
|
||||
animation: opd-hourglass 1s ease-in-out infinite;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
font: 600 14px 'Rajdhani', sans-serif;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes opd-hourglass {
|
||||
0%, 100% { transform: rotate(0deg); }
|
||||
50% { transform: rotate(180deg); }
|
||||
}
|
||||
301
assets/css/passkey.scss
Normal file
301
assets/css/passkey.scss
Normal file
@@ -0,0 +1,301 @@
|
||||
/*!*
|
||||
* This file is part of the SplendidBear Websites' projects.
|
||||
*
|
||||
* Copyright (c) 2026 @ www.splendidbear.org
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
@use "sass:color";
|
||||
|
||||
.twofa-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 14px;
|
||||
border-radius: 6px;
|
||||
font: 600 13px 'Rajdhani', sans-serif;
|
||||
letter-spacing: 0.5px;
|
||||
|
||||
&--enabled {
|
||||
background: rgba(42, 158, 96, 0.12);
|
||||
border: 1px solid rgba(42, 158, 96, 0.3);
|
||||
color: #5ee89a;
|
||||
}
|
||||
|
||||
&--disabled {
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
}
|
||||
|
||||
.twofa-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
flex-wrap: wrap;
|
||||
|
||||
&__form {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.twofa-backup-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
|
||||
&__count {
|
||||
font: 500 13px 'Rajdhani', sans-serif;
|
||||
color: rgba(149, 207, 245, 0.55);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.twofa-backup-reveal {
|
||||
background: rgba(246, 125, 82, 0.07);
|
||||
border: 1px solid rgba(246, 125, 82, 0.25);
|
||||
border-radius: 8px;
|
||||
padding: 18px 20px;
|
||||
|
||||
&__warning {
|
||||
font: 600 12px 'Rajdhani', sans-serif;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1.5px;
|
||||
color: #f6a060;
|
||||
margin: 0 0 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
}
|
||||
|
||||
&__grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.twofa-backup-code {
|
||||
display: block;
|
||||
text-align: center;
|
||||
padding: 7px 10px;
|
||||
background: rgba(0, 0, 0, 0.35);
|
||||
border: 1px solid rgba(246, 125, 82, 0.2);
|
||||
border-radius: 4px;
|
||||
font: 700 13px 'Courier New', monospace;
|
||||
letter-spacing: 2px;
|
||||
color: #e0c890;
|
||||
user-select: all;
|
||||
}
|
||||
|
||||
$primary: #236f87;
|
||||
$primary-dark: #1a5a70;
|
||||
$danger: #c0392b;
|
||||
$warning: #d68910;
|
||||
$success: #388e3c;
|
||||
$text: #e0e0e0;
|
||||
$text-muted: #9e9e9e;
|
||||
$border: rgba(35, 111, 135, 0.3);
|
||||
$bg-card: #0a0e14;
|
||||
$bg-hover: rgba(35, 111, 135, 0.15);
|
||||
|
||||
.passkey-manager {
|
||||
&__actions {
|
||||
margin: 20px 0;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
&__list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.passkey-item {
|
||||
border: 1px solid $border;
|
||||
border-radius: 6px;
|
||||
padding: 15px;
|
||||
background: $bg-card;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
background: $bg-hover;
|
||||
border-color: rgba(35, 111, 135, 0.5);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
&__info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
&__name {
|
||||
margin: 0 0 5px 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: $text;
|
||||
}
|
||||
|
||||
&__meta {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
color: $text-muted;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
&__badges {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
&__actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
justify-content: flex-end;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid $border;
|
||||
}
|
||||
}
|
||||
|
||||
.badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
|
||||
&--info {
|
||||
background: rgba(25, 118, 210, 0.2);
|
||||
color: #64b5f6;
|
||||
}
|
||||
|
||||
&--success {
|
||||
background: rgba(56, 142, 60, 0.2);
|
||||
color: #81c784;
|
||||
}
|
||||
}
|
||||
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 8px 16px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
text-decoration: none;
|
||||
|
||||
&--primary {
|
||||
background: $primary;
|
||||
color: white;
|
||||
|
||||
&:hover {
|
||||
background: $primary-dark;
|
||||
}
|
||||
}
|
||||
|
||||
&--secondary {
|
||||
background: #546e7a;
|
||||
color: white;
|
||||
|
||||
&:hover {
|
||||
background: #455a64;
|
||||
}
|
||||
}
|
||||
|
||||
&--warning {
|
||||
background: $warning;
|
||||
color: white;
|
||||
|
||||
&:hover {
|
||||
background: color.adjust($warning, $lightness: -10%);
|
||||
}
|
||||
}
|
||||
|
||||
&--danger {
|
||||
background: $danger;
|
||||
color: white;
|
||||
|
||||
&:hover {
|
||||
background: color.adjust($danger, $lightness: -10%);
|
||||
}
|
||||
}
|
||||
|
||||
&--sm {
|
||||
padding: 6px 12px;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 40px 20px;
|
||||
border: 1px dashed $border;
|
||||
border-radius: 8px;
|
||||
|
||||
&__icon {
|
||||
font-size: 48px;
|
||||
color: #455a64;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
&__text {
|
||||
font-size: 16px;
|
||||
color: $text;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
&__subtext {
|
||||
font-size: 13px;
|
||||
color: $text-muted;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.registration-status {
|
||||
padding: 15px;
|
||||
border-radius: 4px;
|
||||
margin-top: 15px;
|
||||
font-size: 14px;
|
||||
|
||||
&--success {
|
||||
background: rgba(56, 142, 60, 0.15);
|
||||
color: #81c784;
|
||||
border: 1px solid rgba(56, 142, 60, 0.3);
|
||||
}
|
||||
|
||||
&--error {
|
||||
background: rgba(192, 57, 43, 0.15);
|
||||
color: #e57373;
|
||||
border: 1px solid rgba(192, 57, 43, 0.3);
|
||||
}
|
||||
|
||||
&--loading {
|
||||
background: rgba(25, 118, 210, 0.15);
|
||||
color: #64b5f6;
|
||||
border: 1px solid rgba(25, 118, 210, 0.3);
|
||||
}
|
||||
}
|
||||
25
assets/css/style.homepage.scss
Normal file
25
assets/css/style.homepage.scss
Normal file
@@ -0,0 +1,25 @@
|
||||
/*!*
|
||||
* This file is part of the SplendidBear Websites' projects.
|
||||
*
|
||||
* Copyright (c) 2019 @ www.splendidbear.org
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
@use 'homepage/reset';
|
||||
@use 'homepage/animations';
|
||||
@use 'homepage/header';
|
||||
@use 'homepage/hero';
|
||||
@use 'homepage/hero-compact';
|
||||
@use 'homepage/cta';
|
||||
@use 'homepage/donate';
|
||||
@use 'homepage/auth-bar';
|
||||
@use 'homepage/auth';
|
||||
@use 'homepage/content';
|
||||
@use 'homepage/features';
|
||||
@use 'homepage/tech';
|
||||
@use 'homepage/footer';
|
||||
@use 'homepage/profile';
|
||||
@use 'homepage/battle-dialog';
|
||||
@use 'homepage/responsive';
|
||||
403
assets/css/style.layout.scss
Normal file
403
assets/css/style.layout.scss
Normal file
@@ -0,0 +1,403 @@
|
||||
/*!*
|
||||
* This file is part of the SplendidBear Websites' projects.
|
||||
*
|
||||
* Copyright (c) 2019 @ www.splendidbear.org
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
@import 'fonts-config';
|
||||
@import 'fontawesome-config';
|
||||
@import "style";
|
||||
@import "style.homepage";
|
||||
|
||||
::-webkit-input-placeholder {
|
||||
color: #888982;
|
||||
}
|
||||
|
||||
::-moz-placeholder {
|
||||
color: #888982;
|
||||
}
|
||||
|
||||
:-ms-input-placeholder {
|
||||
color: #888982;
|
||||
}
|
||||
|
||||
:-moz-placeholder {
|
||||
color: #888982;
|
||||
}
|
||||
|
||||
* {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
*,
|
||||
*:after,
|
||||
*::before {
|
||||
-webkit-box-sizing: border-box;
|
||||
-moz-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.ac-custom {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
header section .form-check {
|
||||
display: table;
|
||||
position: relative;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
header section h1 {
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
header section .input-submit button,
|
||||
header section .input-submit button:hover,
|
||||
header section .form-input,
|
||||
header section .form-input:focus,
|
||||
header section .form-input:hover {
|
||||
-webkit-transition: all 250ms ease-in-out;
|
||||
-moz-transition: all 250ms ease-in-out;
|
||||
-o-transition: all 250ms ease-in-out;
|
||||
transition: all 250ms ease-in-out;
|
||||
}
|
||||
|
||||
header section .input-submit button {
|
||||
background: #83aed9;
|
||||
display: table;
|
||||
font: bold 32px 'Rajdhani', sans-serif;
|
||||
text-transform: uppercase;
|
||||
text-decoration: none;
|
||||
width: 500px;
|
||||
border: 1px solid #658fb8;
|
||||
color: #FFFFFF;
|
||||
padding: 25px 150px;
|
||||
margin-top: 20px;
|
||||
|
||||
-webkit-box-shadow: 0 3px 10px rgba(0, 0, 0, 0.3);
|
||||
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.3);
|
||||
-webkit-border-radius: 3px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
header section .input-submit button:hover {
|
||||
background: #86b5e1;
|
||||
|
||||
-webkit-box-shadow: 0 7px 15px rgba(0, 0, 0, 0.2);
|
||||
box-shadow: 0 7px 15px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
header section .form-input {
|
||||
display: block;
|
||||
width: 500px;
|
||||
font: bold 22px 'Rajdhani', sans-serif;
|
||||
border: 1px solid #dddddd;
|
||||
color: #000000;
|
||||
padding: 15px;
|
||||
margin-bottom: 10px;
|
||||
|
||||
-webkit-border-radius: 3px;
|
||||
border-radius: 3px;
|
||||
-webkit-box-shadow: 0 3px 5px rgba(0, 0, 0, 0.15);
|
||||
box-shadow: 0 3px 5px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
header section .form-input:focus,
|
||||
header section .form-input:hover {
|
||||
-webkit-box-shadow: 0 7px 10px rgba(0, 0, 0, 0.1);
|
||||
box-shadow: 0 7px 10px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
header section .failure {
|
||||
background: #f00;
|
||||
position: relative;
|
||||
border: 3px solid #fff;
|
||||
font: bold 16px 'Rajdhani', sans-serif;
|
||||
color: #FFFFFF;
|
||||
padding: 10px;
|
||||
margin: 30px 0 10px 0;
|
||||
|
||||
-webkit-border-radius: 5px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
header section .failure:after,
|
||||
header section .failure:before {
|
||||
content: " ";
|
||||
position: absolute;
|
||||
bottom: 100%;
|
||||
left: 50px;
|
||||
height: 0;
|
||||
width: 0;
|
||||
border: solid transparent;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
header section .failure:after {
|
||||
border-color: rgba(0, 0, 0, 0);
|
||||
border-bottom-color: #f00;
|
||||
border-width: 20px;
|
||||
margin-left: -20px;
|
||||
}
|
||||
|
||||
header section .failure:before {
|
||||
border-color: rgba(255, 255, 255, 0);
|
||||
border-bottom-color: #ffffff;
|
||||
border-width: 26px;
|
||||
margin-left: -26px;
|
||||
}
|
||||
|
||||
header section .failure ul {
|
||||
display: inline-block;
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0 10px 0 0;
|
||||
}
|
||||
|
||||
header section h3.or {
|
||||
font: bold 16px 'Rajdhani', sans-serif;
|
||||
text-transform: uppercase;
|
||||
color: #a1a1a1;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
header section #id_welcome {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-bottom: 115px;
|
||||
}
|
||||
|
||||
header section #id_welcome > div {
|
||||
padding-right: 20px;
|
||||
}
|
||||
|
||||
header section #id_welcome img {
|
||||
width: 100px;
|
||||
border: 5px solid #414040;
|
||||
|
||||
-webkit-border-radius: 50%;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
header section .buttons,
|
||||
header section form {
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
header section div.buttons > a.fb-login,
|
||||
header section div.buttons > a.slack-login {
|
||||
position: relative;
|
||||
display: block;
|
||||
width: 500px;
|
||||
height: 93px;
|
||||
padding: 25px 0 25px 150px;
|
||||
margin-bottom: 10px;
|
||||
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
|
||||
-webkit-border-radius: 3px;
|
||||
border-radius: 3px;
|
||||
-webkit-box-shadow: 0 3px 5px rgba(0, 0, 0, 0.15);
|
||||
box-shadow: 0 3px 5px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
header section div.buttons > a.fb-login:hover,
|
||||
header section div.buttons > a.slack-login:hover {
|
||||
text-decoration: none;
|
||||
|
||||
-webkit-box-shadow: 0 7px 10px rgba(0, 0, 0, 0.1);
|
||||
box-shadow: 0 7px 10px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
header section div.buttons > a.fb-login i,
|
||||
header section div.buttons > a.slack-login i {
|
||||
position: absolute;
|
||||
font-size: 130px;
|
||||
top: 0;
|
||||
left: 15px;
|
||||
}
|
||||
|
||||
header section div.buttons > a.fb-login {
|
||||
background: #5975b1;
|
||||
border: 1px solid #50649f;
|
||||
}
|
||||
|
||||
header section div.buttons > a.fb-login:hover {
|
||||
background: #42598c;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
header section div.buttons > a.slack-login {
|
||||
background: #FFFFFF;
|
||||
border: 1px solid #5c3a58;
|
||||
color: #5c3a58;
|
||||
}
|
||||
|
||||
header section div.buttons > a.slack-login:hover {
|
||||
background: #e6e6e6;
|
||||
color: #5c3a58;
|
||||
}
|
||||
|
||||
header section .failure-main {
|
||||
background: #f00;
|
||||
max-width: 500px;
|
||||
border: 3px solid #fff;
|
||||
font: bold 16px 'Rajdhani', sans-serif;
|
||||
color: #FFFFFF;
|
||||
padding: 10px;
|
||||
margin: 0 0 20px 0;
|
||||
|
||||
-webkit-border-radius: 5px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
main div.txt {
|
||||
width: 100%;
|
||||
max-width: 1000px;
|
||||
font-family: 'Rajdhani', sans-serif;
|
||||
color: #414040;
|
||||
margin: 50px auto 0 auto;
|
||||
}
|
||||
|
||||
main div.txt h2 {
|
||||
margin: 0 0 50px 0;
|
||||
}
|
||||
|
||||
main div.txt p {
|
||||
font: normal 16px 'Rajdhani', sans-serif;
|
||||
}
|
||||
|
||||
main div.txt li {
|
||||
font: normal 16px 'Rajdhani', sans-serif;
|
||||
padding-left: 10px;
|
||||
margin-left: 50px;
|
||||
}
|
||||
|
||||
main .technologies {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
main .technologies img {
|
||||
display: inline-block;
|
||||
width: 90%;
|
||||
max-width: 100px;
|
||||
margin: 20px;
|
||||
}
|
||||
|
||||
main .technologies h1 {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
footer {
|
||||
background: #414040;
|
||||
width: 100%;
|
||||
min-height: 50px;
|
||||
margin-top: 50px;
|
||||
}
|
||||
|
||||
footer nav {
|
||||
display: block;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
footer nav ul {
|
||||
display: inline-block;
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
footer nav ul li {
|
||||
display: inline-block;
|
||||
font: bold 16px 'Rajdhani', sans-serif;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
footer nav ul li:nth-child(even) {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
footer nav ul li a {
|
||||
text-align: center;
|
||||
line-height: 50px;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
footer nav ul li a:hover {
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1100px) {
|
||||
header section #id_welcome {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-top: 50px;
|
||||
}
|
||||
|
||||
header section .form-input,
|
||||
header section .form-check {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
header section .input-submit button {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
header section > div {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
header section div.buttons > a.fb-login,
|
||||
header section div.buttons > a.slack-login {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
main div.txt {
|
||||
padding: 0 20px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 550px) {
|
||||
header section #id_welcome {
|
||||
display: block;
|
||||
}
|
||||
|
||||
header section {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
header section .form-input {
|
||||
width: 100%;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
header section .form-check {
|
||||
margin: 20px auto;
|
||||
}
|
||||
|
||||
header section .input-submit button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
header section div.buttons > a.fb-login,
|
||||
header section div.buttons > a.slack-login {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
header section div.buttons > a.fb-login span,
|
||||
header section div.buttons > a.slack-login span {
|
||||
display: none;
|
||||
}
|
||||
|
||||
footer nav ul li {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
24
assets/css/style.mineseeker.scss
Normal file
24
assets/css/style.mineseeker.scss
Normal file
@@ -0,0 +1,24 @@
|
||||
/*!*
|
||||
* This file is part of the SplendidBear Websites' projects.
|
||||
*
|
||||
* Copyright (c) 2019 @ www.splendidbear.org
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
@import "style";
|
||||
@import 'fonts-config';
|
||||
@import 'fontawesome-config';
|
||||
|
||||
@import 'mineseeker/base';
|
||||
@import 'mineseeker/overlay';
|
||||
@import 'mineseeker/users';
|
||||
@import 'mineseeker/bomb';
|
||||
@import 'mineseeker/mine-counter';
|
||||
@import 'mineseeker/grid';
|
||||
@import 'mineseeker/back-button';
|
||||
@import 'mineseeker/timer';
|
||||
@import 'mineseeker/bonus-box';
|
||||
@import 'mineseeker/responsive';
|
||||
@import 'mineseeker/waiting-dialog';
|
||||
19
assets/css/style.scss
Normal file
19
assets/css/style.scss
Normal file
@@ -0,0 +1,19 @@
|
||||
/*!*
|
||||
* This file is part of the SplendidBear Websites' projects.
|
||||
*
|
||||
* Copyright (c) 2019 @ www.splendidbear.org
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
.mine-beta {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 150px;
|
||||
|
||||
z-index: 4;
|
||||
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
BIN
assets/fonts/Carlito-Bold.ttf
Normal file
BIN
assets/fonts/Carlito-Bold.ttf
Normal file
Binary file not shown.
170
assets/images/waiting-dialog-design.svg
Normal file
170
assets/images/waiting-dialog-design.svg
Normal file
@@ -0,0 +1,170 @@
|
||||
<!---
|
||||
- This file is part of the SplendidBear Websites' projects.
|
||||
-
|
||||
- Copyright (c) 2026 @ www.splendidbear.org
|
||||
-
|
||||
- For the full copyright and license information, please view the LICENSE
|
||||
- file that was distributed with this source code.
|
||||
-->
|
||||
|
||||
<svg viewBox="0 0 800 500" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<style>
|
||||
.bg-dark { fill: #07090d; }
|
||||
.bg-grid { fill: url(#gridPattern); }
|
||||
.border-accent { stroke: rgba(35, 111, 135, 0.4); stroke-width: 2; fill: none; }
|
||||
.shadow { filter: drop-shadow(0 8px 20px rgba(0, 0, 0, 0.4)); }
|
||||
.title-text { font: bold 28px 'Rajdhani', sans-serif; fill: #fff; letter-spacing: 0.5px; }
|
||||
.header-text { font: bold 16px 'Rajdhani', sans-serif; fill: #236f87; letter-spacing: 0.5px; }
|
||||
.desc-text { font: 13px 'Rajdhani', sans-serif; fill: rgba(149, 207, 245, 0.7); letter-spacing: 0.3px; }
|
||||
.divider-line { stroke: rgba(35, 111, 135, 0.25); stroke-width: 1.5; }
|
||||
.divider-text { font: bold 12px 'Rajdhani', sans-serif; fill: rgba(35, 111, 135, 0.5); letter-spacing: 1px; text-transform: uppercase; }
|
||||
.icon-color { fill: rgba(35, 111, 135, 0.9); }
|
||||
.button-gradient { fill: url(#buttonGradient); }
|
||||
.button-text { font: bold 13px 'Rajdhani', sans-serif; fill: #e0f4ff; letter-spacing: 1.5px; text-transform: uppercase; }
|
||||
.glow { filter: drop-shadow(0 0 12px rgba(35, 111, 135, 0.3)); }
|
||||
</style>
|
||||
|
||||
<!-- Grid Pattern Background -->
|
||||
<pattern id="gridPattern" x="0" y="0" width="46" height="46" patternUnits="userSpaceOnUse">
|
||||
<path d="M 46 0 L 0 0 0 46" fill="none" stroke="rgba(35, 111, 135, 0.08)" stroke-width="1"/>
|
||||
</pattern>
|
||||
|
||||
<!-- Button Gradient -->
|
||||
<linearGradient id="buttonGradient" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#236f87;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#1a5068;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
|
||||
<!-- Icon Definitions -->
|
||||
<g id="icon-link">
|
||||
<path d="M 12 8 L 20 8 Q 22 8 22 10 L 22 16 Q 22 18 20 18 L 16 18 M 28 12 L 32 12 Q 34 12 34 14 L 34 20 Q 34 22 32 22 L 28 22 M 22 12 L 28 12"
|
||||
stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
|
||||
<g id="icon-users">
|
||||
<circle cx="16" cy="10" r="3.5" fill="currentColor"/>
|
||||
<path d="M 12 15 Q 12 13 16 13 Q 20 13 20 15 L 20 18 L 12 18 Z" fill="currentColor"/>
|
||||
<circle cx="30" cy="10" r="3.5" fill="currentColor"/>
|
||||
<path d="M 26 15 Q 26 13 30 13 Q 34 13 34 15 L 34 18 L 26 18 Z" fill="currentColor"/>
|
||||
</g>
|
||||
|
||||
<g id="icon-search">
|
||||
<circle cx="18" cy="18" r="8" stroke="currentColor" stroke-width="2" fill="none"/>
|
||||
<line x1="26" y1="26" x2="32" y2="32" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||
</g>
|
||||
|
||||
<g id="icon-clipboard">
|
||||
<rect x="12" y="8" width="16" height="24" rx="2" stroke="currentColor" stroke-width="2" fill="none"/>
|
||||
<rect x="16" y="6" width="8" height="3" fill="currentColor"/>
|
||||
<line x1="16" y1="14" x2="28" y2="14" stroke="currentColor" stroke-width="1.5"/>
|
||||
<line x1="16" y1="18" x2="28" y2="18" stroke="currentColor" stroke-width="1.5"/>
|
||||
<line x1="16" y1="22" x2="24" y2="22" stroke="currentColor" stroke-width="1.5"/>
|
||||
</g>
|
||||
</defs>
|
||||
|
||||
<!-- Main Background -->
|
||||
<rect class="bg-dark" width="800" height="500"/>
|
||||
<rect class="bg-grid" width="800" height="500"/>
|
||||
|
||||
<!-- Dialog Container -->
|
||||
<g class="shadow">
|
||||
<rect class="bg-dark" x="60" y="40" width="680" height="420" rx="12" class="shadow"/>
|
||||
<rect class="border-accent" x="60" y="40" width="680" height="420" rx="12"/>
|
||||
</g>
|
||||
|
||||
<!-- Dialog Header -->
|
||||
<g>
|
||||
<text x="100" y="85" class="title-text">We are waiting...</text>
|
||||
<g transform="translate(720, 60)">
|
||||
<!-- Close Button -->
|
||||
<circle cx="0" cy="0" r="16" stroke="rgba(35, 111, 135, 0.3)" stroke-width="1" fill="none"/>
|
||||
<line x1="-6" y1="-6" x2="6" y2="6" stroke="rgba(149, 207, 245, 0.55)" stroke-width="2" stroke-linecap="round"/>
|
||||
<line x1="6" y1="-6" x2="-6" y2="6" stroke="rgba(149, 207, 245, 0.55)" stroke-width="2" stroke-linecap="round"/>
|
||||
</g>
|
||||
</g>
|
||||
|
||||
<!-- Option 1: Invite a Friend -->
|
||||
<g>
|
||||
<!-- Option Container -->
|
||||
<rect x="80" y="120" width="280" height="280" rx="8" fill="rgba(35, 111, 135, 0.05)" stroke="rgba(35, 111, 135, 0.15)" stroke-width="1"/>
|
||||
|
||||
<!-- Icon -->
|
||||
<g transform="translate(110, 145)">
|
||||
<circle cx="0" cy="0" r="22" fill="rgba(35, 111, 135, 0.15)"/>
|
||||
<g use="#icon-link" class="icon-color" transform="scale(1.8)"/>
|
||||
</g>
|
||||
|
||||
<!-- Title -->
|
||||
<text x="160" y="165" class="header-text">Invite a Friend</text>
|
||||
|
||||
<!-- Description -->
|
||||
<text x="100" y="195" class="desc-text">Share this link with your opponent</text>
|
||||
|
||||
<!-- URL Box -->
|
||||
<rect x="100" y="210" width="240" height="40" rx="6" fill="#d0e8f5" stroke="#7ab8d8" stroke-width="1"/>
|
||||
<text x="118" y="237" style="font: 12px 'Courier New', monospace; fill: #1a4a6a; letter-spacing: 0.3px;">play.mineseeker.com/game-id</text>
|
||||
|
||||
<!-- Copy Button -->
|
||||
<g class="glow">
|
||||
<rect x="100" y="265" width="240" height="40" rx="5" class="button-gradient" stroke="#2e7a9a" stroke-width="1"/>
|
||||
<g transform="translate(120, 285)">
|
||||
<rect x="0" y="0" width="6" height="6" fill="#e0f4ff"/>
|
||||
<rect x="2" y="2" width="4" height="4" fill="none" stroke="#e0f4ff" stroke-width="0.5"/>
|
||||
</g>
|
||||
<text x="140" y="287" class="button-text">Copy Link</text>
|
||||
</g>
|
||||
</g>
|
||||
|
||||
<!-- Divider OR -->
|
||||
<g>
|
||||
<line x1="400" y1="200" x2="400" y2="320" class="divider-line"/>
|
||||
<circle cx="400" cy="260" r="18" fill="#07090d" stroke="rgba(35, 111, 135, 0.25)" stroke-width="1"/>
|
||||
<text x="400" y="267" class="divider-text" text-anchor="middle">OR</text>
|
||||
</g>
|
||||
|
||||
<!-- Option 2: Challenge a Player -->
|
||||
<g>
|
||||
<!-- Option Container -->
|
||||
<rect x="440" y="120" width="280" height="280" rx="8" fill="rgba(35, 111, 135, 0.05)" stroke="rgba(35, 111, 135, 0.15)" stroke-width="1"/>
|
||||
|
||||
<!-- Icon -->
|
||||
<g transform="translate(470, 145)">
|
||||
<circle cx="0" cy="0" r="22" fill="rgba(35, 111, 135, 0.15)"/>
|
||||
<g use="#icon-users" class="icon-color" transform="scale(1.5)"/>
|
||||
</g>
|
||||
|
||||
<!-- Title -->
|
||||
<text x="510" y="165" class="header-text">Challenge a Player</text>
|
||||
|
||||
<!-- Description -->
|
||||
<text x="460" y="195" class="desc-text">Browse online players and challenge</text>
|
||||
|
||||
<!-- Browse Button -->
|
||||
<g class="glow">
|
||||
<rect x="460" y="265" width="240" height="40" rx="5" class="button-gradient" stroke="#2e7a9a" stroke-width="1"/>
|
||||
<g transform="translate(480, 285)">
|
||||
<!-- Search icon simplified -->
|
||||
<circle cx="4" cy="4" r="3" fill="none" stroke="#e0f4ff" stroke-width="1"/>
|
||||
<line x1="7" y1="7" x2="9" y2="9" stroke="#e0f4ff" stroke-width="1" stroke-linecap="round"/>
|
||||
</g>
|
||||
<text x="510" y="287" class="button-text">Browse Players</text>
|
||||
</g>
|
||||
|
||||
<!-- Highlight: Players Online -->
|
||||
<g>
|
||||
<text x="460" y="230" class="desc-text" style="font-weight: bold;">5 players waiting</text>
|
||||
<g transform="translate(680, 220)">
|
||||
<circle r="6" fill="#236f87"/>
|
||||
<circle cx="-8" cy="3" r="4" fill="rgba(35, 111, 135, 0.6)"/>
|
||||
<circle cx="8" cy="3" r="4" fill="rgba(35, 111, 135, 0.6)"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
|
||||
<!-- Bottom Info -->
|
||||
<g>
|
||||
<text x="100" y="435" style="font: 11px 'Rajdhani', sans-serif; fill: rgba(149, 207, 245, 0.35); letter-spacing: 0.5px;">Choose either option to start playing or find an opponent</text>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
|
After Width: | Height: | Size: 7.5 KiB |
22
assets/js/app.jsx
Normal file
22
assets/js/app.jsx
Normal file
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* This file is part of the SplendidBear Websites' projects.
|
||||
*
|
||||
* Copyright (c) 2026 @ www.splendidbear.org
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import MineSeeker from './mine-seeker/MineSeeker';
|
||||
|
||||
const wrapper = document.getElementById('mine-wrapper');
|
||||
|
||||
createRoot(wrapper).render(
|
||||
<MineSeeker
|
||||
env={wrapper.dataset.env}
|
||||
gameId={wrapper.dataset.gameId}
|
||||
opponentName={wrapper.dataset.opponentName || ''}
|
||||
/>,
|
||||
);
|
||||
74
assets/js/components/AvatarUpload.jsx
Normal file
74
assets/js/components/AvatarUpload.jsx
Normal file
@@ -0,0 +1,74 @@
|
||||
/**
|
||||
* This file is part of the SplendidBear Websites' projects.
|
||||
*
|
||||
* Copyright (c) 2026 @ www.splendidbear.org
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
import React, { useMemo, useRef } from 'react';
|
||||
import { string } from 'prop-types';
|
||||
import { useProfileDataProvider } from '@mine-hooks/useGameDataProvider';
|
||||
|
||||
export const AvatarUpload = ({ uploadUrl, initialThumbUrl, initials }) => {
|
||||
const inputRef = useRef(null);
|
||||
const [thumbUrl, setThumbUrl] = React.useState(initialThumbUrl || null);
|
||||
const { uploadAvatarMutation: { isPending, error, mutate } } = useProfileDataProvider();
|
||||
|
||||
const handleChange = e => {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
mutate({ uploadUrl, file }, {
|
||||
onSuccess: data => {
|
||||
setThumbUrl(data.thumbUrl);
|
||||
|
||||
const navImg = document.querySelector('.hero-auth-avatar:not(.hero-auth-avatar--initials)');
|
||||
const navInitials = document.querySelector('.hero-auth-avatar.hero-auth-avatar--initials');
|
||||
|
||||
if (navImg) {
|
||||
navImg.src = data.thumbUrl;
|
||||
} else if (navInitials) {
|
||||
const img = document.createElement('img');
|
||||
img.src = data.thumbUrl;
|
||||
img.alt = navInitials.textContent.trim();
|
||||
img.className = 'hero-auth-avatar';
|
||||
navInitials.replaceWith(img);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const errorMessage = useMemo(() => error?.message ?? null, [error]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`profile-avatar${isPending ? ' profile-avatar--loading' : ''}`}
|
||||
title="Click to change profile picture"
|
||||
onClick={() => inputRef.current?.click()}
|
||||
>
|
||||
{thumbUrl
|
||||
? <img src={thumbUrl} alt={initials} className="profile-avatar__img" />
|
||||
: <span className="profile-avatar__initials">{initials}</span>
|
||||
}
|
||||
<div className="profile-avatar__overlay">
|
||||
<i className="fa fa-camera" />
|
||||
</div>
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="file"
|
||||
accept="image/jpeg,image/png,image/gif,image/webp"
|
||||
style={{ display: 'none' }}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
{errorMessage && <div className="profile-avatar__error">{errorMessage}</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
AvatarUpload.propTypes = {
|
||||
uploadUrl: string.isRequired,
|
||||
initialThumbUrl: string,
|
||||
initials: string.isRequired,
|
||||
};
|
||||
223
assets/js/components/BattleDialog.jsx
Normal file
223
assets/js/components/BattleDialog.jsx
Normal file
@@ -0,0 +1,223 @@
|
||||
/**
|
||||
* This file is part of the SplendidBear Websites' projects.
|
||||
*
|
||||
* Copyright (c) 2026 @ www.splendidbear.org
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { array } from 'prop-types';
|
||||
import { formatDuration } from '@global-utils/format';
|
||||
import Dialog from '@mui/material/Dialog';
|
||||
import { createTheme, styled, ThemeProvider } from '@mui/material/styles';
|
||||
import { Avatar } from './battle-dialog/Avatar';
|
||||
import { BonusPoints } from './battle-dialog/BonusPoints';
|
||||
import { StatRow } from './battle-dialog/StatRow';
|
||||
|
||||
const darkTheme = createTheme({ palette: { mode: 'dark' } });
|
||||
|
||||
const RESULT_META = {
|
||||
win: {
|
||||
label: 'Victory',
|
||||
color: '#5ee89a',
|
||||
bg: 'rgba(42,158,96,0.15)',
|
||||
border: 'rgba(42,158,96,0.4)',
|
||||
icon: 'fa-trophy',
|
||||
},
|
||||
loss: {
|
||||
label: 'Defeated',
|
||||
color: '#f67d52',
|
||||
bg: 'rgba(173,10,5,0.15)',
|
||||
border: 'rgba(173,10,5,0.4)',
|
||||
icon: 'fa-flag',
|
||||
},
|
||||
draw: {
|
||||
label: 'Draw',
|
||||
color: '#95cff5',
|
||||
bg: 'rgba(149,207,245,0.1)',
|
||||
border: 'rgba(149,207,245,0.3)',
|
||||
icon: 'fa-minus',
|
||||
},
|
||||
};
|
||||
|
||||
export const BattleDialog = ({ games }) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [game, setGame] = useState(null);
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const handler = e => {
|
||||
const row = e.target.closest('[data-game-index]');
|
||||
if (!row) return;
|
||||
const idx = parseInt(row.dataset.gameIndex, 10);
|
||||
if (!isNaN(idx) && games[idx]) {
|
||||
setGame(games[idx]);
|
||||
setOpen(true);
|
||||
}
|
||||
};
|
||||
document.addEventListener('click', handler);
|
||||
return () => document.removeEventListener('click', handler);
|
||||
}, [games]);
|
||||
|
||||
if (!game) {
|
||||
return <ThemeProvider theme={darkTheme}><StyledDialog open={false} /></ThemeProvider>;
|
||||
}
|
||||
|
||||
const meta = RESULT_META[game.result] ?? RESULT_META.draw;
|
||||
const resign = game.resign;
|
||||
const maxPoints = Math.max(game.redPoints ?? 0, game.bluePoints ?? 0);
|
||||
const endReason = resign
|
||||
? `${resign.charAt(0).toUpperCase() + resign.slice(1)} resigned`
|
||||
: 26 <= maxPoints ? 'Points' : 'Abandoned';
|
||||
const bothRegistered = game.bothRegistered;
|
||||
const shareUrl = `${window.location.origin}/battle/${game.uuid}`;
|
||||
const canContinue = bothRegistered && !resign && 26 > maxPoints;
|
||||
const canShare = !canContinue;
|
||||
const playUrl = `${window.location.origin}/play/${game.uuid}`;
|
||||
|
||||
const duration = formatDuration(game.created, game.date);
|
||||
const pointDiff = Math.abs((game.redPoints ?? 0) - (game.bluePoints ?? 0));
|
||||
const winnerColor = (game.redPoints ?? 0) > (game.bluePoints ?? 0) ? '#f67d52'
|
||||
: (game.bluePoints ?? 0) > (game.redPoints ?? 0) ? '#95cff5'
|
||||
: 'rgba(255,255,255,0.45)';
|
||||
|
||||
const handleShare = () => {
|
||||
navigator.clipboard.writeText(shareUrl).then(() => {
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2200);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<ThemeProvider theme={darkTheme}>
|
||||
<StyledDialog open={open} onClose={() => setOpen(false)}>
|
||||
<div className="bd">
|
||||
<div className="bd-header">
|
||||
<div className="bd-header-left">
|
||||
<span className="bd-label">Battle Report</span>
|
||||
<h2 className="bd-title">
|
||||
<i className="fa fa-crosshairs" /> Match Details
|
||||
</h2>
|
||||
</div>
|
||||
<div className="bd-header-actions">
|
||||
{canContinue ? (
|
||||
<a
|
||||
className="bd-continue"
|
||||
href={playUrl}
|
||||
aria-label="Continue the game"
|
||||
title="Continue the game"
|
||||
>
|
||||
<i className="fa fa-play" />
|
||||
Continue
|
||||
</a>
|
||||
) : canShare ? (
|
||||
<button
|
||||
className={`bd-share${copied ? ' bd-share--copied' : ''}`}
|
||||
onClick={handleShare}
|
||||
aria-label="Copy share link"
|
||||
title="Copy share link"
|
||||
>
|
||||
<i className={`fa ${copied ? 'fa-check' : 'fa-share-alt'}`} />
|
||||
{copied ? 'Copied!' : 'Share'}
|
||||
</button>
|
||||
) : null}
|
||||
<button className="bd-close" onClick={() => setOpen(false)} aria-label="Close">
|
||||
<i className="fa fa-times" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bd-vs-panel">
|
||||
<Avatar
|
||||
name={game.redName} color="red" avatarUrl={game.redAvatar}
|
||||
bonusPoints={game.redBonusPoints > game.blueBonusPoints ? game.redBonusPoints : 0}
|
||||
/>
|
||||
<div className="bd-vs-center">
|
||||
<div className="bd-vs-score">
|
||||
<span className="bd-vs-score__red">{game.redPoints ?? '—'}</span>
|
||||
<span className="bd-vs-score__sep">:</span>
|
||||
<span className="bd-vs-score__blue">{game.bluePoints ?? '—'}</span>
|
||||
</div>
|
||||
<div className="bd-vs-score bd-bonus-score">
|
||||
<span className="bd-bonus-score__red">
|
||||
<i className="fa fa-star" /> {(game.redBonusPoints ?? 0).toFixed(1)}
|
||||
</span>
|
||||
<span className="bd-vs-score__sep">:</span>
|
||||
<span className="bd-bonus-score__blue">
|
||||
{(game.blueBonusPoints ?? 0).toFixed(1)} <i className="fa fa-star" />
|
||||
</span>
|
||||
</div>
|
||||
<div className="bd-vs-label">VS</div>
|
||||
<div
|
||||
className="bd-result-badge"
|
||||
style={{ '--bd-result-bg': meta.bg, '--bd-result-border': meta.border, '--bd-result-color': meta.color }}
|
||||
>
|
||||
<i className={`fa ${meta.icon}`} /> {meta.label}
|
||||
</div>
|
||||
</div>
|
||||
<Avatar
|
||||
name={game.blueName} color="blue" avatarUrl={game.blueAvatar}
|
||||
bonusPoints={game.blueBonusPoints > game.redBonusPoints ? game.blueBonusPoints : 0}
|
||||
/>
|
||||
</div>
|
||||
<div className="bd-stats">
|
||||
<StatRow icon="fa-calendar" label="Date" value={game.date ?? '—'} />
|
||||
{game.created && game.date && game.created !== game.date && (
|
||||
<StatRow icon="fa-clock" label="Started" value={game.created} />
|
||||
)}
|
||||
{duration && (
|
||||
<StatRow icon="fa-hourglass-half" label="Match duration" value={duration} />
|
||||
)}
|
||||
<StatRow icon="fa-flag-checkered" label="End reason" value={endReason} />
|
||||
{0 < pointDiff && (
|
||||
<StatRow
|
||||
icon="fa-balance-scale" label="Winning margin"
|
||||
value={`${pointDiff} mine${1 === pointDiff ? '' : 's'}`} valueColor={winnerColor}
|
||||
/>
|
||||
)}
|
||||
<StatRow
|
||||
icon="fa-bomb" label="Red used bomb"
|
||||
value={game.redExplodedBomb ? 'Yes' : 'No'}
|
||||
valueColor={game.redExplodedBomb ? '#f67d52' : 'rgba(255,255,255,0.45)'}
|
||||
/>
|
||||
<StatRow
|
||||
icon="fa-bomb" label="Blue used bomb"
|
||||
value={game.blueExplodedBomb ? 'Yes' : 'No'}
|
||||
valueColor={game.blueExplodedBomb ? '#95cff5' : 'rgba(255,255,255,0.45)'}
|
||||
/>
|
||||
</div>
|
||||
<BonusPoints
|
||||
game={game}
|
||||
/>
|
||||
</div>
|
||||
</StyledDialog>
|
||||
</ThemeProvider>
|
||||
);
|
||||
};
|
||||
|
||||
BattleDialog.propTypes = {
|
||||
games: array.isRequired,
|
||||
};
|
||||
|
||||
const StyledDialog = styled(Dialog)({
|
||||
'& .MuiDialog-paper': {
|
||||
background: '#07090d',
|
||||
backgroundImage: `
|
||||
linear-gradient(rgba(35, 111, 135, 0.08) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(35, 111, 135, 0.08) 1px, transparent 1px)
|
||||
`,
|
||||
backgroundSize: '46px 46px',
|
||||
border: '1px solid rgba(35, 111, 135, 0.4)',
|
||||
borderRadius: '12px',
|
||||
boxShadow: '0 0 80px rgba(35, 111, 135, 0.15), 0 32px 80px rgba(0,0,0,0.9)',
|
||||
width: '580px',
|
||||
maxWidth: '94vw',
|
||||
overflow: 'hidden',
|
||||
color: '#fff',
|
||||
},
|
||||
'& .MuiBackdrop-root': {
|
||||
background: 'rgba(2, 4, 8, 0.88)',
|
||||
backdropFilter: 'blur(4px)',
|
||||
},
|
||||
});
|
||||
89
assets/js/components/ContactForm.jsx
Normal file
89
assets/js/components/ContactForm.jsx
Normal file
@@ -0,0 +1,89 @@
|
||||
/**
|
||||
* This file is part of the SplendidBear Websites' projects.
|
||||
*
|
||||
* Copyright (c) 2026 @ www.splendidbear.org
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { string } from 'prop-types';
|
||||
|
||||
/**
|
||||
* ContactForm Component
|
||||
*
|
||||
* Handles reCAPTCHA v3 integration for the contact form.
|
||||
* Intercepts form submission, executes reCAPTCHA, and submits the form with the token.
|
||||
*
|
||||
* @param {string} siteKey - Google reCAPTCHA site key
|
||||
* @param {string} recaptchaFieldId - ID of the hidden recaptcha input field
|
||||
*/
|
||||
const ContactForm = ({ siteKey, recaptchaFieldId }) => {
|
||||
const formRef = useRef(null);
|
||||
const isSubmittingRef = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
const form = document.querySelector('.auth-form');
|
||||
|
||||
if (!form) {
|
||||
console.warn('ContactForm: No .auth-form found');
|
||||
return;
|
||||
}
|
||||
|
||||
formRef.current = form;
|
||||
|
||||
const handleSubmit = e => {
|
||||
e.preventDefault();
|
||||
|
||||
if (isSubmittingRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
isSubmittingRef.current = true;
|
||||
|
||||
if ('undefined' !== typeof grecaptcha) {
|
||||
grecaptcha.ready(() => {
|
||||
grecaptcha
|
||||
.execute(siteKey, { action: 'contact' })
|
||||
.then(token => {
|
||||
const recaptchaField = document.getElementById(recaptchaFieldId);
|
||||
|
||||
if (recaptchaField) {
|
||||
recaptchaField.value = token;
|
||||
} else {
|
||||
console.error(`ContactForm: Recaptcha field with ID "${recaptchaFieldId}" not found`);
|
||||
}
|
||||
|
||||
isSubmittingRef.current = false;
|
||||
form.submit();
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('ContactForm: reCAPTCHA execution failed', error);
|
||||
isSubmittingRef.current = false;
|
||||
});
|
||||
});
|
||||
} else {
|
||||
console.error('ContactForm: grecaptcha is not loaded');
|
||||
isSubmittingRef.current = false;
|
||||
}
|
||||
};
|
||||
|
||||
form.addEventListener('submit', handleSubmit);
|
||||
|
||||
return () => {
|
||||
if (formRef.current) {
|
||||
formRef.current.removeEventListener('submit', handleSubmit);
|
||||
}
|
||||
};
|
||||
}, [siteKey, recaptchaFieldId]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
ContactForm.propTypes = {
|
||||
siteKey: string.isRequired,
|
||||
recaptchaFieldId: string.isRequired,
|
||||
};
|
||||
|
||||
export default ContactForm;
|
||||
118
assets/js/components/PasskeyLogin.jsx
Normal file
118
assets/js/components/PasskeyLogin.jsx
Normal file
@@ -0,0 +1,118 @@
|
||||
/**
|
||||
* This file is part of the SplendidBear Websites' projects.
|
||||
*
|
||||
* Copyright (c) 2026 @ www.splendidbear.org
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import { shape, string } from 'prop-types';
|
||||
|
||||
const base64ToArrayBuffer = base64 => {
|
||||
const binary = atob(base64.replace(/([-_])/g, m => ('-' === m ? '+' : '/')));
|
||||
const bytes = new Uint8Array(binary.length);
|
||||
for (let i = 0; i < binary.length; i++) {
|
||||
bytes[i] = binary.charCodeAt(i);
|
||||
}
|
||||
return bytes.buffer;
|
||||
};
|
||||
|
||||
const arrayBufferToBase64url = buffer =>
|
||||
btoa(String.fromCharCode(...new Uint8Array(buffer)))
|
||||
.replace(/([+\/=])/g, m => ('+' === m ? '-' : '/' === m ? '_' : ''));
|
||||
|
||||
const credentialToJSON = credential => ({
|
||||
id: credential.id,
|
||||
rawId: arrayBufferToBase64url(credential.rawId),
|
||||
type: credential.type,
|
||||
response: {
|
||||
authenticatorData: arrayBufferToBase64url(credential.response.authenticatorData),
|
||||
clientDataJSON: arrayBufferToBase64url(credential.response.clientDataJSON),
|
||||
signature: arrayBufferToBase64url(credential.response.signature),
|
||||
userHandle: credential.response.userHandle
|
||||
? arrayBufferToBase64url(credential.response.userHandle)
|
||||
: null,
|
||||
},
|
||||
});
|
||||
|
||||
const PasskeyLogin = ({ apiRoutes }) => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
const handleLogin = useCallback(async () => {
|
||||
setLoading(true);
|
||||
setError('');
|
||||
|
||||
try {
|
||||
const beginResponse = await fetch(apiRoutes.authenticationBegin, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
|
||||
if (!beginResponse.ok) throw new Error('Failed to get authentication options');
|
||||
const options = await beginResponse.json();
|
||||
|
||||
const publicKey = {
|
||||
...options,
|
||||
challenge: base64ToArrayBuffer(options.challenge),
|
||||
allowCredentials: (options.allowCredentials ?? []).map(cred => ({
|
||||
...cred,
|
||||
id: base64ToArrayBuffer(cred.id),
|
||||
})),
|
||||
};
|
||||
|
||||
const credential = await navigator.credentials.get({ publicKey });
|
||||
if (!credential) throw new Error('Authentication was cancelled');
|
||||
|
||||
const completeResponse = await fetch(apiRoutes.authenticationComplete, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ credential: credentialToJSON(credential) }),
|
||||
});
|
||||
|
||||
if (!completeResponse.ok) {
|
||||
const body = await completeResponse.json();
|
||||
throw new Error(body.error || 'Authentication failed');
|
||||
}
|
||||
|
||||
const result = await completeResponse.json();
|
||||
if (result.success) {
|
||||
window.location.href = result.redirect || '/';
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err.message);
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
}, [apiRoutes]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
className="auth-passkey-btn"
|
||||
onClick={handleLogin}
|
||||
disabled={loading}
|
||||
>
|
||||
<i className={loading ? 'fa fa-spinner fa-spin' : 'fa fa-key'} />
|
||||
{loading ? 'Waiting for passkey…' : 'Sign In with Passkey'}
|
||||
</button>
|
||||
{error && (
|
||||
<p className="auth-error" style={{ marginTop: '10px' }}>
|
||||
<i className="fa fa-exclamation-triangle" /> {error}
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default PasskeyLogin;
|
||||
|
||||
PasskeyLogin.propTypes = {
|
||||
apiRoutes: shape({
|
||||
authenticationBegin: string.isRequired,
|
||||
authenticationComplete: string.isRequired,
|
||||
}).isRequired,
|
||||
};
|
||||
428
assets/js/components/PasskeyManager.jsx
Normal file
428
assets/js/components/PasskeyManager.jsx
Normal file
@@ -0,0 +1,428 @@
|
||||
/**
|
||||
* This file is part of the SplendidBear Websites' projects.
|
||||
*
|
||||
* Copyright (c) 2026 @ www.splendidbear.org
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
import React, { Fragment, useCallback, useEffect, useState } from 'react';
|
||||
import Dialog from '@mui/material/Dialog';
|
||||
import { styled } from '@mui/material/styles';
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
import DialogContent from '@mui/material/DialogContent';
|
||||
import DialogActions from '@mui/material/DialogActions';
|
||||
import Button from '@mui/material/Button';
|
||||
import TextField from '@mui/material/TextField';
|
||||
import { arrayOf, shape, string, bool } from 'prop-types';
|
||||
|
||||
const StyledDialog = styled(Dialog)({
|
||||
'& .MuiDialog-paper': {
|
||||
background: '#0a0e14',
|
||||
color: '#e0e0e0',
|
||||
},
|
||||
'& .MuiTextField-root .MuiInputLabel-root': {
|
||||
color: '#9e9e9e',
|
||||
},
|
||||
'& .MuiTextField-root .MuiOutlinedInput-root': {
|
||||
'& fieldset': {
|
||||
borderColor: 'rgba(35, 111, 135, 0.5)',
|
||||
},
|
||||
'&:hover fieldset': {
|
||||
borderColor: 'rgba(35, 111, 135, 0.8)',
|
||||
},
|
||||
'&.Mui-focused fieldset': {
|
||||
borderColor: '#236f87',
|
||||
},
|
||||
},
|
||||
'& .MuiTextField-root .MuiOutlinedInput-input': {
|
||||
color: '#e0e0e0',
|
||||
},
|
||||
'& .MuiFormHelperText-root': {
|
||||
width: '500px',
|
||||
maxWidth: '94vw',
|
||||
overflow: 'hidden',
|
||||
color: '#e0e0e0',
|
||||
},
|
||||
'& .MuiBackdrop-root': {
|
||||
background: 'rgba(2, 4, 8, 0.88)',
|
||||
backdropFilter: 'blur(4px)',
|
||||
},
|
||||
});
|
||||
|
||||
const base64ToArrayBuffer = base64 => {
|
||||
const binary = atob(base64.replace(/([-_])/g, m => ('-' === m ? '+' : '/')));
|
||||
const bytes = new Uint8Array(binary.length);
|
||||
for (let i = 0; i < binary.length; i++) {
|
||||
bytes[i] = binary.charCodeAt(i);
|
||||
}
|
||||
return bytes.buffer;
|
||||
};
|
||||
|
||||
const credentialToJSON = credential => {
|
||||
const attestationObject = credential.response.attestationObject;
|
||||
const clientDataJSON = credential.response.clientDataJSON;
|
||||
|
||||
return {
|
||||
id: credential.id,
|
||||
rawId: btoa(String.fromCharCode(...new Uint8Array(credential.rawId))).replace(/([+\/=])/g, m => ('+' === m ? '-' : '/' === m ? '_' : '')),
|
||||
type: credential.type,
|
||||
response: {
|
||||
attestationObject: btoa(String.fromCharCode(...new Uint8Array(attestationObject))).replace(/([+\/=])/g, m => ('+' === m ? '-' : '/' === m ? '_' : '')),
|
||||
clientDataJSON: btoa(String.fromCharCode(...new Uint8Array(clientDataJSON))).replace(/([+\/=])/g, m => ('+' === m ? '-' : '/' === m ? '_' : '')),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const PasskeyManager = ({ credentials, apiRoutes }) => {
|
||||
const [addModalOpen, setAddModalOpen] = useState(false);
|
||||
const [renameModalOpen, setRenameModalOpen] = useState(false);
|
||||
const [deleteModalOpen, setDeleteModalOpen] = useState(false);
|
||||
const [selectedCredential, setSelectedCredential] = useState(null);
|
||||
const [passkeyName, setPasskeyName] = useState('');
|
||||
const [renameName, setRenameName] = useState('');
|
||||
const [status, setStatus] = useState({ type: '', message: '' });
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [credentialsList, setCredentialsList] = useState(credentials);
|
||||
|
||||
useEffect(() => {
|
||||
setCredentialsList(credentials);
|
||||
}, [credentials]);
|
||||
|
||||
const showStatus = useCallback((message, type) => {
|
||||
setStatus({ message, type });
|
||||
}, []);
|
||||
|
||||
const closeAddModal = useCallback(() => {
|
||||
setAddModalOpen(false);
|
||||
setPasskeyName('');
|
||||
setStatus({ type: '', message: '' });
|
||||
}, []);
|
||||
|
||||
const openAddModal = useCallback(() => {
|
||||
setPasskeyName('');
|
||||
setStatus({ type: '', message: '' });
|
||||
setAddModalOpen(true);
|
||||
}, []);
|
||||
|
||||
const handleAddPasskey = useCallback(async () => {
|
||||
if (!passkeyName.trim()) {
|
||||
showStatus('Please enter a passkey name', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
showStatus('Starting registration...', 'loading');
|
||||
|
||||
try {
|
||||
const optionsResponse = await fetch(apiRoutes.registrationBegin, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ credentialName: passkeyName.trim() }),
|
||||
});
|
||||
|
||||
if (!optionsResponse.ok) throw new Error('Failed to get registration options');
|
||||
const options = await optionsResponse.json();
|
||||
|
||||
showStatus('Please touch your security key or use biometric authentication...', 'loading');
|
||||
|
||||
const publicKey = {
|
||||
...options,
|
||||
challenge: base64ToArrayBuffer(options.challenge),
|
||||
user: {
|
||||
...options.user,
|
||||
id: base64ToArrayBuffer(options.user.id),
|
||||
},
|
||||
attestation: 'direct',
|
||||
};
|
||||
|
||||
const credential = await navigator.credentials.create({ publicKey });
|
||||
|
||||
if (!credential) {
|
||||
showStatus('Registration was cancelled', 'error');
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
showStatus('Verifying credential...', 'loading');
|
||||
|
||||
const completeResponse = await fetch(apiRoutes.registrationComplete, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
credential: credentialToJSON(credential),
|
||||
}),
|
||||
});
|
||||
|
||||
if (!completeResponse.ok) throw new Error('Failed to complete registration');
|
||||
|
||||
showStatus('Passkey registered successfully!', 'success');
|
||||
setTimeout(() => window.location.reload(), 1500);
|
||||
} catch (error) {
|
||||
console.error('Registration error:', error);
|
||||
showStatus('Error: ' + error.message, 'error');
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
}, [passkeyName, apiRoutes, showStatus]);
|
||||
|
||||
const openRenameModal = useCallback(credential => {
|
||||
setSelectedCredential(credential);
|
||||
setRenameName(credential.credentialName);
|
||||
setStatus({ type: '', message: '' });
|
||||
setRenameModalOpen(true);
|
||||
}, []);
|
||||
|
||||
const closeRenameModal = useCallback(() => {
|
||||
setRenameModalOpen(false);
|
||||
setSelectedCredential(null);
|
||||
setRenameName('');
|
||||
setStatus({ type: '', message: '' });
|
||||
}, []);
|
||||
|
||||
const handleRename = useCallback(async () => {
|
||||
if (!renameName.trim() || !selectedCredential) {
|
||||
showStatus('Please enter a new name', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
const response = await fetch(`${apiRoutes.credentials}/${selectedCredential.id}/rename`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name: renameName.trim() }),
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error('Failed to rename passkey');
|
||||
|
||||
window.location.reload();
|
||||
} catch (error) {
|
||||
console.error('Rename error:', error);
|
||||
showStatus('Error: ' + error.message, 'error');
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
}, [renameName, selectedCredential, apiRoutes, showStatus]);
|
||||
|
||||
const openDeleteModal = useCallback(credential => {
|
||||
setSelectedCredential(credential);
|
||||
setDeleteModalOpen(true);
|
||||
}, []);
|
||||
|
||||
const closeDeleteModal = useCallback(() => {
|
||||
setDeleteModalOpen(false);
|
||||
setSelectedCredential(null);
|
||||
}, []);
|
||||
|
||||
const handleDelete = useCallback(async () => {
|
||||
if (!selectedCredential) return;
|
||||
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
const response = await fetch(`${apiRoutes.credentials}/${selectedCredential.id}`, {
|
||||
method: 'DELETE',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
window.location.reload();
|
||||
} else {
|
||||
showStatus('Failed to delete passkey', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Delete error:', error);
|
||||
showStatus('Error: ' + error.message, 'error');
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
setDeleteModalOpen(false);
|
||||
}, [selectedCredential, apiRoutes, showStatus]);
|
||||
|
||||
const statusClass = 'error' === status.type
|
||||
? 'registration-status--error'
|
||||
: 'success' === status.type
|
||||
? 'registration-status--success'
|
||||
: 'loading' === status.type
|
||||
? 'registration-status--loading'
|
||||
: '';
|
||||
|
||||
return (
|
||||
<div className="passkey-manager">
|
||||
<div className="passkey-manager__actions">
|
||||
<button className="btn btn--primary" onClick={openAddModal} type="button">
|
||||
<i className="fa fa-plus" /> Add Passkey
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="passkey-manager__list">
|
||||
{0 < credentialsList.length ? (
|
||||
credentialsList.map(credential => (
|
||||
<div key={credential.id} className="passkey-item" data-credential-id={credential.id}>
|
||||
<div className="passkey-item__header">
|
||||
<div className="passkey-item__info">
|
||||
<h3 className="passkey-item__name">{credential.credentialName}</h3>
|
||||
<p className="passkey-item__meta">
|
||||
Created: {credential.createdAt}
|
||||
{credential.lastUsedAt && (
|
||||
<>
|
||||
<br />Last used: {credential.lastUsedAt}
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="passkey-item__badges">
|
||||
{credential.isBackupEligible && (
|
||||
<span className="badge badge--info" title="This passkey can be backed up">
|
||||
<i className="fa fa-cloud" /> Backup Eligible
|
||||
</span>
|
||||
)}
|
||||
{credential.isBackupAuthenticated && (
|
||||
<span className="badge badge--success" title="This passkey is backed up">
|
||||
<i className="fa fa-check" /> Backed Up
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="passkey-item__actions">
|
||||
<button
|
||||
className="btn btn--sm btn--warning"
|
||||
onClick={() => openRenameModal(credential)}
|
||||
type="button"
|
||||
>
|
||||
<i className="fa fa-edit" /> Rename
|
||||
</button>
|
||||
<button
|
||||
className="btn btn--sm btn--danger"
|
||||
onClick={() => openDeleteModal(credential)}
|
||||
type="button"
|
||||
>
|
||||
<i className="fa fa-trash" /> Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="empty-state">
|
||||
<i className="fa fa-key empty-state__icon" />
|
||||
<p className="empty-state__text">No passkeys registered yet.</p>
|
||||
<p className="empty-state__subtext">
|
||||
Add your first passkey to get started with secure, passwordless authentication.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<StyledDialog open={addModalOpen} onClose={closeAddModal}>
|
||||
<DialogTitle>Add New Passkey</DialogTitle>
|
||||
<DialogContent>
|
||||
<TextField
|
||||
autoFocus
|
||||
fullWidth
|
||||
id="passkeyName"
|
||||
label="Passkey Name"
|
||||
placeholder="e.g., My Laptop, iPhone"
|
||||
value={passkeyName}
|
||||
onChange={e => setPasskeyName(e.target.value)}
|
||||
onKeyUp={e => 'Enter' === e.key && handleAddPasskey()}
|
||||
margin="dense"
|
||||
variant="outlined"
|
||||
helperText="Give this passkey a descriptive name to help you remember it."
|
||||
/>
|
||||
{status.message && (
|
||||
<div className={`registration-status ${statusClass}`}>
|
||||
{status.message}
|
||||
</div>
|
||||
)}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={closeAddModal} disabled={loading}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleAddPasskey} variant="contained" disabled={loading}>
|
||||
Continue
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</StyledDialog>
|
||||
|
||||
<StyledDialog open={renameModalOpen} onClose={closeRenameModal}>
|
||||
<DialogTitle>Rename Passkey</DialogTitle>
|
||||
<DialogContent>
|
||||
<TextField
|
||||
autoFocus
|
||||
fullWidth
|
||||
id="renamePasskeyName"
|
||||
label="New Name"
|
||||
value={renameName}
|
||||
onChange={e => setRenameName(e.target.value)}
|
||||
onKeyUp={e => 'Enter' === e.key && handleRename()}
|
||||
margin="dense"
|
||||
variant="outlined"
|
||||
/>
|
||||
{status.message && (
|
||||
<div className={`registration-status ${statusClass}`}>
|
||||
{status.message}
|
||||
</div>
|
||||
)}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={closeRenameModal} disabled={loading}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleRename} variant="contained" disabled={loading}>
|
||||
Rename
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</StyledDialog>
|
||||
|
||||
<StyledDialog open={deleteModalOpen} onClose={closeDeleteModal}>
|
||||
<DialogTitle>Delete Passkey</DialogTitle>
|
||||
<DialogContent>
|
||||
<p>
|
||||
Are you sure you want to delete the passkey
|
||||
{selectedCredential && (
|
||||
<Fragment>
|
||||
<strong>{selectedCredential.credentialName}</strong>?
|
||||
This action cannot be undone.
|
||||
</Fragment>
|
||||
)}
|
||||
</p>
|
||||
{status.message && (
|
||||
<div className={`registration-status ${statusClass}`}>
|
||||
{status.message}
|
||||
</div>
|
||||
)}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={closeDeleteModal} disabled={loading}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleDelete} variant="contained" color="error" disabled={loading}>
|
||||
Delete
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</StyledDialog>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PasskeyManager;
|
||||
|
||||
PasskeyManager.propTypes = {
|
||||
credentials: arrayOf(shape({
|
||||
id: string.isRequired,
|
||||
credentialName: string.isRequired,
|
||||
createdAt: string,
|
||||
lastUsedAt: string,
|
||||
isBackupEligible: bool,
|
||||
isBackupAuthenticated: bool,
|
||||
})).isRequired,
|
||||
apiRoutes: shape({
|
||||
credentials: string.isRequired,
|
||||
registrationBegin: string.isRequired,
|
||||
registrationComplete: string.isRequired,
|
||||
}).isRequired,
|
||||
};
|
||||
165
assets/js/components/ProfileCharts.jsx
Normal file
165
assets/js/components/ProfileCharts.jsx
Normal file
@@ -0,0 +1,165 @@
|
||||
/**
|
||||
* This file is part of the SplendidBear Websites' projects.
|
||||
*
|
||||
* Copyright (c) 2026 @ www.splendidbear.org
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { BarChart } from '@mui/x-charts/BarChart';
|
||||
import { LineChart } from '@mui/x-charts/LineChart';
|
||||
import { PieChart } from '@mui/x-charts/PieChart';
|
||||
import { createTheme, ThemeProvider } from '@mui/material/styles';
|
||||
import { shape, arrayOf, number, string } from 'prop-types';
|
||||
|
||||
const darkTheme = createTheme({
|
||||
palette: {
|
||||
mode: 'dark',
|
||||
background: { paper: 'transparent' },
|
||||
},
|
||||
typography: {
|
||||
fontFamily: '\'Rajdhani\', sans-serif',
|
||||
},
|
||||
});
|
||||
|
||||
const WIN_COLOR = '#5ee89a';
|
||||
const LOSS_COLOR = '#f67d52';
|
||||
const DRAW_COLOR = '#95cff5';
|
||||
const MINES_COLOR = '#f67d52';
|
||||
const BONUS_COLOR = '#ffd700';
|
||||
|
||||
const axisStyle = {
|
||||
tickLabelStyle: { fill: 'rgba(255,255,255,0.4)', fontSize: 11, fontFamily: '\'Rajdhani\', sans-serif' },
|
||||
labelStyle: { fill: 'rgba(255,255,255,0.4)', fontSize: 11, fontFamily: '\'Rajdhani\', sans-serif' },
|
||||
};
|
||||
|
||||
export default function ProfileCharts({ chartData }) {
|
||||
const { months, wins, losses, draws, pieWins, pieLosses, pieDraws, recentGames } = chartData;
|
||||
const total = pieWins + pieLosses + pieDraws;
|
||||
|
||||
const hasBars = wins.some(v => 0 < v) || losses.some(v => 0 < v) || draws.some(v => 0 < v);
|
||||
const hasRecent = recentGames
|
||||
&& (recentGames.mines?.some(v => 0 < v) || recentGames.bonus?.some(v => 0 < v));
|
||||
|
||||
return (
|
||||
<ThemeProvider theme={darkTheme}>
|
||||
<div className="profile-charts">
|
||||
{0 < total && (
|
||||
<div className="profile-chart-block">
|
||||
<h2 className="profile-section__title">
|
||||
<i className="fa fa-pie-chart" /> Result Breakdown
|
||||
</h2>
|
||||
<div className="profile-chart-inner">
|
||||
<PieChart
|
||||
series={[{
|
||||
data: [
|
||||
{ id: 0, value: pieWins, label: `Wins ${pieWins}`, color: WIN_COLOR },
|
||||
{ id: 1, value: pieLosses, label: `Losses ${pieLosses}`, color: LOSS_COLOR },
|
||||
{ id: 2, value: pieDraws, label: `Draws ${pieDraws}`, color: DRAW_COLOR },
|
||||
],
|
||||
innerRadius: 52,
|
||||
paddingAngle: 3,
|
||||
cornerRadius: 4,
|
||||
highlightScope: { fade: 'global', highlight: 'item' },
|
||||
}]}
|
||||
slotProps={{
|
||||
legend: {
|
||||
labelStyle: {
|
||||
fill: 'rgba(255,255,255,0.55)',
|
||||
fontSize: 13,
|
||||
fontFamily: '\'Rajdhani\', sans-serif',
|
||||
},
|
||||
},
|
||||
}}
|
||||
width={320}
|
||||
height={200}
|
||||
margin={{ top: 10, bottom: 10, left: 10, right: 120 }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{hasBars && (
|
||||
<div className="profile-chart-block">
|
||||
<h2 className="profile-section__title">
|
||||
<i className="fa fa-bar-chart" /> Activity (6 months)
|
||||
</h2>
|
||||
<div className="profile-chart-inner">
|
||||
<BarChart
|
||||
xAxis={[{ scaleType: 'band', data: months, ...axisStyle }]}
|
||||
yAxis={[{ ...axisStyle }]}
|
||||
series={[
|
||||
{ data: wins, label: 'Wins', color: WIN_COLOR, stack: 'total' },
|
||||
{ data: losses, label: 'Losses', color: LOSS_COLOR, stack: 'total' },
|
||||
{ data: draws, label: 'Draws', color: DRAW_COLOR, stack: 'total' },
|
||||
]}
|
||||
slotProps={{
|
||||
legend: {
|
||||
labelStyle: {
|
||||
fill: 'rgba(255,255,255,0.55)',
|
||||
fontSize: 13,
|
||||
fontFamily: '\'Rajdhani\', sans-serif',
|
||||
},
|
||||
},
|
||||
}}
|
||||
borderRadius={3}
|
||||
width={460}
|
||||
height={200}
|
||||
margin={{ top: 10, bottom: 30, left: 30, right: 120 }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{hasRecent && (
|
||||
<div className="profile-chart-block profile-chart-block--wide">
|
||||
<h2 className="profile-section__title">
|
||||
<i className="fa fa-line-chart" /> Last {recentGames.labels.length} games — mines & bonus
|
||||
</h2>
|
||||
<div className="profile-chart-inner">
|
||||
<LineChart
|
||||
xAxis={[{ scaleType: 'band', data: recentGames.labels, ...axisStyle }]}
|
||||
yAxis={[{ ...axisStyle }]}
|
||||
series={[
|
||||
{ data: recentGames.mines, label: 'Mines hit', color: MINES_COLOR },
|
||||
{ data: recentGames.bonus, label: 'Bonus points', color: BONUS_COLOR },
|
||||
]}
|
||||
slotProps={{
|
||||
legend: {
|
||||
labelStyle: {
|
||||
fill: 'rgba(255,255,255,0.55)',
|
||||
fontSize: 13,
|
||||
fontFamily: '\'Rajdhani\', sans-serif',
|
||||
},
|
||||
},
|
||||
}}
|
||||
borderRadius={3}
|
||||
height={220}
|
||||
margin={{ top: 10, bottom: 30, left: 40, right: 140 }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
|
||||
ProfileCharts.propTypes = {
|
||||
chartData: shape({
|
||||
months: arrayOf(string).isRequired,
|
||||
wins: arrayOf(number).isRequired,
|
||||
losses: arrayOf(number).isRequired,
|
||||
draws: arrayOf(number).isRequired,
|
||||
pieWins: number.isRequired,
|
||||
pieLosses: number.isRequired,
|
||||
pieDraws: number.isRequired,
|
||||
recentGames: shape({
|
||||
labels: arrayOf(string).isRequired,
|
||||
mines: arrayOf(number).isRequired,
|
||||
bonus: arrayOf(number).isRequired,
|
||||
}).isRequired,
|
||||
}).isRequired,
|
||||
};
|
||||
54
assets/js/components/battle-dialog/Avatar.jsx
Normal file
54
assets/js/components/battle-dialog/Avatar.jsx
Normal file
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* This file is part of the SplendidBear Websites' projects.
|
||||
*
|
||||
* Copyright (c) 2026 @ www.splendidbear.org
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
import { string } from 'prop-types';
|
||||
|
||||
export const Avatar = ({ name, color, avatarUrl, bonusPoints = 0 }) => {
|
||||
const isRed = 'red' === color;
|
||||
const initials = useMemo(() => (name || '?').slice(0, 2).toUpperCase(), [name]);
|
||||
|
||||
const cssVars = isRed ? {
|
||||
'--bd-avatar-gradient': 'linear-gradient(135deg, rgba(173,10,5,0.6) 0%, rgba(246,125,82,0.4) 100%)',
|
||||
'--bd-avatar-glow': '0 0 0 3px rgba(173,10,5,0.2), 0 0 28px rgba(173,10,5,0.35)',
|
||||
'--bd-avatar-border': 'rgba(173,10,5,0.5)',
|
||||
'--bd-avatar-color': '#f67d52',
|
||||
} : {
|
||||
'--bd-avatar-gradient': 'linear-gradient(135deg, rgba(35,111,135,0.6) 0%, rgba(41,128,185,0.4) 100%)',
|
||||
'--bd-avatar-glow': '0 0 0 3px rgba(35,111,135,0.2), 0 0 28px rgba(35,111,135,0.35)',
|
||||
'--bd-avatar-border': 'rgba(35,111,135,0.5)',
|
||||
'--bd-avatar-color': '#95cff5',
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bd-avatar-wrap" style={cssVars}>
|
||||
<div className="bd-avatar-ring-wrap">
|
||||
<div className="bd-avatar-ring">
|
||||
{avatarUrl
|
||||
? <img src={avatarUrl} alt={name} className="bd-avatar-img" />
|
||||
: initials}
|
||||
</div>
|
||||
{0 < bonusPoints && (
|
||||
<div className="bd-avatar-bonus">
|
||||
<i className="fa fa-star" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<span className="bd-avatar-name">{name}</span>
|
||||
<span className="bd-avatar-side">{isRed ? 'Red' : 'Blue'}</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Avatar.propTypes = {
|
||||
name: string,
|
||||
color: string,
|
||||
avatarUrl: string,
|
||||
bonusPoints: string,
|
||||
};
|
||||
122
assets/js/components/battle-dialog/BonusPoints.jsx
Normal file
122
assets/js/components/battle-dialog/BonusPoints.jsx
Normal file
@@ -0,0 +1,122 @@
|
||||
/**
|
||||
* This file is part of the SplendidBear Websites' projects.
|
||||
*
|
||||
* Copyright (c) 2026 @ www.splendidbear.org
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
import { useMemo } from 'react';
|
||||
import { StatRow } from './StatRow';
|
||||
import { object } from 'prop-types';
|
||||
|
||||
export const BonusPoints = ({ game }) => {
|
||||
const hasBonuspoints = useMemo(
|
||||
() => 0 < game?.redBonusPoints
|
||||
|| 0 < game?.blueBonusPoints
|
||||
|| game?.redBonusStats?.blindHits
|
||||
|| game?.blueBonusStats?.blindHits,
|
||||
[
|
||||
game?.blueBonusPoints,
|
||||
game?.blueBonusStats?.blindHits,
|
||||
game?.redBonusPoints,
|
||||
game?.redBonusStats?.blindHits,
|
||||
],
|
||||
);
|
||||
|
||||
const hasRedNoBonuses = useMemo(
|
||||
() => !game.redBonusStats?.blindHits
|
||||
&& !game.redBonusStats?.chainBest
|
||||
&& !game.redBonusStats?.edgeMines
|
||||
&& !game.redBonusStats?.lastMineHits
|
||||
&& !game.redBonusStats?.biggestReveal,
|
||||
[
|
||||
game.redBonusStats?.biggestReveal,
|
||||
game.redBonusStats?.blindHits,
|
||||
game.redBonusStats?.chainBest,
|
||||
game.redBonusStats?.edgeMines,
|
||||
game.redBonusStats?.lastMineHits,
|
||||
],
|
||||
);
|
||||
|
||||
const hasBlueNoBonuses = useMemo(
|
||||
() => !game.blueBonusStats?.blindHits
|
||||
&& !game.blueBonusStats?.chainBest
|
||||
&& !game.blueBonusStats?.edgeMines
|
||||
&& !game.blueBonusStats?.lastMineHits
|
||||
&& !game.blueBonusStats?.biggestReveal,
|
||||
[
|
||||
game.blueBonusStats?.biggestReveal,
|
||||
game.blueBonusStats?.blindHits,
|
||||
game.blueBonusStats?.chainBest,
|
||||
game.blueBonusStats?.edgeMines,
|
||||
game.blueBonusStats?.lastMineHits,
|
||||
],
|
||||
);
|
||||
|
||||
if (!hasBonuspoints) return '';
|
||||
|
||||
return (
|
||||
<div className="bd-bonus">
|
||||
<div className="bd-bonus__grid">
|
||||
<div className="bd-bonus__column bd-bonus__column--red">
|
||||
<span className="bd-bonus__heading">
|
||||
<i className="fa fa-star" /> Red Bonus Statistics
|
||||
</span>
|
||||
<div className="bd-bonus__rows">
|
||||
<StatRow icon="fa-star" label="Total Bonus Points" value={(game.redBonusPoints ?? 0).toFixed(1)} valueColor="#ffd700" />
|
||||
{0 < game.redBonusStats?.blindHits && (
|
||||
<StatRow icon="fa-bullseye" label="Blind hits" value={game.redBonusStats.blindHits} />
|
||||
)}
|
||||
{0 < game.redBonusStats?.chainBest && (
|
||||
<StatRow icon="fa-link" label="Best chain" value={game.redBonusStats.chainBest} />
|
||||
)}
|
||||
{0 < game.redBonusStats?.edgeMines && (
|
||||
<StatRow icon="fa-border-all" label="Edge mines" value={game.redBonusStats.edgeMines} />
|
||||
)}
|
||||
{0 < game.redBonusStats?.lastMineHits && (
|
||||
<StatRow icon="fa-hourglass-end" label="Endgame mines" value={game.redBonusStats.lastMineHits} />
|
||||
)}
|
||||
{0 < game.redBonusStats?.biggestReveal && (
|
||||
<StatRow icon="fa-expand" label="Biggest reveal" value={game.redBonusStats.biggestReveal} />
|
||||
)}
|
||||
{hasRedNoBonuses && (
|
||||
<StatRow icon="fa-minus-circle" label="Status" value="No bonuses" valueColor="rgba(255,255,255,0.3)" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="bd-bonus__column bd-bonus__column--blue">
|
||||
<span className="bd-bonus__heading">
|
||||
<i className="fa fa-star" /> Blue Bonus Statistics
|
||||
</span>
|
||||
<div className="bd-bonus__rows">
|
||||
<StatRow icon="fa-star" label="Total Bonus Points" value={(game.blueBonusPoints ?? 0).toFixed(1)} valueColor="#ffd700" />
|
||||
{0 < game.blueBonusStats?.blindHits && (
|
||||
<StatRow icon="fa-bullseye" label="Blind hits" value={game.blueBonusStats.blindHits} />
|
||||
)}
|
||||
{0 < game.blueBonusStats?.chainBest && (
|
||||
<StatRow icon="fa-link" label="Best chain" value={game.blueBonusStats.chainBest} />
|
||||
)}
|
||||
{0 < game.blueBonusStats?.edgeMines && (
|
||||
<StatRow icon="fa-border-all" label="Edge mines" value={game.blueBonusStats.edgeMines} />
|
||||
)}
|
||||
{0 < game.blueBonusStats?.lastMineHits && (
|
||||
<StatRow icon="fa-hourglass-end" label="Endgame mines" value={game.blueBonusStats.lastMineHits} />
|
||||
)}
|
||||
{0 < game.blueBonusStats?.biggestReveal && (
|
||||
<StatRow icon="fa-expand" label="Biggest reveal" value={game.blueBonusStats.biggestReveal} />
|
||||
)}
|
||||
{hasBlueNoBonuses && (
|
||||
<StatRow icon="fa-minus-circle" label="Status" value="No bonuses" valueColor="rgba(255,255,255,0.3)" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
BonusPoints.propTypes = {
|
||||
game: object.isRequired,
|
||||
};
|
||||
31
assets/js/components/battle-dialog/StatRow.jsx
Normal file
31
assets/js/components/battle-dialog/StatRow.jsx
Normal file
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* This file is part of the SplendidBear Websites' projects.
|
||||
*
|
||||
* Copyright (c) 2026 @ www.splendidbear.org
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { node, string } from 'prop-types';
|
||||
|
||||
export const StatRow = ({ icon, label, value, valueColor }) => (
|
||||
<div className="bd-stat-row">
|
||||
<i className={`fa ${icon} bd-stat-row__icon`} />
|
||||
<span className="bd-stat-row__label">{label}</span>
|
||||
<span
|
||||
className="bd-stat-row__value"
|
||||
style={valueColor ? { '--bd-stat-value-color': valueColor } : undefined}
|
||||
>
|
||||
{value}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
StatRow.propTypes = {
|
||||
icon: string.isRequired,
|
||||
label: string.isRequired,
|
||||
value: node.isRequired,
|
||||
valueColor: string,
|
||||
};
|
||||
18
assets/js/components/index.js
Normal file
18
assets/js/components/index.js
Normal file
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* This file is part of the SplendidBear Websites' projects.
|
||||
*
|
||||
* Copyright (c) 2026 @ www.splendidbear.org
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
export { AvatarUpload } from './AvatarUpload';
|
||||
export { BattleDialog } from './BattleDialog';
|
||||
export { default as ContactForm } from './ContactForm';
|
||||
export { default as PasskeyLogin } from './PasskeyLogin';
|
||||
export { default as PasskeyManager } from './PasskeyManager';
|
||||
export { default as ProfileCharts } from './ProfileCharts';
|
||||
export { BonusPoints } from './battle-dialog/BonusPoints';
|
||||
export { Avatar } from './battle-dialog/Avatar';
|
||||
export { StatRow } from './battle-dialog/StatRow';
|
||||
30
assets/js/contact.jsx
Normal file
30
assets/js/contact.jsx
Normal file
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* This file is part of the SplendidBear Websites' projects.
|
||||
*
|
||||
* Copyright (c) 2026 @ www.splendidbear.org
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { ContactForm } from '@global-components';
|
||||
|
||||
const wrapper = document.getElementById('contact-form-wrapper');
|
||||
|
||||
if (wrapper) {
|
||||
const siteKey = wrapper.dataset.siteKey;
|
||||
const recaptchaFieldId = wrapper.dataset.recaptchaFieldId;
|
||||
|
||||
if (siteKey && recaptchaFieldId) {
|
||||
createRoot(wrapper).render(
|
||||
<ContactForm
|
||||
siteKey={siteKey}
|
||||
recaptchaFieldId={recaptchaFieldId}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
console.error('ContactForm: Missing siteKey or recaptchaFieldId in data attributes');
|
||||
}
|
||||
}
|
||||
43
assets/js/mine-seeker/MineSeeker.jsx
Normal file
43
assets/js/mine-seeker/MineSeeker.jsx
Normal file
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* This file is part of the SplendidBear Websites' projects.
|
||||
*
|
||||
* Copyright (c) 2026 @ www.splendidbear.org
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
import React, { useRef } from 'react';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { GameProvider } from '@mine-contexts';
|
||||
import { GameBoard } from '@mine-components';
|
||||
import { string } from 'prop-types';
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
const MineSeeker = ({ env, gameId, opponentName = '' }) => {
|
||||
const isEnvDev = 'dev' === env;
|
||||
const gameAssoc = useRef('' !== gameId ? gameId : crypto.randomUUID()).current;
|
||||
const gameInherited = '' !== gameId;
|
||||
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<GameProvider>
|
||||
<GameBoard
|
||||
gameAssoc={gameAssoc}
|
||||
gameInherited={gameInherited}
|
||||
opponentName={opponentName}
|
||||
isEnvDev={isEnvDev}
|
||||
/>
|
||||
</GameProvider>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default MineSeeker;
|
||||
|
||||
MineSeeker.propTypes = {
|
||||
env: string.isRequired,
|
||||
gameId: string,
|
||||
opponentName: string,
|
||||
};
|
||||
33
assets/js/mine-seeker/components/BonusBox.jsx
Normal file
33
assets/js/mine-seeker/components/BonusBox.jsx
Normal file
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* This file is part of the SplendidBear Websites' projects.
|
||||
*
|
||||
* Copyright (c) 2026 @ www.splendidbear.org
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { func, number, string } from 'prop-types';
|
||||
|
||||
const BonusBox = ({ color, points, onClick, title }) => (
|
||||
<button
|
||||
type="button"
|
||||
className={`bonus-box ${color}-bonus`}
|
||||
onClick={onClick}
|
||||
title={title || 'View bonus statistics'}
|
||||
aria-label={`${color} bonus points: ${points}`}
|
||||
>
|
||||
<i className="fa fa-star bonus-box__icon" />
|
||||
<span className="bonus-box__value">{points}</span>
|
||||
</button>
|
||||
);
|
||||
|
||||
export default BonusBox;
|
||||
|
||||
BonusBox.propTypes = {
|
||||
color: string.isRequired,
|
||||
points: number.isRequired,
|
||||
onClick: func.isRequired,
|
||||
title: string,
|
||||
};
|
||||
79
assets/js/mine-seeker/components/BonusStatsDialog.jsx
Normal file
79
assets/js/mine-seeker/components/BonusStatsDialog.jsx
Normal file
@@ -0,0 +1,79 @@
|
||||
/**
|
||||
* This file is part of the SplendidBear Websites' projects.
|
||||
*
|
||||
* Copyright (c) 2026 @ www.splendidbear.org
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import Dialog from '@mui/material/Dialog';
|
||||
import { styled } from '@mui/material/styles';
|
||||
import { PlayerColumn } from '@mine-components';
|
||||
import { bool, func, shape, string, number, object } from 'prop-types';
|
||||
|
||||
const BonusStatsDialog = ({ open, onClose, red, blue }) => (
|
||||
<StyledDialog open={open} onClose={onClose}>
|
||||
<div className="bsd">
|
||||
<div className="bsd-header">
|
||||
<div className="bsd-header-text">
|
||||
<span className="bsd-label">Scoring</span>
|
||||
<h2 className="bsd-title">
|
||||
<i className="fa fa-star" />
|
||||
Bonus Statistics
|
||||
</h2>
|
||||
</div>
|
||||
<button className="bsd-close" onClick={onClose} aria-label="Close">
|
||||
<i className="fa fa-times" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="bsd-body">
|
||||
<PlayerColumn color="red" player={red} />
|
||||
<PlayerColumn color="blue" player={blue} />
|
||||
</div>
|
||||
<p className="bsd-note">
|
||||
Bonus points are awarded alongside the main score for skillful play.
|
||||
</p>
|
||||
</div>
|
||||
</StyledDialog>
|
||||
);
|
||||
|
||||
const StyledDialog = styled(Dialog)({
|
||||
'& .MuiDialog-paper': {
|
||||
background: '#07090d',
|
||||
backgroundImage: `
|
||||
linear-gradient(rgba(35, 111, 135, 0.08) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(35, 111, 135, 0.08) 1px, transparent 1px)
|
||||
`,
|
||||
backgroundSize: '46px 46px',
|
||||
border: '1px solid rgba(35, 111, 135, 0.4)',
|
||||
borderRadius: '12px',
|
||||
boxShadow: '0 0 80px rgba(35, 111, 135, 0.15), 0 32px 80px rgba(0, 0, 0, 0.9)',
|
||||
width: '560px',
|
||||
maxWidth: '94vw',
|
||||
overflow: 'hidden',
|
||||
color: '#fff',
|
||||
},
|
||||
'& .MuiBackdrop-root': {
|
||||
background: 'rgba(2, 4, 8, 0.88)',
|
||||
backdropFilter: 'blur(4px)',
|
||||
},
|
||||
});
|
||||
|
||||
export default BonusStatsDialog;
|
||||
|
||||
BonusStatsDialog.propTypes = {
|
||||
open: bool.isRequired,
|
||||
onClose: func.isRequired,
|
||||
red: shape({
|
||||
name: string,
|
||||
bonusPoints: number,
|
||||
bonusStats: object,
|
||||
}).isRequired,
|
||||
blue: shape({
|
||||
name: string,
|
||||
bonusPoints: number,
|
||||
bonusStats: object,
|
||||
}).isRequired,
|
||||
};
|
||||
123
assets/js/mine-seeker/components/CaptchaOverlay.jsx
Normal file
123
assets/js/mine-seeker/components/CaptchaOverlay.jsx
Normal file
@@ -0,0 +1,123 @@
|
||||
/**
|
||||
* This file is part of the SplendidBear Websites' projects.
|
||||
*
|
||||
* Copyright (c) 2026 @ www.splendidbear.org
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
import React, { Fragment, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { func, node, string } from 'prop-types';
|
||||
|
||||
const CAPTCHA_STORAGE_KEY = 'mineseeker_captcha_verified';
|
||||
const CAPTCHA_TOKEN_KEY = 'mineseeker_captcha_token';
|
||||
const RECAPTCHA_ACTION = 'mineseeker_play';
|
||||
|
||||
const CaptchaOverlay = ({ siteKey, onVerified, children }) => {
|
||||
const [verified, setVerified] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState(false);
|
||||
|
||||
const handleToken = useCallback(token => {
|
||||
const wrapper = document.getElementById('mine-wrapper');
|
||||
if (wrapper) {
|
||||
wrapper.dataset.captchaToken = token;
|
||||
}
|
||||
sessionStorage.setItem(CAPTCHA_TOKEN_KEY, token);
|
||||
sessionStorage.setItem(CAPTCHA_STORAGE_KEY, Date.now().toString());
|
||||
setVerified(true);
|
||||
onVerified?.();
|
||||
}, [onVerified]);
|
||||
|
||||
const buttonClasses = useMemo(() => [
|
||||
'captcha-button',
|
||||
error && 'captcha-button--error',
|
||||
loading && 'captcha-button--loading',
|
||||
].filter(Boolean).join(' '), [error, loading]);
|
||||
|
||||
useEffect(() => {
|
||||
const storedToken = sessionStorage.getItem(CAPTCHA_TOKEN_KEY);
|
||||
const storedTime = sessionStorage.getItem(CAPTCHA_STORAGE_KEY);
|
||||
|
||||
if (storedToken && storedTime) {
|
||||
const elapsed = (Date.now() - parseInt(storedTime)) / 1000;
|
||||
if (110 > elapsed) {
|
||||
const wrapper = document.getElementById('mine-wrapper');
|
||||
if (wrapper) {
|
||||
wrapper.dataset.captchaToken = storedToken;
|
||||
}
|
||||
setVerified(true);
|
||||
onVerified?.();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (window.grecaptcha) {
|
||||
window.grecaptcha.ready(() => {
|
||||
window.grecaptcha
|
||||
.execute(siteKey, { action: RECAPTCHA_ACTION })
|
||||
.then(token => {
|
||||
handleToken(token);
|
||||
})
|
||||
.catch(() => {
|
||||
setError(true);
|
||||
});
|
||||
});
|
||||
}
|
||||
}, [siteKey, onVerified, handleToken]);
|
||||
|
||||
|
||||
const handleClick = () => {
|
||||
setLoading(true);
|
||||
setError(false);
|
||||
|
||||
window.grecaptcha.ready(() => {
|
||||
window.grecaptcha
|
||||
.execute(siteKey, { action: RECAPTCHA_ACTION })
|
||||
.then(token => {
|
||||
handleToken(token);
|
||||
setLoading(false);
|
||||
})
|
||||
.catch(() => {
|
||||
setLoading(false);
|
||||
setError(true);
|
||||
setTimeout(() => setError(false), 2000);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
if (verified) {
|
||||
return <Fragment>{children}</Fragment>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="captcha-overlay">
|
||||
<div className="captcha-content">
|
||||
<div className="captcha-icon">
|
||||
<i className="fa fa-shield-halved" />
|
||||
</div>
|
||||
<h1 className="captcha-title">Ready to Play?</h1>
|
||||
<p className="captcha-description">
|
||||
Click below to verify you're human and start playing.
|
||||
</p>
|
||||
<button
|
||||
className={buttonClasses}
|
||||
onClick={handleClick}
|
||||
disabled={loading}
|
||||
>
|
||||
<i className={`fa ${loading ? 'fa-spinner fa-spin' : error ? 'fa-exclamation-circle' : 'fa-play'}`} />
|
||||
{loading ? 'Verifying...' : error ? 'Try Again' : 'Start Playing'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CaptchaOverlay;
|
||||
|
||||
CaptchaOverlay.propTypes = {
|
||||
siteKey: string.isRequired,
|
||||
onVerified: func,
|
||||
children: node,
|
||||
};
|
||||
48
assets/js/mine-seeker/components/ChallengeCountdown.jsx
Normal file
48
assets/js/mine-seeker/components/ChallengeCountdown.jsx
Normal file
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* This file is part of the SplendidBear Websites' projects.
|
||||
*
|
||||
* Copyright (c) 2026 @ www.splendidbear.org
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
import { Fragment, useEffect, useState } from 'react';
|
||||
import { func, number } from 'prop-types';
|
||||
|
||||
const ChallengeCountdown = ({ onAccept, onDecline, seconds = 30 }) => {
|
||||
const [countdown, setCountdown] = useState(seconds);
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
setCountdown(prev => {
|
||||
if (1 >= prev) {
|
||||
clearInterval(interval);
|
||||
onDecline();
|
||||
return 0;
|
||||
}
|
||||
return prev - 1;
|
||||
});
|
||||
}, 1000);
|
||||
return () => clearInterval(interval);
|
||||
}, [onDecline]);
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<p style={{ textAlign: 'center', marginBottom: 20, color: '#95cff5' }}>
|
||||
You have {countdown} second{1 === countdown ? '' : 's'} to answer to the challenge!
|
||||
</p>
|
||||
<div className="resign">
|
||||
<a onClick={onAccept}>Accept</a>
|
||||
<a onClick={onDecline}>Decline</a>
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChallengeCountdown;
|
||||
|
||||
ChallengeCountdown.propTypes = {
|
||||
onAccept: func.isRequired,
|
||||
onDecline: func.isRequired,
|
||||
seconds: number,
|
||||
};
|
||||
52
assets/js/mine-seeker/components/GameBoard.jsx
Normal file
52
assets/js/mine-seeker/components/GameBoard.jsx
Normal file
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* This file is part of the SplendidBear Websites' projects.
|
||||
*
|
||||
* Copyright (c) 2026 @ www.splendidbear.org
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { useGame } from '@mine-contexts';
|
||||
import { useServerCommunication } from '@mine-hooks';
|
||||
import CaptchaOverlay from './CaptchaOverlay';
|
||||
import GridControl from './grid/GridControl';
|
||||
import { bool, string } from 'prop-types';
|
||||
|
||||
export const GameBoard = ({ gameAssoc, gameInherited, opponentName = '', isEnvDev }) => {
|
||||
const { gridReady } = useGame();
|
||||
const { onClick, resign } = useServerCommunication(gameAssoc, gameInherited, opponentName, isEnvDev);
|
||||
const [captchaVerified, setCaptchaVerified] = useState(false);
|
||||
|
||||
const siteKey = document.getElementById('mine-wrapper')?.dataset.recaptchaSiteKey;
|
||||
|
||||
if (!gridReady) {
|
||||
return (
|
||||
<div className="game-overlay">
|
||||
<div className="game-overlay-window"><h1>Loading…</h1></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!captchaVerified && siteKey) {
|
||||
return (
|
||||
<CaptchaOverlay siteKey={siteKey} onVerified={() => setCaptchaVerified(true)} />
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<GridControl
|
||||
gameAssoc={gameAssoc}
|
||||
onClick={onClick}
|
||||
resign={resign}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
GameBoard.propTypes = {
|
||||
gameAssoc: string.isRequired,
|
||||
gameInherited: bool.isRequired,
|
||||
opponentName: string,
|
||||
isEnvDev: bool,
|
||||
};
|
||||
135
assets/js/mine-seeker/components/GameTimer.jsx
Normal file
135
assets/js/mine-seeker/components/GameTimer.jsx
Normal file
@@ -0,0 +1,135 @@
|
||||
/**
|
||||
* This file is part of the SplendidBear Websites' projects.
|
||||
*
|
||||
* Copyright (c) 2026 @ www.splendidbear.org
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { BonusBox, BonusStatsDialog, Avatar } from '@mine-components';
|
||||
import { formatTime } from '@global-utils/format';
|
||||
import { useGame } from '@mine-contexts';
|
||||
|
||||
const GameTimer = () => {
|
||||
const { overlay, connectionLost, endRef, activePlayer, red, blue } = useGame();
|
||||
const [redTime, setRedTime] = useState(0);
|
||||
const [blueTime, setBlueTime] = useState(0);
|
||||
const [isRunning, setIsRunning] = useState(false);
|
||||
const [bonusDialogOpen, setBonusDialogOpen] = useState(false);
|
||||
const timerIntervalRef = useRef(null);
|
||||
const gameStartedRef = useRef(false);
|
||||
|
||||
const redStartTimeRef = useRef(null);
|
||||
const blueStartTimeRef = useRef(null);
|
||||
const lastActivePlayerRef = useRef(null);
|
||||
const pausedRedTimeRef = useRef(0);
|
||||
const pausedBlueTimeRef = useRef(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (!overlay && !gameStartedRef.current) {
|
||||
gameStartedRef.current = true;
|
||||
setIsRunning(true);
|
||||
setRedTime(0);
|
||||
setBlueTime(0);
|
||||
redStartTimeRef.current = Date.now();
|
||||
blueStartTimeRef.current = Date.now();
|
||||
pausedRedTimeRef.current = 0;
|
||||
pausedBlueTimeRef.current = 0;
|
||||
lastActivePlayerRef.current = activePlayer;
|
||||
}
|
||||
}, [activePlayer, overlay]);
|
||||
|
||||
useEffect(() => {
|
||||
if (endRef.current) setIsRunning(false);
|
||||
}, [endRef]);
|
||||
|
||||
useEffect(() => {
|
||||
if (connectionLost) setIsRunning(false);
|
||||
}, [connectionLost]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isRunning) return;
|
||||
|
||||
if (lastActivePlayerRef.current !== activePlayer) {
|
||||
const startRef = lastActivePlayerRef.current ? blueStartTimeRef.current : redStartTimeRef.current;
|
||||
if (startRef) {
|
||||
const elapsed = Math.floor((Date.now() - startRef) / 1000);
|
||||
if (lastActivePlayerRef.current) {
|
||||
pausedBlueTimeRef.current += elapsed;
|
||||
} else {
|
||||
pausedRedTimeRef.current += elapsed;
|
||||
}
|
||||
}
|
||||
|
||||
if (activePlayer) {
|
||||
blueStartTimeRef.current = Date.now();
|
||||
} else {
|
||||
redStartTimeRef.current = Date.now();
|
||||
}
|
||||
|
||||
lastActivePlayerRef.current = activePlayer;
|
||||
}
|
||||
}, [activePlayer, isRunning]);
|
||||
|
||||
const syncTimes = useCallback(() => {
|
||||
let currentRedTime = pausedRedTimeRef.current;
|
||||
let currentBlueTime = pausedBlueTimeRef.current;
|
||||
|
||||
if (!activePlayer && redStartTimeRef.current) {
|
||||
currentRedTime += Math.floor((Date.now() - redStartTimeRef.current) / 1000);
|
||||
} else if (activePlayer && blueStartTimeRef.current) {
|
||||
currentBlueTime += Math.floor((Date.now() - blueStartTimeRef.current) / 1000);
|
||||
}
|
||||
|
||||
setRedTime(currentRedTime);
|
||||
setBlueTime(currentBlueTime);
|
||||
}, [activePlayer]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isRunning) {
|
||||
if (timerIntervalRef.current) clearInterval(timerIntervalRef.current);
|
||||
return;
|
||||
}
|
||||
|
||||
timerIntervalRef.current = setInterval(syncTimes, 100);
|
||||
|
||||
return () => {
|
||||
if (timerIntervalRef.current) clearInterval(timerIntervalRef.current);
|
||||
};
|
||||
}, [isRunning, activePlayer, syncTimes]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleFocus = () => {
|
||||
if (isRunning) syncTimes();
|
||||
};
|
||||
window.addEventListener('focus', handleFocus);
|
||||
return () => window.removeEventListener('focus', handleFocus);
|
||||
}, [isRunning, activePlayer, syncTimes]);
|
||||
|
||||
useEffect(() => () => {
|
||||
if (timerIntervalRef.current) clearInterval(timerIntervalRef.current);
|
||||
}, []);
|
||||
|
||||
|
||||
return (
|
||||
<div className="game-timer-container">
|
||||
<BonusBox color="red" points={red.bonusPoints ?? 0} onClick={() => setBonusDialogOpen(true)} />
|
||||
<div className={`game-timer red-timer ${!activePlayer ? 'active' : ''}`}>
|
||||
<Avatar player={red} />
|
||||
<i className={`fa ${!activePlayer ? 'fa-hourglass-half' : 'fa-hourglass-start'} timer-icon`} />
|
||||
<span className="timer-display">{formatTime(redTime)}</span>
|
||||
</div>
|
||||
<div className={`game-timer blue-timer ${activePlayer ? 'active' : ''}`}>
|
||||
<Avatar player={blue} />
|
||||
<i className={`fa ${activePlayer ? 'fa-hourglass-half' : 'fa-hourglass-start'} timer-icon`} />
|
||||
<span className="timer-display">{formatTime(blueTime)}</span>
|
||||
</div>
|
||||
<BonusBox color="blue" points={blue.bonusPoints ?? 0} onClick={() => setBonusDialogOpen(true)} />
|
||||
<BonusStatsDialog open={bonusDialogOpen} onClose={() => setBonusDialogOpen(false)} red={red} blue={blue} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default GameTimer;
|
||||
294
assets/js/mine-seeker/components/OnlinePlayersDialog.jsx
Normal file
294
assets/js/mine-seeker/components/OnlinePlayersDialog.jsx
Normal file
@@ -0,0 +1,294 @@
|
||||
/**
|
||||
* This file is part of the SplendidBear Websites' projects.
|
||||
*
|
||||
* Copyright (c) 2026 @ www.splendidbear.org
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { formatSince } from '@global-utils/format';
|
||||
import Dialog from '@mui/material/Dialog';
|
||||
import { styled } from '@mui/material/styles';
|
||||
import { useLobbyDataProvider } from '@mine-hooks';
|
||||
import { bool, func, string } from 'prop-types';
|
||||
|
||||
const OnlinePlayersDialog = ({ open, onClose, currentGameAssoc, isEnvDev = false }) => {
|
||||
const [players, setPlayers] = useState([]);
|
||||
const [search, setSearch] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [refreshKey, setRefreshKey] = useState(0);
|
||||
const [snapshotLoaded, setSnapshotLoaded] = useState(false);
|
||||
const [challengingGameAssoc, setChallengingGameAssoc] = useState(null);
|
||||
const [declinedMsg, setDeclinedMsg] = useState('');
|
||||
const [waitingCountdown, setWaitingCountdown] = useState(0);
|
||||
const declinedTimerRef = useRef(null);
|
||||
const { waitingPlayersQuery, challengeMutation } = useLobbyDataProvider();
|
||||
|
||||
const addPlayer = useCallback(entry => {
|
||||
setPlayers(prev =>
|
||||
prev.some(p => p.gameAssoc === entry.gameAssoc)
|
||||
? prev
|
||||
: [...prev, entry],
|
||||
);
|
||||
}, []);
|
||||
|
||||
const removePlayer = useCallback(gameAssoc => {
|
||||
setPlayers(prev => prev.filter(p => p.gameAssoc !== gameAssoc));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
setLoading(true);
|
||||
setSnapshotLoaded(false);
|
||||
|
||||
waitingPlayersQuery.refetch().then(result => {
|
||||
if (result.data) {
|
||||
// Filter out current user's game from the snapshot
|
||||
const filtered = result.data.filter(p => p.gameAssoc !== currentGameAssoc);
|
||||
setPlayers(filtered);
|
||||
}
|
||||
setSnapshotLoaded(true);
|
||||
setLoading(false);
|
||||
}).catch(() => {
|
||||
setPlayers([]);
|
||||
setSnapshotLoaded(true);
|
||||
setLoading(false);
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [open, refreshKey, currentGameAssoc]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open || !snapshotLoaded) return;
|
||||
setSearch('');
|
||||
|
||||
const wrapper = document.getElementById('mine-wrapper');
|
||||
const hubUrl = wrapper?.dataset?.mercureHubUrl;
|
||||
const jwt = wrapper?.dataset?.mercureSubscriberJwt;
|
||||
if (!hubUrl) return;
|
||||
|
||||
const url = new URL(hubUrl, window.location.origin);
|
||||
url.searchParams.append('topic', 'mineseeker/lobby');
|
||||
if (jwt) url.searchParams.append('authorization', jwt);
|
||||
|
||||
const es = new EventSource(url.toString());
|
||||
es.onmessage = e => {
|
||||
const { action, gameAssoc, name, since } = JSON.parse(e.data);
|
||||
// Don't add the current user's game to the list
|
||||
if (gameAssoc === currentGameAssoc) return;
|
||||
if ('join' === action) addPlayer({ gameAssoc, name, since });
|
||||
if ('leave' === action) removePlayer(gameAssoc);
|
||||
};
|
||||
|
||||
return () => es.close();
|
||||
}, [open, snapshotLoaded, addPlayer, removePlayer, currentGameAssoc]);
|
||||
|
||||
useEffect(() => {
|
||||
if (challengeMutation.isError) {
|
||||
setChallengingGameAssoc(null);
|
||||
setWaitingCountdown(0);
|
||||
}
|
||||
}, [challengeMutation.isError]);
|
||||
|
||||
useEffect(() => {
|
||||
const handler = () => {
|
||||
setChallengingGameAssoc(null);
|
||||
clearTimeout(declinedTimerRef.current);
|
||||
setDeclinedMsg('Challenge was not accepted.');
|
||||
setWaitingCountdown(0);
|
||||
declinedTimerRef.current = setTimeout(() => setDeclinedMsg(''), 3500);
|
||||
};
|
||||
window.addEventListener('challenge-declined', handler);
|
||||
return () => {
|
||||
window.removeEventListener('challenge-declined', handler);
|
||||
clearTimeout(declinedTimerRef.current);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!waitingCountdown) return;
|
||||
const interval = setInterval(() => {
|
||||
setWaitingCountdown(prev => {
|
||||
if (1 >= prev) return 0;
|
||||
return prev - 1;
|
||||
});
|
||||
}, 1000);
|
||||
return () => clearInterval(interval);
|
||||
}, [waitingCountdown]);
|
||||
|
||||
const handleChallenge = player => {
|
||||
if (challengingGameAssoc) return;
|
||||
setChallengingGameAssoc(player.gameAssoc);
|
||||
setDeclinedMsg('');
|
||||
setWaitingCountdown(30);
|
||||
|
||||
challengeMutation.mutate(
|
||||
{ targetGameAssoc: player.gameAssoc, challengerGameAssoc: currentGameAssoc }
|
||||
);
|
||||
};
|
||||
|
||||
const visible = players
|
||||
.filter(p => p.gameAssoc !== currentGameAssoc)
|
||||
.filter(p => p.name.toLowerCase().includes(search.toLowerCase()));
|
||||
|
||||
const shown = visible.slice(0, 5);
|
||||
const hasMore = 5 < visible.length;
|
||||
|
||||
// Debug: log if currentGameAssoc is undefined or if current user appears
|
||||
if (isEnvDev && 0 < players.length) {
|
||||
const userInList = players.find(p => p.gameAssoc === currentGameAssoc);
|
||||
|
||||
if (userInList) {
|
||||
console.warn('[OnlinePlayersDialog] Current user appears in players list:', { currentGameAssoc, userInList });
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledDialog
|
||||
open={open}
|
||||
onClose={0 < waitingCountdown ? undefined : onClose}
|
||||
>
|
||||
<div className="opd">
|
||||
<div className="opd-header">
|
||||
<div className="opd-header-text">
|
||||
<span className="opd-label">Multiplayer</span>
|
||||
<h2 className="opd-title">
|
||||
<i className="fa fa-users" />
|
||||
Online Players
|
||||
</h2>
|
||||
</div>
|
||||
<div className="opd-header-actions">
|
||||
<button
|
||||
className={`opd-refresh${loading ? ' opd-refresh--spin' : ''}`}
|
||||
onClick={() => { if (0 === waitingCountdown) setRefreshKey(k => k + 1); }}
|
||||
disabled={loading || 0 < waitingCountdown}
|
||||
aria-label="Refresh"
|
||||
title="Refresh list"
|
||||
>
|
||||
<i className="fa fa-refresh" />
|
||||
</button>
|
||||
<button
|
||||
className="opd-close"
|
||||
onClick={() => { if (0 === waitingCountdown) onClose(); }}
|
||||
disabled={0 < waitingCountdown}
|
||||
aria-label="Close"
|
||||
>
|
||||
<i className="fa fa-times" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{0 < waitingCountdown ? (
|
||||
<div className="opd-waiting">
|
||||
<i className="fa fa-hourglass-start" />
|
||||
<p>Waiting {waitingCountdown} second{1 === waitingCountdown ? '' : 's'} for opponent's answer...</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="opd-search-wrap">
|
||||
<i className="fa fa-search opd-search-icon" />
|
||||
<input
|
||||
className="opd-search"
|
||||
placeholder="Search by username…"
|
||||
value={search}
|
||||
onChange={e => setSearch(e.target.value)}
|
||||
/>
|
||||
{search && (
|
||||
<button className="opd-search-clear" onClick={() => setSearch('')}>
|
||||
<i className="fa fa-times" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="opd-list">
|
||||
{loading && (
|
||||
<div className="opd-empty">
|
||||
<i className="fa fa-spinner fa-spin" />
|
||||
<p>Loading players…</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!loading && 0 === shown.length && (
|
||||
<div className="opd-empty">
|
||||
<i className="fa fa-hourglass-half" />
|
||||
<p>
|
||||
{search
|
||||
? 'No players match your search.'
|
||||
: 'No other players are waiting right now.'}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{declinedMsg && (
|
||||
<div className="opd-declined">
|
||||
<i className="fa fa-times-circle" />
|
||||
{' '}{declinedMsg}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!loading && shown.map(player => {
|
||||
const isWaiting = challengingGameAssoc === player.gameAssoc;
|
||||
return (
|
||||
<div key={player.gameAssoc} className="opd-row">
|
||||
<div className="opd-avatar">
|
||||
{player.name.slice(0, 2).toUpperCase()}
|
||||
</div>
|
||||
<div className="opd-info">
|
||||
<span className="opd-name">{player.name}</span>
|
||||
<span className="opd-since">
|
||||
<i className="fa fa-clock" />
|
||||
{' '}Waiting {formatSince(player.since)}
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
className={`opd-join${isWaiting ? ' opd-join--waiting' : ''}`}
|
||||
onClick={() => handleChallenge(player)}
|
||||
disabled={!!challengingGameAssoc}
|
||||
>
|
||||
<i className={`fa ${isWaiting ? 'fa-spinner fa-spin' : 'fa-play'}`} />
|
||||
{isWaiting ? 'Waiting...' : 'Join'}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{!loading && hasMore && (
|
||||
<p className="opd-note">
|
||||
Showing 5 of {visible.length} waiting players
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</StyledDialog>
|
||||
);
|
||||
};
|
||||
|
||||
const StyledDialog = styled(Dialog)({
|
||||
'& .MuiDialog-paper': {
|
||||
background: '#07090d',
|
||||
backgroundImage: `
|
||||
linear-gradient(rgba(35, 111, 135, 0.08) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(35, 111, 135, 0.08) 1px, transparent 1px)
|
||||
`,
|
||||
backgroundSize: '46px 46px',
|
||||
border: '1px solid rgba(35, 111, 135, 0.4)',
|
||||
borderRadius: '12px',
|
||||
boxShadow: '0 0 80px rgba(35, 111, 135, 0.15), 0 32px 80px rgba(0, 0, 0, 0.9)',
|
||||
width: '500px',
|
||||
maxWidth: '94vw',
|
||||
overflow: 'hidden',
|
||||
color: '#fff',
|
||||
},
|
||||
'& .MuiBackdrop-root': {
|
||||
background: 'rgba(2, 4, 8, 0.88)',
|
||||
backdropFilter: 'blur(4px)',
|
||||
},
|
||||
});
|
||||
|
||||
export default OnlinePlayersDialog;
|
||||
|
||||
OnlinePlayersDialog.propTypes = {
|
||||
open: bool.isRequired,
|
||||
onClose: func.isRequired,
|
||||
currentGameAssoc: string,
|
||||
isEnvDev: bool,
|
||||
};
|
||||
104
assets/js/mine-seeker/components/WaitingOverlayContent.jsx
Normal file
104
assets/js/mine-seeker/components/WaitingOverlayContent.jsx
Normal file
@@ -0,0 +1,104 @@
|
||||
/**
|
||||
* This file is part of the SplendidBear Websites' projects.
|
||||
*
|
||||
* Copyright (c) 2026 @ www.splendidbear.org
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
import { Fragment, useState } from 'react';
|
||||
import { OnlinePlayersDialog } from '@mine-components';
|
||||
import { bool, string } from 'prop-types';
|
||||
|
||||
const WaitingOverlayContent = ({ shareUrl, currentGameAssoc, opponentName = '', inviteOnly = false }) => {
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
const inviteHeader = inviteOnly && opponentName
|
||||
? `Invite ${opponentName}`
|
||||
: 'Invite a Friend';
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<div className={`waiting-options${inviteOnly ? ' waiting-options--invite-only' : ''}`}>
|
||||
<div className="waiting-option">
|
||||
<div className="waiting-option-header">
|
||||
<i className="fa fa-link" />
|
||||
<span>{inviteHeader}</span>
|
||||
</div>
|
||||
<p className="waiting-option-desc">Share this link with your opponent</p>
|
||||
<ShareLinkBox
|
||||
url={shareUrl}
|
||||
/>
|
||||
</div>
|
||||
{!inviteOnly && (
|
||||
<Fragment>
|
||||
<div className="waiting-divider">
|
||||
<span>OR</span>
|
||||
</div>
|
||||
<div className="waiting-option">
|
||||
<div className="waiting-option-header">
|
||||
<i className="fa fa-users" />
|
||||
<span>Challenge a Player</span>
|
||||
</div>
|
||||
<p className="waiting-option-desc">Browse online players and challenge them</p>
|
||||
<button
|
||||
className="browse-players-btn"
|
||||
onClick={() => setDialogOpen(true)}
|
||||
>
|
||||
<i className="fa fa-search" />
|
||||
Browse Players
|
||||
</button>
|
||||
</div>
|
||||
</Fragment>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!inviteOnly && (
|
||||
<OnlinePlayersDialog
|
||||
open={dialogOpen}
|
||||
onClose={() => setDialogOpen(false)}
|
||||
currentGameAssoc={currentGameAssoc}
|
||||
/>
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
const ShareLinkBox = ({ url }) => {
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
const handleCopy = () => {
|
||||
navigator.clipboard.writeText(url)
|
||||
.then(() => {
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2500);
|
||||
})
|
||||
.catch(() => null);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="share-invite">
|
||||
<div className="share-url-box" onClick={e => e.currentTarget.querySelector('input').select()}>
|
||||
<i className="fa fa-link share-url-icon" />
|
||||
<input
|
||||
className="share-url-input"
|
||||
readOnly
|
||||
value={url}
|
||||
onClick={e => e.target.select()}
|
||||
/>
|
||||
</div>
|
||||
<button className={`share-copy-btn${copied ? ' copied' : ''}`} onClick={handleCopy}>
|
||||
<i className={`fa ${copied ? 'fa-check' : 'fa-clipboard'}`} />
|
||||
{copied ? 'Copied!' : 'Copy Link'}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default WaitingOverlayContent;
|
||||
|
||||
WaitingOverlayContent.propTypes = {
|
||||
shareUrl: string.isRequired,
|
||||
currentGameAssoc: string,
|
||||
opponentName: string,
|
||||
inviteOnly: bool,
|
||||
};
|
||||
123
assets/js/mine-seeker/components/grid/GridControl.jsx
Normal file
123
assets/js/mine-seeker/components/grid/GridControl.jsx
Normal file
@@ -0,0 +1,123 @@
|
||||
/**
|
||||
* This file is part of the SplendidBear Websites' projects.
|
||||
*
|
||||
* Copyright (c) 2026 @ www.splendidbear.org
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
import React, { Fragment, useState } from 'react';
|
||||
import { useGame } from '@mine-contexts';
|
||||
import GridField from './GridField';
|
||||
import UserControl from '../user/UserControl';
|
||||
import GameTimer from '../GameTimer';
|
||||
import { BOMB_SYMBOLS, bombRadius } from '@mine-utils';
|
||||
import { func, string } from 'prop-types';
|
||||
|
||||
const GridControl = ({ gameAssoc, onClick, resign }) => {
|
||||
const {
|
||||
overlay, overlayTitle, overlaySubTitle,
|
||||
webPlayer, activePlayer, bombSelected,
|
||||
cells, setCells, endRef,
|
||||
} = useGame();
|
||||
|
||||
const [copied, setCopied] = useState(false);
|
||||
const shareUrl = gameAssoc ? `${window.location.origin}/play/${gameAssoc}` : null;
|
||||
const endShareUrl = gameAssoc ? `${window.location.origin}/battle/${gameAssoc}` : null;
|
||||
const isAuthenticated = '1' === document.getElementById('mine-wrapper')?.dataset.isAuthenticated;
|
||||
|
||||
const handleShare = () => {
|
||||
const url = endRef.current ? endShareUrl : shareUrl;
|
||||
if (!url) return;
|
||||
navigator.clipboard.writeText(url).then(() => {
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2200);
|
||||
});
|
||||
};
|
||||
|
||||
const handleHover = (row, col) => {
|
||||
if (!bombSelected) return;
|
||||
const activeColor = activePlayer ? 'blue' : 'red';
|
||||
if (activeColor !== webPlayer) return;
|
||||
|
||||
setCells(prev => {
|
||||
const next = prev.map(r => r.map(c =>
|
||||
null !== c.bombTargetArea ? { ...c, bombTargetArea: null } : c,
|
||||
));
|
||||
bombRadius(row, col, prev.length, prev[0]?.length ?? 0).forEach(([r, c], i) => {
|
||||
if (!next[r]?.[c]) return;
|
||||
|
||||
next[r] = [...next[r]];
|
||||
next[r][c] = { ...next[r][c], bombTargetArea: BOMB_SYMBOLS[i] };
|
||||
});
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<GameTimer />
|
||||
<div className="game-wrapper">
|
||||
<div className={`game-overlay${overlay ? '' : ' hide'}`}>
|
||||
<div className="game-overlay-window">
|
||||
<h1>{overlayTitle}</h1>
|
||||
{'string' === typeof overlaySubTitle && (
|
||||
<h2>{overlaySubTitle}</h2>
|
||||
)}
|
||||
{'string' !== typeof overlaySubTitle && (
|
||||
<Fragment>{overlaySubTitle}</Fragment>
|
||||
)}
|
||||
{gameAssoc && endRef.current && (
|
||||
<div className="game-overlay-actions">
|
||||
<button
|
||||
className={`game-overlay-share${copied ? ' copied' : ''}`}
|
||||
onClick={handleShare}
|
||||
title="Copy share link"
|
||||
aria-label="Copy share link"
|
||||
>
|
||||
<i className={`fa ${copied ? 'fa-check' : 'fa-share-alt'}`} />
|
||||
{copied ? 'Copied!' : 'Share Battle'}
|
||||
</button>
|
||||
<a
|
||||
className="game-overlay-profile"
|
||||
href={isAuthenticated ? '/profile' : '/'}
|
||||
title={isAuthenticated ? 'Go to your profile' : 'Go to homepage'}
|
||||
aria-label={isAuthenticated ? 'Go to your profile' : 'Go to homepage'}
|
||||
>
|
||||
<i className={`fa ${isAuthenticated ? 'fa-user' : 'fa-house'}`} />
|
||||
{isAuthenticated ? 'My Profile' : 'Homepage'}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<UserControl
|
||||
resign={resign}
|
||||
/>
|
||||
<div className="grid-container">
|
||||
<div className="grid">
|
||||
{cells.flatMap((row, r) =>
|
||||
row.map((cell, c) => (
|
||||
<GridField
|
||||
key={`${r}_${c}`}
|
||||
cell={cell}
|
||||
onClick={() => onClick([r, c])}
|
||||
onMouseEnter={() => handleHover(r, c)}
|
||||
/>
|
||||
)),
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export default GridControl;
|
||||
|
||||
GridControl.propTypes = {
|
||||
gameAssoc: string,
|
||||
onClick: func.isRequired,
|
||||
resign: func.isRequired,
|
||||
};
|
||||
91
assets/js/mine-seeker/components/grid/GridField.jsx
Normal file
91
assets/js/mine-seeker/components/grid/GridField.jsx
Normal file
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* This file is part of the SplendidBear Websites' projects.
|
||||
*
|
||||
* Copyright (c) 2026 @ www.splendidbear.org
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
import React, { memo, useMemo } from 'react';
|
||||
import { IMAGES } from '@mine-utils';
|
||||
import { func, shape, bool, number, string } from 'prop-types';
|
||||
|
||||
const bombSrc = area => {
|
||||
if (null === area) return null;
|
||||
const vert = ['left', 'center', 'right'][area[0]] ?? null;
|
||||
const hor = ['top', 'middle', 'bottom'][area[1]] ?? null;
|
||||
if (null === vert || null === hor) return IMAGES.bombEmpty;
|
||||
return IMAGES.bombPos(hor, vert);
|
||||
};
|
||||
|
||||
const GridField = memo(function GridField({ cell, onClick, onMouseEnter }) {
|
||||
const { currentImage, currentObj, active, lastClickedRed, lastClickedBlue, bombTargetArea } = cell;
|
||||
|
||||
const fieldClass = 'field'
|
||||
+ (active ? ' active' : '')
|
||||
+ (active && 'm' === currentObj ? ' mine' : '')
|
||||
+ ' color-' + currentObj;
|
||||
const bombSourceString = useMemo(() => bombSrc(bombTargetArea), [bombTargetArea]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="field-wrapper"
|
||||
onClick={onClick}
|
||||
onMouseEnter={onMouseEnter}
|
||||
>
|
||||
<img
|
||||
className="field-target"
|
||||
src={IMAGES.target}
|
||||
alt="Field of target"
|
||||
/>
|
||||
{bombSourceString && (
|
||||
<img
|
||||
className="field-bomb-target"
|
||||
src={bombSourceString}
|
||||
alt="Field of bomb target"
|
||||
/>
|
||||
)}
|
||||
{(lastClickedRed || lastClickedBlue) && (
|
||||
<img
|
||||
className={`field-${lastClickedRed ? 'red' : 'blue'}-last last-clicked`}
|
||||
src={IMAGES.last(lastClickedRed ? 'red' : 'blue')}
|
||||
alt="Last clicked area"
|
||||
/>
|
||||
)}
|
||||
<div className={fieldClass}>
|
||||
<div
|
||||
style={{ background: "url('/images/bg-corner-outbg.png') no-repeat top left / 100% 100%" }}
|
||||
className="field-corner"
|
||||
>
|
||||
{isNaN(currentImage) && (
|
||||
<div className="flag-mine">
|
||||
<img src={currentImage} alt="" />
|
||||
<div className="flag-mine-base" />
|
||||
</div>
|
||||
)}
|
||||
{!isNaN(currentImage) && 0 !== currentImage && (
|
||||
<div className="flag-number">
|
||||
{currentImage}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export default GridField;
|
||||
|
||||
GridField.propTypes = {
|
||||
cell: shape({
|
||||
currentImage: string,
|
||||
currentObj: string,
|
||||
active: bool,
|
||||
lastClickedRed: bool,
|
||||
lastClickedBlue: bool,
|
||||
bombTargetArea: number,
|
||||
}).isRequired,
|
||||
onClick: func.isRequired,
|
||||
onMouseEnter: func.isRequired,
|
||||
};
|
||||
22
assets/js/mine-seeker/components/index.js
Normal file
22
assets/js/mine-seeker/components/index.js
Normal file
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* This file is part of the SplendidBear Websites' projects.
|
||||
*
|
||||
* Copyright (c) 2026 @ www.splendidbear.org
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
export { GameBoard } from './GameBoard';
|
||||
export { default as OnlinePlayersDialog } from './OnlinePlayersDialog';
|
||||
export { default as WaitingOverlayContent } from './WaitingOverlayContent';
|
||||
export { default as ChallengeCountdown } from './ChallengeCountdown';
|
||||
export { default as GameTimer } from './GameTimer';
|
||||
export { default as GridControl } from './grid/GridControl';
|
||||
export { default as GridField } from './grid/GridField';
|
||||
export { default as User } from './user/User';
|
||||
export { default as UserControl } from './user/UserControl';
|
||||
export { default as BonusBox } from './BonusBox';
|
||||
export { default as BonusStatsDialog } from './BonusStatsDialog';
|
||||
export { Avatar } from './timer/Avatar';
|
||||
export { PlayerColumn } from './profile/PlayerColumn';
|
||||
52
assets/js/mine-seeker/components/profile/PlayerColumn.jsx
Normal file
52
assets/js/mine-seeker/components/profile/PlayerColumn.jsx
Normal file
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* This file is part of the SplendidBear Websites' projects.
|
||||
*
|
||||
* Copyright (c) 2026 @ www.splendidbear.org
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { object, string } from 'prop-types';
|
||||
import { BONUS_LABELS } from '@mine-utils';
|
||||
|
||||
const formatPlayerName = name => {
|
||||
if (name && name.startsWith('anon_')) {
|
||||
return 'Anonymous';
|
||||
}
|
||||
|
||||
if (name && 10 < name.length) {
|
||||
return name.substring(0, 7) + '...';
|
||||
}
|
||||
|
||||
return name || 'Unknown';
|
||||
};
|
||||
|
||||
export const PlayerColumn = ({ color, player }) => (
|
||||
<div className={`bsd-column bsd-column--${color}`}>
|
||||
<div className="bsd-column-header">
|
||||
<span className="bsd-column-name">{formatPlayerName(player.name)}</span>
|
||||
<span className="bsd-column-total">
|
||||
<i className="fa fa-star" />
|
||||
{player.bonusPoints}
|
||||
</span>
|
||||
</div>
|
||||
<ul className="bsd-stats">
|
||||
{Object.entries(BONUS_LABELS).map(([key, { label, desc }]) => (
|
||||
<li key={key} className="bsd-stat">
|
||||
<div className="bsd-stat-text">
|
||||
<span className="bsd-stat-label">{label}</span>
|
||||
<span className="bsd-stat-desc">{desc}</span>
|
||||
</div>
|
||||
<span className="bsd-stat-value">{player.bonusStats?.[key] ?? 0}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
|
||||
PlayerColumn.propTypes = {
|
||||
color: string.isRequired,
|
||||
player: object.isRequired,
|
||||
};
|
||||
28
assets/js/mine-seeker/components/timer/Avatar.jsx
Normal file
28
assets/js/mine-seeker/components/timer/Avatar.jsx
Normal file
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* This file is part of the SplendidBear Websites' projects.
|
||||
*
|
||||
* Copyright (c) 2026 @ www.splendidbear.org
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { object } from 'prop-types';
|
||||
|
||||
export const Avatar = ({ player }) => {
|
||||
if (!player.registered) return '';
|
||||
|
||||
return (
|
||||
<div className="timer-avatar">
|
||||
{player.avatar
|
||||
? <img src={player.avatar} alt={player.name} className="timer-avatar__img" />
|
||||
: <span className="timer-avatar__initials">{player.name.slice(0, 2).toUpperCase()}</span>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Avatar.propTypes = {
|
||||
player: object.isRequired,
|
||||
};
|
||||
67
assets/js/mine-seeker/components/user/User.jsx
Normal file
67
assets/js/mine-seeker/components/user/User.jsx
Normal file
@@ -0,0 +1,67 @@
|
||||
/**
|
||||
* This file is part of the SplendidBear Websites' projects.
|
||||
*
|
||||
* Copyright (c) 2026 @ www.splendidbear.org
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
import React, { memo } from 'react';
|
||||
import { IMAGES } from '@mine-utils';
|
||||
import { bool, func, number, string } from 'prop-types';
|
||||
|
||||
const User = memo(function User(
|
||||
{
|
||||
color,
|
||||
webPlayer,
|
||||
name,
|
||||
desc,
|
||||
active,
|
||||
mines,
|
||||
haveBomb,
|
||||
enabledBomb,
|
||||
onClickBombSelector,
|
||||
},
|
||||
) {
|
||||
const buzzClass = 'bomb-container'
|
||||
+ (active && color === webPlayer && haveBomb && enabledBomb ? ' buzz' : '');
|
||||
|
||||
return (
|
||||
<div className={`user-container user-${color}`}>
|
||||
<div className="user-header">
|
||||
<div className="user-color">{color}</div>
|
||||
{active && <img src={IMAGES.cursor(color)} alt="" className="user-cursor" />}
|
||||
<img src={IMAGES.figure(color)} alt="" />
|
||||
</div>
|
||||
<div className="user-name"> {name} </div>
|
||||
<div className="user-caret"><i className="fa fa-caret-down" /></div>
|
||||
<div className="user-desc"> {desc} </div>
|
||||
<div className="user-control">
|
||||
<img src={IMAGES.flag(color)} alt="" />
|
||||
<div className="user-control-mines">{mines}</div>
|
||||
<div className={buzzClass} onClick={onClickBombSelector}>
|
||||
<div className="bomb">
|
||||
{haveBomb && <img src={enabledBomb && active ? IMAGES.bomb : IMAGES.bombDisabled} alt="" />}
|
||||
{!haveBomb && <img src={IMAGES.bombExploded} alt="" />}
|
||||
</div>
|
||||
</div>
|
||||
<div className="clear" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export default User;
|
||||
|
||||
User.propTypes = {
|
||||
color: string.isRequired,
|
||||
webPlayer: string,
|
||||
name: string,
|
||||
desc: string,
|
||||
active: bool,
|
||||
mines: number,
|
||||
haveBomb: bool,
|
||||
enabledBomb: bool,
|
||||
onClickBombSelector: func.isRequired,
|
||||
};
|
||||
76
assets/js/mine-seeker/components/user/UserControl.jsx
Normal file
76
assets/js/mine-seeker/components/user/UserControl.jsx
Normal file
@@ -0,0 +1,76 @@
|
||||
/**
|
||||
* This file is part of the SplendidBear Websites' projects.
|
||||
*
|
||||
* Copyright (c) 2026 @ www.splendidbear.org
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
import React, { Fragment, useState } from 'react';
|
||||
import { useGame } from '@mine-contexts';
|
||||
import User from './User';
|
||||
import BonusStatsDialog from '../BonusStatsDialog';
|
||||
import { func } from 'prop-types';
|
||||
|
||||
const UserControl = ({ resign }) => {
|
||||
const { webPlayer, activePlayer, foundMines, red, blue, onBombToggle } = useGame();
|
||||
const [bonusDialogOpen, setBonusDialogOpen] = useState(false);
|
||||
const activeColor = activePlayer ? 'blue' : 'red';
|
||||
const resignClass = 'resign' + (activeColor !== webPlayer ? ' disabled' : '');
|
||||
const minesClass = 'active-mines' + (foundMines ? ' found-mine' : '');
|
||||
const remainingMines = 51 - red.mines - blue.mines;
|
||||
|
||||
const handleBombClick = (color, player) => {
|
||||
const p = 'red' === color ? red : blue;
|
||||
if (p.haveBomb && p.enabledBomb && activePlayer === player) {
|
||||
onBombToggle();
|
||||
}
|
||||
};
|
||||
|
||||
const handleBonusClick = () => {
|
||||
setBonusDialogOpen(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<div className="users">
|
||||
<User
|
||||
color="blue" webPlayer={webPlayer} {...blue}
|
||||
onClickBombSelector={() => handleBombClick('blue', 1)}
|
||||
onBonusClick={handleBonusClick}
|
||||
/>
|
||||
<div className="active-mines-container">
|
||||
<i className="fa fa-star" />
|
||||
<div className={minesClass}>
|
||||
<div className="active-mines-nbr">{remainingMines}</div>
|
||||
<div className="active-mines-shine" />
|
||||
</div>
|
||||
<i className="fa fa-star" />
|
||||
</div>
|
||||
<div className="clear" />
|
||||
<User
|
||||
color="red" webPlayer={webPlayer} {...red}
|
||||
onClickBombSelector={() => handleBombClick('red', 0)}
|
||||
onBonusClick={handleBonusClick}
|
||||
/>
|
||||
<button className={resignClass} onClick={resign}>
|
||||
<div className="resign-shine" />
|
||||
Resign
|
||||
</button>
|
||||
</div>
|
||||
<BonusStatsDialog
|
||||
open={bonusDialogOpen}
|
||||
onClose={() => setBonusDialogOpen(false)}
|
||||
red={red}
|
||||
blue={blue}
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
export default UserControl;
|
||||
|
||||
UserControl.propTypes = {
|
||||
resign: func.isRequired,
|
||||
};
|
||||
16
assets/js/mine-seeker/contexts/GameContext.jsx
Normal file
16
assets/js/mine-seeker/contexts/GameContext.jsx
Normal file
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* This file is part of the SplendidBear Websites' projects.
|
||||
*
|
||||
* Copyright (c) 2026 @ www.splendidbear.org
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
import { createContext, useContext } from 'react';
|
||||
|
||||
const GameContext = createContext(null);
|
||||
|
||||
export const useGame = () => useContext(GameContext);
|
||||
|
||||
export default GameContext;
|
||||
280
assets/js/mine-seeker/contexts/GameProvider.jsx
Normal file
280
assets/js/mine-seeker/contexts/GameProvider.jsx
Normal file
@@ -0,0 +1,280 @@
|
||||
/**
|
||||
* This file is part of the SplendidBear Websites' projects.
|
||||
*
|
||||
* Copyright (c) 2026 @ www.splendidbear.org
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
import React, { useRef } from 'react';
|
||||
import { Howl } from 'howler';
|
||||
import GameContext from './GameContext';
|
||||
import { useGameRefs, useGameState } from '@mine-hooks';
|
||||
import { DESC, IMAGES, patchCells } from '@mine-utils';
|
||||
|
||||
export const GameProvider = ({ children }) => {
|
||||
const {
|
||||
webPlayerRef,
|
||||
activePlayerRef,
|
||||
bombSelectedRef,
|
||||
connectionLostRef,
|
||||
redRef,
|
||||
blueRef,
|
||||
lastClickedRef,
|
||||
endRef,
|
||||
} = useGameRefs();
|
||||
|
||||
const {
|
||||
webPlayer, setWebPlayer,
|
||||
activePlayer, setActivePlayer,
|
||||
overlay, setOverlay,
|
||||
overlayTitle, setOverlayTitle,
|
||||
overlaySubTitle, setOverlaySubTitle,
|
||||
mines, setMines,
|
||||
bombSelected, setBombSelected,
|
||||
foundMines, setFoundMines,
|
||||
red, setRed,
|
||||
blue, setBlue,
|
||||
cells, setCells,
|
||||
gridReady, setGridReady,
|
||||
connectionLost, setConnectionLost,
|
||||
} = useGameState();
|
||||
|
||||
const [gameUuid, setGameUuid] = React.useState(null);
|
||||
|
||||
const sounds = useRef({
|
||||
click: new Howl({ src: ['/sound/click.mp3'] }),
|
||||
bomb: new Howl({ src: ['/sound/bomb.mp3'] }),
|
||||
mine: new Howl({ src: ['/sound/mine.mp3'] }),
|
||||
warning: new Howl({ src: ['/sound/warning.mp3'] }),
|
||||
won: new Howl({ src: ['/sound/won.mp3'] }),
|
||||
starting: new Howl({ src: ['/sound/starting.mp3'] }),
|
||||
});
|
||||
|
||||
/** Sync helpers (keep ref + state in lockstep) */
|
||||
|
||||
const syncWebPlayer = v => {
|
||||
webPlayerRef.current = v;
|
||||
setWebPlayer(v);
|
||||
};
|
||||
const syncActivePlayer = v => {
|
||||
activePlayerRef.current = v;
|
||||
setActivePlayer(v);
|
||||
};
|
||||
const syncBombSelected = v => {
|
||||
bombSelectedRef.current = v;
|
||||
setBombSelected(v);
|
||||
};
|
||||
const syncConnLost = v => {
|
||||
connectionLostRef.current = v;
|
||||
setConnectionLost(v);
|
||||
};
|
||||
const syncRed = fn => {
|
||||
const n = fn(redRef.current);
|
||||
redRef.current = n;
|
||||
setRed(n);
|
||||
};
|
||||
const syncBlue = fn => {
|
||||
const n = fn(blueRef.current);
|
||||
blueRef.current = n;
|
||||
setBlue(n);
|
||||
};
|
||||
|
||||
/** Overlay */
|
||||
|
||||
const showOverlay = (title, sub) => {
|
||||
setOverlay(true);
|
||||
setOverlayTitle(title);
|
||||
setOverlaySubTitle(sub);
|
||||
};
|
||||
|
||||
const hideOverlay = () => setOverlay(false);
|
||||
|
||||
/** Cell helpers */
|
||||
|
||||
const applyRevealedCell = (cell, player, isMainCell = false) => {
|
||||
const { row, col, value } = cell;
|
||||
setCells(prev => {
|
||||
if (prev[row][col].active) return prev;
|
||||
const patch = 'm' === value
|
||||
? { currentImage: IMAGES.flag(player), currentObj: 'm', active: true }
|
||||
: { currentImage: value, currentObj: value, active: true };
|
||||
if (isMainCell) {
|
||||
patch.lastClickedRed = 'red' === player;
|
||||
patch.lastClickedBlue = 'blue' === player;
|
||||
}
|
||||
return patchCells(prev, [{ row, col, ...patch }]);
|
||||
});
|
||||
if (isMainCell) lastClickedRef.current = { ...lastClickedRef.current, [player]: [row, col] };
|
||||
};
|
||||
|
||||
const showLeftMines = (leftMines = []) => {
|
||||
if (!leftMines.length) return;
|
||||
setCells(prev => {
|
||||
const patches = leftMines
|
||||
.filter(({ row, col }) => !prev[row][col].active)
|
||||
.map(({ row, col }) => ({ row, col, currentImage: IMAGES.leftMine }));
|
||||
return patches.length ? patchCells(prev, patches) : prev;
|
||||
});
|
||||
};
|
||||
|
||||
/** Game logic */
|
||||
|
||||
const changePlayer = () => {
|
||||
const wasColor = activePlayerRef.current ? 'blue' : 'red';
|
||||
const nextColor = activePlayerRef.current ? 'red' : 'blue';
|
||||
const nextVal = activePlayerRef.current ? 0 : 1;
|
||||
const desc = wasColor === webPlayerRef.current ? DESC.buddy : DESC.you;
|
||||
|
||||
syncActivePlayer(nextVal);
|
||||
syncRed(p => ({ ...p, active: 'red' === nextColor, desc: 'red' === nextColor ? desc : '' }));
|
||||
syncBlue(p => ({ ...p, active: 'blue' === nextColor, desc: 'blue' === nextColor ? desc : '' }));
|
||||
};
|
||||
|
||||
const applyStep = stepData => {
|
||||
const {
|
||||
player,
|
||||
bomb: isBomb,
|
||||
minesFound = 0,
|
||||
revealedCells = [],
|
||||
redPoints: rp,
|
||||
bluePoints: bp,
|
||||
redBonusPoints = 0,
|
||||
blueBonusPoints = 0,
|
||||
redBonusStats = {},
|
||||
blueBonusStats = {},
|
||||
} = stepData;
|
||||
|
||||
if (isBomb) {
|
||||
sounds.current.bomb.play();
|
||||
} else if (0 < minesFound) {
|
||||
const cur = ('red' === player ? redRef : blueRef).current.mines;
|
||||
sounds.current[20 < cur + minesFound ? 'warning' : 'mine'].play();
|
||||
} else {
|
||||
sounds.current.click.play();
|
||||
}
|
||||
|
||||
const lc = lastClickedRef.current[player];
|
||||
setCells(prev => {
|
||||
let next = prev;
|
||||
if (lc) next = patchCells(next, [{ row: lc[0], col: lc[1], lastClickedRed: false, lastClickedBlue: false }]);
|
||||
|
||||
revealedCells.forEach(({ row, col, value }, idx) => {
|
||||
if (next[row][col].active) return;
|
||||
const patch = 'm' === value
|
||||
? { currentImage: IMAGES.flag(player), currentObj: 'm', active: true }
|
||||
: { currentImage: value, currentObj: value, active: true };
|
||||
if (0 === idx) {
|
||||
patch.lastClickedRed = 'red' === player;
|
||||
patch.lastClickedBlue = 'blue' === player;
|
||||
}
|
||||
next = patchCells(next, [{ row, col, ...patch }]);
|
||||
});
|
||||
|
||||
if (isBomb) next = next.map(r => r.map(c => null !== c.bombTargetArea ? { ...c, bombTargetArea: null } : c));
|
||||
return next;
|
||||
});
|
||||
|
||||
if (0 < revealedCells.length) {
|
||||
lastClickedRef.current = { ...lastClickedRef.current, [player]: [revealedCells[0].row, revealedCells[0].col] };
|
||||
}
|
||||
|
||||
if (0 < minesFound) {
|
||||
setMines(51 - rp - bp);
|
||||
setFoundMines(true);
|
||||
setTimeout(() => setFoundMines(false), 500);
|
||||
syncRed(p => ({ ...p, mines: 'red' === player ? rp : p.mines }));
|
||||
syncBlue(p => ({ ...p, mines: 'blue' === player ? bp : p.mines }));
|
||||
}
|
||||
|
||||
/** Update bonus points and stats */
|
||||
syncRed(p => ({
|
||||
...p,
|
||||
bonusPoints: 'red' === player ? redBonusPoints : p.bonusPoints,
|
||||
bonusStats: 'red' === player ? redBonusStats : p.bonusStats,
|
||||
}));
|
||||
syncBlue(p => ({
|
||||
...p,
|
||||
bonusPoints: 'blue' === player ? blueBonusPoints : p.bonusPoints,
|
||||
bonusStats: 'blue' === player ? blueBonusStats : p.bonusStats,
|
||||
}));
|
||||
|
||||
syncRed(p => ({ ...p, enabledBomb: rp <= bp }));
|
||||
syncBlue(p => ({ ...p, enabledBomb: bp <= rp }));
|
||||
|
||||
if (isBomb || 0 === minesFound) changePlayer();
|
||||
|
||||
if (isBomb) {
|
||||
syncBombSelected(false);
|
||||
syncRed(p => 'red' === player ? { ...p, haveBomb: false } : p);
|
||||
syncBlue(p => 'blue' === player ? { ...p, haveBomb: false } : p);
|
||||
}
|
||||
};
|
||||
|
||||
const makeGameEndIfItEnds = (bluePoints, redPoints, resign = false, leftMines = []) => {
|
||||
const redWins = 25 < redPoints;
|
||||
const blueWins = 25 < bluePoints;
|
||||
|
||||
if (redWins || blueWins || resign) {
|
||||
sounds.current.won.play();
|
||||
|
||||
if (!resign) {
|
||||
endRef.current = true;
|
||||
showOverlay((redWins ? 'Red' : 'Blue') + ' wins the game!', null);
|
||||
}
|
||||
|
||||
showLeftMines(leftMines);
|
||||
syncActivePlayer(false);
|
||||
syncRed(p => ({ ...p, desc: '' }));
|
||||
syncBlue(p => ({ ...p, desc: '' }));
|
||||
}
|
||||
};
|
||||
|
||||
const resignProcess = (color, uuid = null) => {
|
||||
const wp = webPlayerRef.current;
|
||||
if (uuid) {
|
||||
setGameUuid(uuid);
|
||||
}
|
||||
showOverlay(
|
||||
color === wp ? 'You have been give up' : 'Your opponent has been resigned',
|
||||
color === wp ? 'You LOSE!' : 'You WIN!',
|
||||
);
|
||||
endRef.current = true;
|
||||
makeGameEndIfItEnds(0, 0, true);
|
||||
};
|
||||
|
||||
const onBombToggle = () => {
|
||||
const next = !bombSelectedRef.current;
|
||||
syncBombSelected(next);
|
||||
if (!next) {
|
||||
setCells(prev => prev.map(r => r.map(c => null !== c.bombTargetArea ? { ...c, bombTargetArea: null } : c)));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<GameContext.Provider
|
||||
value={{
|
||||
/** State (for rendering) */
|
||||
webPlayer, activePlayer, overlay, overlayTitle, overlaySubTitle,
|
||||
mines, bombSelected, foundMines, red, blue, cells, gridReady, connectionLost, gameUuid,
|
||||
/** Setters needed by useServerComm */
|
||||
setCells, setGridReady, setGameUuid,
|
||||
/** Refs (needed by useServerComm for async-safe reads) */
|
||||
webPlayerRef, activePlayerRef, bombSelectedRef, connectionLostRef, lastClickedRef, endRef,
|
||||
/** Sync helpers */
|
||||
syncWebPlayer, syncActivePlayer, syncBombSelected, syncConnLost, syncRed, syncBlue,
|
||||
/** Game logic called by useServerComm */
|
||||
showOverlay, hideOverlay,
|
||||
applyRevealedCell, applyStep,
|
||||
makeGameEndIfItEnds, resignProcess,
|
||||
/** UI action */
|
||||
onBombToggle,
|
||||
/** Sounds */
|
||||
sounds,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</GameContext.Provider>
|
||||
);
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user