Compare commits
12 Commits
v2026.2.9-
...
userlist
| Author | SHA1 | Date | |
|---|---|---|---|
| ae8794f564 | |||
| 2f3062bf33 | |||
| 0e86f7d4aa | |||
| 8851a6d68b | |||
| 94ebe9a428 | |||
| 4a94855be9 | |||
| a8ac13850b | |||
| fe5378f69b | |||
| e8691f29bb | |||
| fcd490ef4e | |||
| a3465f6cf9 | |||
| a91172ef7a |
3
.babelrc
Normal file
3
.babelrc
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"presets": ["es2015", "es2016", "es2017", "react"]
|
||||||
|
}
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
.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
884
.editorconfig
@@ -1,884 +0,0 @@
|
|||||||
[*]
|
|
||||||
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
59
.env.dist
@@ -1,59 +0,0 @@
|
|||||||
# 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 ###
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
# define your env variables for the test env here
|
|
||||||
KERNEL_CLASS='App\Kernel'
|
|
||||||
APP_SECRET='$ecretf0rt3st'
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
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 = []
|
|
||||||
@@ -1,142 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,126 +0,0 @@
|
|||||||
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
|
|
||||||
58
.gitignore
vendored
58
.gitignore
vendored
@@ -1,21 +1,39 @@
|
|||||||
###> system7 - jotunheimr ###
|
/app/config/parameters.yml
|
||||||
compose.override.yaml
|
/build/
|
||||||
+bak/
|
|
||||||
.idea/
|
|
||||||
node_modules/
|
|
||||||
nohup.out
|
|
||||||
/public/build/
|
|
||||||
/config/reference.php
|
|
||||||
###< system7 - jotunheimr ###
|
|
||||||
|
|
||||||
###> symfony/framework-bundle ###
|
|
||||||
/.env
|
|
||||||
/public/bundles/
|
|
||||||
/var/
|
|
||||||
/vendor/
|
|
||||||
###< symfony/framework-bundle ###
|
|
||||||
|
|
||||||
###> phpunit/phpunit ###
|
|
||||||
/phpunit.xml
|
/phpunit.xml
|
||||||
/.phpunit.cache/
|
/var/*
|
||||||
###< phpunit/phpunit ###
|
!/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/
|
||||||
|
|
||||||
|
nohup.out
|
||||||
|
src/Mine/SeekerBundle/Resources/public/js/build/
|
||||||
|
npm-debug.log
|
||||||
|
|||||||
464
AGENTS.md
464
AGENTS.md
@@ -1,464 +0,0 @@
|
|||||||
# 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! 🚀**
|
|
||||||
552
CHANGELOG.md
552
CHANGELOG.md
@@ -1,552 +0,0 @@
|
|||||||
# Changelog
|
|
||||||
|
|
||||||
|
|
||||||
## v2026.2.8-3 (2026-04-22)
|
|
||||||
|
|
||||||
### Fix
|
|
||||||
|
|
||||||
* The error message cannot be seen during avatar changing #10. [Lang]
|
|
||||||
|
|
||||||
|
|
||||||
## v2026.2.8-2 (2026-04-21)
|
|
||||||
|
|
||||||
### Changes
|
|
||||||
|
|
||||||
* Increase the 2 MB avatar maximum file size to 10 MB #10. [Lang]
|
|
||||||
|
|
||||||
|
|
||||||
## v2026.2.8-1 (2026-04-21)
|
|
||||||
|
|
||||||
### Changes
|
|
||||||
|
|
||||||
* Upgrade front-end & back-end deps to the latest available version #10. [Lang]
|
|
||||||
|
|
||||||
|
|
||||||
## v2026.2.8-0 (2026-04-21)
|
|
||||||
|
|
||||||
### New
|
|
||||||
|
|
||||||
* Add CI/CD improvements - add new CI workflow - & improve the deployment w/ tests #10. [Lang]
|
|
||||||
|
|
||||||
* Add test cases to back-end w/ real database connection in it #10. [Lang]
|
|
||||||
|
|
||||||
### Changes
|
|
||||||
|
|
||||||
* The original CI/CD workflow is restored - the work with tests is postponed #10. [Lang]
|
|
||||||
|
|
||||||
* Add sound when the game start #10. [Lang]
|
|
||||||
|
|
||||||
* Create AGENTS.md file for future maintenance #9. [Lang]
|
|
||||||
|
|
||||||
* Remove the wrongly implemented font installation in docker - & replace it with static font on BattleCardGenerator (it solves the shareable image problem on bare-metal too) #8. [Lang]
|
|
||||||
|
|
||||||
|
|
||||||
## v2026.2.7-1 (2026-04-21)
|
|
||||||
|
|
||||||
### Changes
|
|
||||||
|
|
||||||
* Fine-tune the recent battle list #8. [Lang]
|
|
||||||
|
|
||||||
|
|
||||||
## v2026.2.7-0 (2026-04-21)
|
|
||||||
|
|
||||||
### Changes
|
|
||||||
|
|
||||||
* Small changes on docs - and improve text on homepage #8. [Lang]
|
|
||||||
|
|
||||||
* Massive refactor on front-end for unification and readiness #8. [Lang]
|
|
||||||
|
|
||||||
* Update all doc blocks on back-end #8. [Lang]
|
|
||||||
|
|
||||||
* Small refactors on back-end #8. [Lang]
|
|
||||||
|
|
||||||
* Add RecentBattle entity that is a Materialized View to speed up the view - and further refactor on ProfileController #8. [Lang]
|
|
||||||
|
|
||||||
* Create the UserStats entity what is a Materialized View to store Profile stats for every user - & massive ProfileController refactor #8. [Lang]
|
|
||||||
|
|
||||||
* Refactor the SecurityController #7. [Lang]
|
|
||||||
|
|
||||||
* Refactor the code - there was unnecessary codes and wrongly formatted or designed code that are related to Repositories #7. [Lang]
|
|
||||||
|
|
||||||
* Upgrade the doctrine related back-end pkgs to the latest available version #7. [Lang]
|
|
||||||
|
|
||||||
* Add filter to the Profile page's recent plays and an infite list too #7. [Lang]
|
|
||||||
|
|
||||||
* Upgrade to the latest doctrine pkg on back-end #7. [Lang]
|
|
||||||
|
|
||||||
### Fix
|
|
||||||
|
|
||||||
* Do not hide the end-game overlay ever #8. [Lang]
|
|
||||||
|
|
||||||
* The username was not recognized properly #7. [Lang]
|
|
||||||
|
|
||||||
|
|
||||||
## v2026.2.6-1 (2026-04-19)
|
|
||||||
|
|
||||||
### Fix
|
|
||||||
|
|
||||||
* The PostgreSQL logo was horrible #7. [Lang]
|
|
||||||
|
|
||||||
|
|
||||||
## v2026.2.6-0 (2026-04-19)
|
|
||||||
|
|
||||||
### Changes
|
|
||||||
|
|
||||||
* Add ReCaptcha overlay again to protect the game #7. [Lang]
|
|
||||||
|
|
||||||
* Upgrade all front-end packages to the latest available version - and fix all eslint warnings & errors #7. [Lang]
|
|
||||||
|
|
||||||
* Upgrade fe packages #7. [Lang]
|
|
||||||
|
|
||||||
* Massive refactor on fetches - create centralized dataProvider #7. [Lang]
|
|
||||||
|
|
||||||
|
|
||||||
## v2026.2.5-0 (2026-04-19)
|
|
||||||
|
|
||||||
### New
|
|
||||||
|
|
||||||
* Add Firebase deps to back-end #7. [Lang]
|
|
||||||
|
|
||||||
* Add missing buttons for overlays #7. [Lang]
|
|
||||||
|
|
||||||
* A new feature came up - the abandoned plays can be restored, if both users are registered users #7. [Lang]
|
|
||||||
|
|
||||||
### Changes
|
|
||||||
|
|
||||||
* Fix the '0' in Battle reports #6. [Lang]
|
|
||||||
|
|
||||||
* Fix missing icons on "Battle report" #6. [Lang]
|
|
||||||
|
|
||||||
### Fix
|
|
||||||
|
|
||||||
* The bomb using was not recorded correctly - the old data will be corrupted #6. [Lang]
|
|
||||||
|
|
||||||
|
|
||||||
## v2026.2.4-0 (2026-04-18)
|
|
||||||
|
|
||||||
### New
|
|
||||||
|
|
||||||
* Add new profile charts and stats - & add new logo to the tech stack #5. [Lang]
|
|
||||||
|
|
||||||
### Changes
|
|
||||||
|
|
||||||
* Upgrade fe deps #5. [Lang]
|
|
||||||
|
|
||||||
* Improve the Battle reports to change unnecessary data with interesting data #5. [Lang]
|
|
||||||
|
|
||||||
### Fix
|
|
||||||
|
|
||||||
* The react is crashing on some cases #5. [Lang]
|
|
||||||
|
|
||||||
|
|
||||||
## v2026.2.3-0 (2026-04-18)
|
|
||||||
|
|
||||||
### New
|
|
||||||
|
|
||||||
* Add initialization bonus points' system to the gameplay #5. [Lang]
|
|
||||||
|
|
||||||
### Changes
|
|
||||||
|
|
||||||
* Add extended data to battle reports and sharing image to make viewable bonus points #5. [Lang]
|
|
||||||
|
|
||||||
|
|
||||||
## v2026.2.2-9 (2026-04-18)
|
|
||||||
|
|
||||||
### New
|
|
||||||
|
|
||||||
* Add rules page #4. [Lang]
|
|
||||||
|
|
||||||
### Fix
|
|
||||||
|
|
||||||
* The font-awesome simplifying to work on bare-metal - & fix all warnings at build time #4. [Lang]
|
|
||||||
|
|
||||||
* The css problem had been solved on reponsive gfx on homepage #4. [Lang]
|
|
||||||
|
|
||||||
|
|
||||||
## v2026.2.1-8 (2026-04-18)
|
|
||||||
|
|
||||||
### Fix
|
|
||||||
|
|
||||||
* Quickfix for https-only login - & add user data when the user is not logged in #4. [Lang]
|
|
||||||
|
|
||||||
|
|
||||||
## v2026.2.1-7 (2026-04-16)
|
|
||||||
|
|
||||||
### Changes
|
|
||||||
|
|
||||||
* Add consent checkbox to user's registration - and fix the sharing pics #4. [Lang]
|
|
||||||
|
|
||||||
* Add correct version numbering and CHANGELOG - and add the LICENSE #4. [Lang]
|
|
||||||
|
|
||||||
|
|
||||||
## v2026.2.1-6 (2026-04-16)
|
|
||||||
|
|
||||||
### Changes
|
|
||||||
|
|
||||||
* Update all texts on all pages - extend them with the game specific things #4. [Lang]
|
|
||||||
|
|
||||||
|
|
||||||
## v2026.2.1-5 (2026-04-16)
|
|
||||||
|
|
||||||
### Fix
|
|
||||||
|
|
||||||
* The meta tags does not have https scheme - nothing worked in configuration #4. [Lang]
|
|
||||||
|
|
||||||
|
|
||||||
## v2026.2.1-4 (2026-04-15)
|
|
||||||
|
|
||||||
### New
|
|
||||||
|
|
||||||
* Add notification email when a user is registered #4. [Lang]
|
|
||||||
|
|
||||||
### Changes
|
|
||||||
|
|
||||||
* Add notification on activation too #4. [Lang]
|
|
||||||
|
|
||||||
|
|
||||||
## v2026.2.1-2 (2026-04-15)
|
|
||||||
|
|
||||||
### Fix
|
|
||||||
|
|
||||||
* Another attempt to fix the email assets #4. [Lang]
|
|
||||||
|
|
||||||
|
|
||||||
## v2026.2.1-1 (2026-04-15)
|
|
||||||
|
|
||||||
### Fix
|
|
||||||
|
|
||||||
* The images does not shows in emails #4. [Lang]
|
|
||||||
|
|
||||||
|
|
||||||
## v2026.2.1-0 (2026-04-15)
|
|
||||||
|
|
||||||
### New
|
|
||||||
|
|
||||||
* Add Contact page with email sending behaviour #4. [Lang]
|
|
||||||
|
|
||||||
### Changes
|
|
||||||
|
|
||||||
* Add missing .env variable and increase the version number and add missing data from front-end and back-end deps descriptor #4. [Lang]
|
|
||||||
|
|
||||||
* Change the shareable battle - add avatars to it - even on the og tags #4. [Lang]
|
|
||||||
|
|
||||||
* Change text #4. [Lang]
|
|
||||||
|
|
||||||
### Fix
|
|
||||||
|
|
||||||
* The mailhog is crashed on development env #4. [Lang]
|
|
||||||
|
|
||||||
* The og tags did not have proper http schema - they should have https #4. [Lang]
|
|
||||||
|
|
||||||
|
|
||||||
## v2026.2.0-5 (2026-04-14)
|
|
||||||
|
|
||||||
### Changes
|
|
||||||
|
|
||||||
* Add donation button #4. [Lang]
|
|
||||||
|
|
||||||
|
|
||||||
## v2026.2.0-4 (2026-04-14)
|
|
||||||
|
|
||||||
### New
|
|
||||||
|
|
||||||
* Add timer for the acceptance of the challenge #4. [Lang]
|
|
||||||
|
|
||||||
### Changes
|
|
||||||
|
|
||||||
* Protect the gameplay with recaptcha #4. [Lang]
|
|
||||||
|
|
||||||
* The waiting dialog is uncloseable until the time is up #4. [Lang]
|
|
||||||
|
|
||||||
* Add share button to the overlay when the game ends #4. [Lang]
|
|
||||||
|
|
||||||
* Make fancy og tags - and create a special one for battle sharing #4. [Lang]
|
|
||||||
|
|
||||||
### Fix
|
|
||||||
|
|
||||||
* Missing font-awesome icons on bare-metal environment #4. [Lang]
|
|
||||||
|
|
||||||
|
|
||||||
## v2026.2.0-3 (2026-04-14)
|
|
||||||
|
|
||||||
### Changes
|
|
||||||
|
|
||||||
* The user's avatar will be saved as a uuid.extension #4. [Lang]
|
|
||||||
|
|
||||||
|
|
||||||
## v2026.2.0-1 (2026-04-14)
|
|
||||||
|
|
||||||
### Fix
|
|
||||||
|
|
||||||
* Quickfix for email sending #4. [Lang]
|
|
||||||
|
|
||||||
|
|
||||||
## v2026.2.0-0 (2026-04-14)
|
|
||||||
|
|
||||||
### New
|
|
||||||
|
|
||||||
* Registered users have avatars next to the timer #4. [Lang]
|
|
||||||
|
|
||||||
* Add opportunity to use profile picture. #4. [Lang]
|
|
||||||
|
|
||||||
* Add more stats and a dialog for the recent battle that can be shareable #4. [Lang]
|
|
||||||
|
|
||||||
* Implement the 2FA authentication (TOTP and backup codes) #4. [Lang]
|
|
||||||
|
|
||||||
* Add beta logo to the corner #3. [Lang]
|
|
||||||
|
|
||||||
* Add mineseeker game to the symfony 4 project #3. [Lang]
|
|
||||||
|
|
||||||
* Upgrade to the latest symfony v4 #3. [Lang]
|
|
||||||
|
|
||||||
### Changes
|
|
||||||
|
|
||||||
* Implement CD script to Gitea and add docs to the process #4. [Lang]
|
|
||||||
|
|
||||||
* Remove unnecessary cdn based fonts #4. [Lang]
|
|
||||||
|
|
||||||
* Update docs #4. [Lang]
|
|
||||||
|
|
||||||
* Add JWT generation script to make Mercure safe #4. [Lang]
|
|
||||||
|
|
||||||
* Fix missing favicon #4. [Lang]
|
|
||||||
|
|
||||||
* Make compatible the whole project with bare metal AND with docker #4. [Lang]
|
|
||||||
|
|
||||||
* Add modern Webauthn authentication #4. [Lang]
|
|
||||||
|
|
||||||
* Refactor all forms to have Symfony Form Types & Validation Constrainsts - & implement Google ReCapthca v3 #4. [Lang]
|
|
||||||
|
|
||||||
* Add forgot password functionality #4. [Lang]
|
|
||||||
|
|
||||||
* Increase the minimum PHP version to the latest major - and massive refactor on back-end, like Controllers and Repositories #4. [Lang]
|
|
||||||
|
|
||||||
* Redesign the resign dialog #4. [Lang]
|
|
||||||
|
|
||||||
* Re-implement the waiting for opponent dialog - refactor its gfx - & add online user selection dialog #4. [Lang]
|
|
||||||
|
|
||||||
* Improve the gfx on homepage - implement login/register and activation for authentication - and add the first version of profile page #4. [Lang]
|
|
||||||
|
|
||||||
* Refactor and redesign the gfx on front-end #4. [Lang]
|
|
||||||
|
|
||||||
* Upgrade to the latest LTS Symfony package and backend #4. [Lang]
|
|
||||||
|
|
||||||
* Add timers to each player - renew the whole migration #4. [Lang]
|
|
||||||
|
|
||||||
* Update the vite related stuff because CORS and React errors - reinit the miration #4. [Lang]
|
|
||||||
|
|
||||||
* Use namespaces for front-end #4. [Lang]
|
|
||||||
|
|
||||||
* Replace webpack w/ vite & remove old, legacy jQuery from the code #4. [Lang]
|
|
||||||
|
|
||||||
* More, massive refactor for front-end #4. [Lang]
|
|
||||||
|
|
||||||
* Massive refactor on front-end - and remove unnecessary deps #4. [Lang]
|
|
||||||
|
|
||||||
* Change the code style to fit the current standard #4. [Lang]
|
|
||||||
|
|
||||||
* Refactor to use Attributes instead of yaml markdown #4. [Lang]
|
|
||||||
|
|
||||||
* Outsource the Grid generation and interactions to the backend #4. [Lang]
|
|
||||||
|
|
||||||
* Remove unnecessary variables and prune the Facebook registration method #4. [Lang]
|
|
||||||
|
|
||||||
* Replace the legacy gos/web-socket-bundle & replace it with Mercure protocol #4. [Lang]
|
|
||||||
|
|
||||||
* Make a massive refactor to the backend and remove all unnecessary deps - and make small refactors for the frontend too #4. [Lang]
|
|
||||||
|
|
||||||
* Created the first working solution since 7 yrs #4. [Lang]
|
|
||||||
|
|
||||||
* Add some changes on BE - add eslint and editorconfig - and add some deps #4. [Lang]
|
|
||||||
|
|
||||||
* Make the first working version - the stepping is broken due to the algorythm structure #4. [Lang]
|
|
||||||
|
|
||||||
* Change the composer default php minimum environment #3. [Lang]
|
|
||||||
|
|
||||||
* Change the default url to wss on frontend #3. [Lang]
|
|
||||||
|
|
||||||
* Refactor Rpc and Topic classes #3. [Lang]
|
|
||||||
|
|
||||||
* Refactor classes and reformat some layout #3. [Lang]
|
|
||||||
|
|
||||||
* Remove deprecated files #3. [Lang]
|
|
||||||
|
|
||||||
* Doc in README.md #3. [Lang]
|
|
||||||
|
|
||||||
* Gitignore a js.map file #2. [Lang]
|
|
||||||
|
|
||||||
### Other
|
|
||||||
|
|
||||||
* Pkg: usr: solve the not-working mailing on dev env under docker #4. [Lang]
|
|
||||||
|
|
||||||
|
|
||||||
## 1.1.0 (2019-10-26)
|
|
||||||
|
|
||||||
### Changes
|
|
||||||
|
|
||||||
* Reinit project - disable redis module and make the project compatible w/ PHP7.3 #2. [Lang]
|
|
||||||
|
|
||||||
|
|
||||||
## 0.4.0 (2019-10-26)
|
|
||||||
|
|
||||||
### Other
|
|
||||||
|
|
||||||
* Change session driver to REDIS. [Lang]
|
|
||||||
|
|
||||||
* Add created, updated field to db && improve graph design. [Lang]
|
|
||||||
|
|
||||||
* Cache setup && optimalize for google pagespeed && optimalize all images. [Lang]
|
|
||||||
|
|
||||||
* Improve graph design on homepage && add footer and techs && add official pages. [Lang]
|
|
||||||
|
|
||||||
* Bugfix mine websocket periodic mysql calling. [Lang]
|
|
||||||
|
|
||||||
* Bugfix hwioauth remember me && centralize hwioauth and facebook settings. [Lang]
|
|
||||||
|
|
||||||
* Centralize jquery && bugfix mysql auto-termination problem w/ user auth. [Lang]
|
|
||||||
|
|
||||||
* Release beta4. [Lang]
|
|
||||||
|
|
||||||
* Gitignore npm debug log. [Lang]
|
|
||||||
|
|
||||||
* Add english lang everywhere && add snowfall && add centralized version nbr && improve stylesheet && slack integration. [Lang]
|
|
||||||
|
|
||||||
* Bugfix #30 && random bg in game. [Lang]
|
|
||||||
|
|
||||||
* Add google analytics and facebook scripts && improve url share method w/ fb && enforce https in prod. [Lang]
|
|
||||||
|
|
||||||
* Reg and login buttons on index && remove list method && facebook centralize. [Lang]
|
|
||||||
|
|
||||||
* Redesign user frontend. [Lang]
|
|
||||||
|
|
||||||
* Mods for performance; one js.min file on prod. [Lang]
|
|
||||||
|
|
||||||
* Improve webpack config for prod compile #23. [Lang]
|
|
||||||
|
|
||||||
* Ssl handling #22 && reconnection issues #20, #21. [Lang]
|
|
||||||
|
|
||||||
* Facebook prod settings w/ app; hwi/HWIOAuthBundle. [Lang]
|
|
||||||
|
|
||||||
* Refact && game reconnection and restore w/o refresh #3 && bugfix bomb explosion on opponent mines #19. [Lang]
|
|
||||||
|
|
||||||
* Typo in rpc. [Lang]
|
|
||||||
|
|
||||||
* Handle prod mysql timeout && graphics improve. [Lang]
|
|
||||||
|
|
||||||
* Gitignore webpacked index.js. [Lang]
|
|
||||||
|
|
||||||
* Add production mods. [Lang]
|
|
||||||
|
|
||||||
* Bugfix points saving and exploded bombs to db && you can resign #6. [Lang]
|
|
||||||
|
|
||||||
* Bugfix resign button existence #11. [Lang]
|
|
||||||
|
|
||||||
* Bugfix opponent bomb btn buzz on hover #10. [Lang]
|
|
||||||
|
|
||||||
* Bugfix points problem in the end #16. [Lang]
|
|
||||||
|
|
||||||
* Add desc to every user #9. [Lang]
|
|
||||||
|
|
||||||
* Clipboard - not working #8. [Lang]
|
|
||||||
|
|
||||||
* Random player on start #5. [Lang]
|
|
||||||
|
|
||||||
* Show left mines after end #2 && reduce network traffic && better active field checking method. [Lang]
|
|
||||||
|
|
||||||
* Some refactor #13. [Lang]
|
|
||||||
|
|
||||||
* Bugfix grid field render #12. [Lang]
|
|
||||||
|
|
||||||
* Game ends after x mines. [Lang]
|
|
||||||
|
|
||||||
* Add new sounds && refactor && new bg images && form redesigns. [Lang]
|
|
||||||
|
|
||||||
* Bugfix entities gridrow, grid && improve graph design on homepage. [Lang]
|
|
||||||
|
|
||||||
* Some refactor && prod settings. [Lang]
|
|
||||||
|
|
||||||
* Improve graphics design in game. [Lang]
|
|
||||||
|
|
||||||
* Bugfix grid row in entity. [Lang]
|
|
||||||
|
|
||||||
* Bugfix changePlayer after bomb explosion. [Lang]
|
|
||||||
|
|
||||||
* Improve game graph design. [Lang]
|
|
||||||
|
|
||||||
* Login and register form more design. [Lang]
|
|
||||||
|
|
||||||
* Add basic design to userbundle && refactor. [Lang]
|
|
||||||
|
|
||||||
* Add font-awesome. [Lang]
|
|
||||||
|
|
||||||
* Working user authentication w/ fb and plain login. [Lang]
|
|
||||||
|
|
||||||
* Add facebook login module, hwi/HWIOAuthBundle. [Lang]
|
|
||||||
|
|
||||||
* Login && register form overrided. [Lang]
|
|
||||||
|
|
||||||
* Js and config refactor. [Lang]
|
|
||||||
|
|
||||||
* Replace gridcol object to json array in db. [Lang]
|
|
||||||
|
|
||||||
* Refactor. [Lang]
|
|
||||||
|
|
||||||
* Save steps and point info to db. [Lang]
|
|
||||||
|
|
||||||
* Save the step data to db. [Lang]
|
|
||||||
|
|
||||||
* Renamed the acme to mineseeker && handle when the user connection has been lost. [Lang]
|
|
||||||
|
|
||||||
* Add player names to UI. [Lang]
|
|
||||||
|
|
||||||
* Add overlay && game do not start until the opponent came. [Lang]
|
|
||||||
|
|
||||||
* Add base64 encryption to grid when it has been sended to server. [Lang]
|
|
||||||
|
|
||||||
* On click opponents bomb, you cannot target && refactor. [Lang]
|
|
||||||
|
|
||||||
* Warning when player has been found more than 20 mines. [Lang]
|
|
||||||
|
|
||||||
* Bugfix center mine counter animation. [Lang]
|
|
||||||
|
|
||||||
* The opponent is the next when bomb is exploded. [Lang]
|
|
||||||
|
|
||||||
* Current username checked && refactor && remove players in channel when they are more than 2. [Lang]
|
|
||||||
|
|
||||||
* Send bomb info and use it on opponent. [Lang]
|
|
||||||
|
|
||||||
* Add sounds w/ howler. [Lang]
|
|
||||||
|
|
||||||
* Bugfix multiple empty fields w/ one click on opponent view. [Lang]
|
|
||||||
|
|
||||||
* Refact && remove sound and logging && bugfix BIGBUG - handleGridField and showAppropriateFields sort order... [Lang]
|
|
||||||
|
|
||||||
* Create first working communication. [Lang]
|
|
||||||
|
|
||||||
* Create entities and repositories. [Lang]
|
|
||||||
|
|
||||||
* Changed websocket default port && debug RPC. [Lang]
|
|
||||||
|
|
||||||
* Created working session and client handler w/ websocket. [Lang]
|
|
||||||
|
|
||||||
* Working websocket client and server w/o session handling and storage. [Lang]
|
|
||||||
|
|
||||||
* Composer update. [Lang]
|
|
||||||
|
|
||||||
* Improve game && start sound creating. [Lang]
|
|
||||||
|
|
||||||
* Refactor grid control and grid field. [Lang]
|
|
||||||
|
|
||||||
* Created basic game w/ table and animations. [Lang]
|
|
||||||
|
|
||||||
* Websocket basic setup FE & BE && working basic game w/ react && webpack & babel config. [Lang]
|
|
||||||
|
|
||||||
* Gitignore node_modules && add symlink to node_modules (just for install) && basic react. [Lang]
|
|
||||||
|
|
||||||
* Add react hello world. [Lang]
|
|
||||||
|
|
||||||
* Rename project in config. [Lang]
|
|
||||||
|
|
||||||
* Initial commit && create project in symfony3. [Lang]
|
|
||||||
|
|
||||||
|
|
||||||
29
Caddyfile
29
Caddyfile
@@ -1,29 +0,0 @@
|
|||||||
{
|
|
||||||
{$CADDY_GLOBAL_OPTIONS}
|
|
||||||
|
|
||||||
frankenphp {
|
|
||||||
{$FRANKENPHP_CONFIG}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
{$SERVER_NAME:localhost} {
|
|
||||||
log
|
|
||||||
|
|
||||||
root * /app/public
|
|
||||||
|
|
||||||
encode zstd br gzip
|
|
||||||
|
|
||||||
# Forward scheme information to the PHP application
|
|
||||||
header X-Forwarded-Proto {scheme}
|
|
||||||
header X-Forwarded-Host {host}
|
|
||||||
|
|
||||||
mercure {
|
|
||||||
transport_url {$MERCURE_TRANSPORT_URL:bolt:///data/mercure.db}
|
|
||||||
publisher_jwt {$MERCURE_JWT_SECRET} HS256
|
|
||||||
subscriber_jwt {$MERCURE_JWT_SECRET} HS256
|
|
||||||
anonymous
|
|
||||||
subscriptions
|
|
||||||
}
|
|
||||||
|
|
||||||
php_server
|
|
||||||
}
|
|
||||||
53
Dockerfile
53
Dockerfile
@@ -1,53 +0,0 @@
|
|||||||
FROM oven/bun:1-alpine AS assets
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
COPY package.json bun.lock ./
|
|
||||||
RUN bun install --frozen-lockfile
|
|
||||||
|
|
||||||
COPY vite.config.js ./
|
|
||||||
COPY assets/ assets/
|
|
||||||
COPY public/ public/
|
|
||||||
|
|
||||||
RUN bun run build
|
|
||||||
|
|
||||||
FROM dunglas/frankenphp:latest
|
|
||||||
|
|
||||||
RUN install-php-extensions \
|
|
||||||
pdo_pgsql \
|
|
||||||
gd \
|
|
||||||
intl \
|
|
||||||
zip \
|
|
||||||
opcache \
|
|
||||||
apcu \
|
|
||||||
sodium
|
|
||||||
|
|
||||||
RUN mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini"
|
|
||||||
RUN printf '[PHP]\nupload_max_filesize=10M\npost_max_size=11M\n' > "$PHP_INI_DIR/conf.d/uploads.ini"
|
|
||||||
RUN printf '[opcache]\nopcache.enable=1\nopcache.memory_consumption=256\nopcache.max_accelerated_files=20000\nopcache.validate_timestamps=0\n' \
|
|
||||||
> "$PHP_INI_DIR/conf.d/opcache.ini"
|
|
||||||
|
|
||||||
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
|
|
||||||
|
|
||||||
ENV APP_ENV=prod
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
COPY . .
|
|
||||||
|
|
||||||
RUN composer install \
|
|
||||||
--no-dev \
|
|
||||||
--no-interaction \
|
|
||||||
--no-scripts \
|
|
||||||
--optimize-autoloader
|
|
||||||
|
|
||||||
COPY --from=assets /app/public/build ./public/build
|
|
||||||
|
|
||||||
RUN mkdir -p var/cache var/log var/sessions && \
|
|
||||||
chown -R www-data:www-data var/
|
|
||||||
|
|
||||||
COPY Caddyfile /etc/caddy/Caddyfile
|
|
||||||
COPY docker/entrypoint.sh /entrypoint.sh
|
|
||||||
RUN chmod +x /entrypoint.sh
|
|
||||||
|
|
||||||
ENTRYPOINT ["/entrypoint.sh"]
|
|
||||||
124
LICENSE
124
LICENSE
@@ -1,124 +0,0 @@
|
|||||||
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
105
Makefile
@@ -1,105 +0,0 @@
|
|||||||
.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
|
|
||||||
381
README.md
381
README.md
@@ -1,379 +1,40 @@
|
|||||||
# MineSeeker
|
This is a Symfony 3 project w/ React JS in standalone mode and w/ WebSocket.
|
||||||
|
|
||||||
A real-time **1v1 multiplayer minesweeper** game played in the browser.
|
0.) Must installed modules w/ npm are in package.json + to global:
|
||||||
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.
|
|
||||||
|
|
||||||
Created by [SplendidBear](https://www.splendidbear.org).
|
$ npm install webpack -g
|
||||||
|
|
||||||
---
|
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!!
|
||||||
|
|
||||||
## Features
|
(!) Tutorial: https://egghead.io/lessons/react-introduction-to-properties
|
||||||
|
|
||||||
- **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
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Tech stack
|
1.) Backend WebSocket server start as daemon - GeniusesOfSymfony/WebSocketBundle
|
||||||
|
|
||||||
| Layer | Technology |
|
$ nohup php bin/console gos:websocket:server --env=prod &
|
||||||
|---|---|
|
|
||||||
| 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
|
||||||
|
|
||||||
## Requirements
|
$ webpack -p --config=webpack-prod.config.js
|
||||||
|
|
||||||
### Bare-metal development
|
PROD
|
||||||
- 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)
|
|
||||||
|
|
||||||
### Docker
|
$ webpack --progress --colors --watch -d
|
||||||
- Docker Engine 24+
|
|
||||||
- Docker Compose v2
|
|
||||||
- Bun (for building frontend assets before `make start-build`)
|
|
||||||
- Composer (for `make start-build`)
|
|
||||||
|
|
||||||
---
|
DEV
|
||||||
|
|
||||||
## Installation
|
-d --> Debugger; If you write this line somewhere: debugger;
|
||||||
|
The browser will stop the code here!!!
|
||||||
|
|
||||||
### 1. Clone the repository
|
3.) Connect to Prod
|
||||||
|
|
||||||
```bash
|
$ ssh xxsvci@laszlolang.com -i ~/.ssh/id_rsa_laszlolang
|
||||||
git clone https://github.com/splendidbear/mineseeker.git
|
|
||||||
cd mineseeker
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Configure environment
|
4.) Stunnel config
|
||||||
|
|
||||||
```bash
|
$ sudo nano /etc/stunnel/stunnel.conf
|
||||||
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
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|||||||
7
app/.htaccess
Normal file
7
app/.htaccess
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<IfModule mod_authz_core.c>
|
||||||
|
Require all denied
|
||||||
|
</IfModule>
|
||||||
|
<IfModule !mod_authz_core.c>
|
||||||
|
Order deny,allow
|
||||||
|
Deny from all
|
||||||
|
</IfModule>
|
||||||
7
app/AppCache.php
Normal file
7
app/AppCache.php
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Symfony\Bundle\FrameworkBundle\HttpCache\HttpCache;
|
||||||
|
|
||||||
|
class AppCache extends HttpCache
|
||||||
|
{
|
||||||
|
}
|
||||||
63
app/AppKernel.php
Normal file
63
app/AppKernel.php
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
<?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');
|
||||||
|
}
|
||||||
|
}
|
||||||
51
app/Resources/views/base.html.twig
Normal file
51
app/Resources/views/base.html.twig
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
<!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>
|
||||||
13
app/autoload.php
Normal file
13
app/autoload.php
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<?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;
|
||||||
112
app/config/config.yml
Normal file
112
app/config/config.yml
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
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
|
||||||
|
fos.email: 7system7@gmail.com
|
||||||
|
fos.name: system7
|
||||||
|
|
||||||
|
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.42.21 (beta8)"
|
||||||
|
facebook_api: "%facebook.api%"
|
||||||
|
facebook_scope: "%facebook.scope%"
|
||||||
|
facebook_api_version: "%facebook.version%"
|
||||||
|
|
||||||
|
# Doctrine Configuration
|
||||||
|
doctrine:
|
||||||
|
dbal:
|
||||||
|
driver: pdo_mysql
|
||||||
|
host: "%database_host%"
|
||||||
|
port: "%database_port%"
|
||||||
|
dbname: "%database_name%"
|
||||||
|
user: "%database_user%"
|
||||||
|
password: "%database_password%"
|
||||||
|
charset: UTF8
|
||||||
|
# if using pdo_sqlite as your database driver:
|
||||||
|
# 1. add the path in parameters.yml
|
||||||
|
# e.g. database_path: "%kernel.root_dir%/data/data.db3"
|
||||||
|
# 2. Uncomment database_path in parameters.yml.dist
|
||||||
|
# 3. Uncomment next line:
|
||||||
|
# path: "%database_path%"
|
||||||
|
|
||||||
|
orm:
|
||||||
|
auto_generate_proxy_classes: "%kernel.debug%"
|
||||||
|
naming_strategy: doctrine.orm.naming_strategy.underscore
|
||||||
|
auto_mapping: true
|
||||||
|
|
||||||
|
# Assetic Configuration
|
||||||
|
assetic:
|
||||||
|
debug: "%kernel.debug%"
|
||||||
|
use_controller: "%kernel.debug%"
|
||||||
|
bundles: ~
|
||||||
|
node: /usr/bin/nodejs
|
||||||
|
filters:
|
||||||
|
cssrewrite: ~
|
||||||
|
uglifyjs2:
|
||||||
|
bin: "%kernel.root_dir%/../node_modules/uglify-js/bin/uglifyjs"
|
||||||
|
no_copyright: true
|
||||||
|
uglifycss:
|
||||||
|
bin: "%kernel.root_dir%/../node_modules/uglifycss/uglifycss"
|
||||||
|
|
||||||
|
# FOS User Configuration
|
||||||
|
fos_user:
|
||||||
|
db_driver: orm # other valid values are 'mongodb', 'couchdb' and 'propel'
|
||||||
|
firewall_name: secured_area
|
||||||
|
user_class: Jotunheimr\UserBundle\Entity\User
|
||||||
|
from_email:
|
||||||
|
address: "%fos.email%"
|
||||||
|
sender_name: "%fos.name%"
|
||||||
|
|
||||||
|
# Facebook OAuth
|
||||||
|
hwi_oauth:
|
||||||
|
firewall_names: [secured_area]
|
||||||
|
resource_owners:
|
||||||
|
facebook:
|
||||||
|
type: facebook
|
||||||
|
client_id: "%facebook.api%"
|
||||||
|
client_secret: "%facebook.api-secret%"
|
||||||
|
scope: "%facebook.scope%"
|
||||||
|
options:
|
||||||
|
display: popup
|
||||||
|
auth_type: rerequest
|
||||||
|
csrf: true
|
||||||
|
|
||||||
|
# Slack integration
|
||||||
|
#cl_slack:
|
||||||
|
# api_token: xoxp-107639806167-107029084564-115427085733-cccaa4f96c89c87ce680c7f22acfd001
|
||||||
40
app/config/config_dev.yml
Normal file
40
app/config/config_dev.yml
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
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
|
||||||
31
app/config/config_prod.yml
Normal file
31
app/config/config_prod.yml
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
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
|
||||||
16
app/config/config_test.yml
Normal file
16
app/config/config_test.yml
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
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
|
||||||
19
app/config/parameters.yml.dist
Normal file
19
app/config/parameters.yml.dist
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# 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
|
||||||
12
app/config/parameters_dev.yml
Normal file
12
app/config/parameters_dev.yml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
# 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
|
||||||
15
app/config/parameters_prod.yml
Normal file
15
app/config/parameters_prod.yml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# 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
|
||||||
14
app/config/routing.yml
Normal file
14
app/config/routing.yml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
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: /
|
||||||
15
app/config/routing_dev.yml
Normal file
15
app/config/routing_dev.yml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
_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]
|
||||||
46
app/config/security.yml
Normal file
46
app/config/security.yml
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
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 }
|
||||||
9
app/config/services.yml
Normal file
9
app/config/services.yml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# 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%"]
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
/*!*
|
|
||||||
* 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';
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
/*!*
|
|
||||||
* 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';
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
/*!*
|
|
||||||
* 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); }
|
|
||||||
}
|
|
||||||
@@ -1,128 +0,0 @@
|
|||||||
/*!*
|
|
||||||
* 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,474 +0,0 @@
|
|||||||
/*!*
|
|
||||||
* 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,200 +0,0 @@
|
|||||||
/*!*
|
|
||||||
* 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);
|
|
||||||
}
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
/*!*
|
|
||||||
* 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;
|
|
||||||
}
|
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
/*!*
|
|
||||||
* 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;
|
|
||||||
}
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
/*!*
|
|
||||||
* 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,363 +0,0 @@
|
|||||||
/*!*
|
|
||||||
* 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,121 +0,0 @@
|
|||||||
/*!*
|
|
||||||
* 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; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
/*!*
|
|
||||||
* 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;
|
|
||||||
}
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
/*!*
|
|
||||||
* 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;
|
|
||||||
}
|
|
||||||
@@ -1,99 +0,0 @@
|
|||||||
/*!*
|
|
||||||
* This file is part of the SplendidBear Websites' projects.
|
|
||||||
*
|
|
||||||
* Copyright (c) 2026 @ www.splendidbear.org
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view the LICENSE
|
|
||||||
* file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
.hero {
|
|
||||||
position: relative;
|
|
||||||
z-index: 2;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
text-align: center;
|
|
||||||
min-height: 100vh;
|
|
||||||
padding: 80px 40px 160px;
|
|
||||||
gap: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Decorative glow blobs in opposite corners
|
|
||||||
.hero::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: -60px;
|
|
||||||
left: -60px;
|
|
||||||
width: 420px;
|
|
||||||
height: 420px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: radial-gradient(circle, rgba(173, 10, 5, 0.09) 0%, transparent 65%);
|
|
||||||
pointer-events: none;
|
|
||||||
z-index: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero::after {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
bottom: 100px;
|
|
||||||
right: -60px;
|
|
||||||
width: 380px;
|
|
||||||
height: 380px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: radial-gradient(circle, rgba(35, 111, 135, 0.1) 0%, transparent 65%);
|
|
||||||
pointer-events: none;
|
|
||||||
z-index: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Logo
|
|
||||||
.hero-logo {
|
|
||||||
display: block;
|
|
||||||
margin-bottom: 72px;
|
|
||||||
position: relative;
|
|
||||||
z-index: 1;
|
|
||||||
animation: appear 0.9s cubic-bezier(0.22, 1, 0.36, 1) both;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-logo img {
|
|
||||||
width: 400px;
|
|
||||||
max-width: 82vw;
|
|
||||||
filter:
|
|
||||||
drop-shadow(0 0 40px rgba(35, 111, 135, 0.35))
|
|
||||||
drop-shadow(0 6px 20px rgba(0, 0, 0, 0.8));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Body text block
|
|
||||||
.hero-body {
|
|
||||||
position: relative;
|
|
||||||
z-index: 1;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-sub {
|
|
||||||
font: 300 17px 'Rajdhani', sans-serif;
|
|
||||||
color: rgba(149, 207, 245, 0.9);
|
|
||||||
letter-spacing: 3px;
|
|
||||||
text-transform: uppercase;
|
|
||||||
margin-bottom: 18px;
|
|
||||||
animation: rise 0.8s 0.15s cubic-bezier(0.22, 1, 0.36, 1) both;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-sub strong {
|
|
||||||
font-weight: 600;
|
|
||||||
color: #95cff5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero h1 {
|
|
||||||
font: 800 58px 'Rajdhani', sans-serif;
|
|
||||||
color: #ffffff;
|
|
||||||
line-height: 1.1;
|
|
||||||
letter-spacing: 1px;
|
|
||||||
margin-bottom: 56px;
|
|
||||||
text-shadow: 0 4px 30px rgba(0, 0, 0, 0.7);
|
|
||||||
animation: rise 0.8s 0.28s cubic-bezier(0.22, 1, 0.36, 1) both;
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,32 +0,0 @@
|
|||||||
/*!*
|
|
||||||
* 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%;
|
|
||||||
}
|
|
||||||
@@ -1,113 +0,0 @@
|
|||||||
/*!*
|
|
||||||
* 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
/*!*
|
|
||||||
* 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
/*!*
|
|
||||||
* 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);
|
|
||||||
}
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
/*!*
|
|
||||||
* 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;
|
|
||||||
}
|
|
||||||
@@ -1,234 +0,0 @@
|
|||||||
/*!*
|
|
||||||
* 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;
|
|
||||||
}
|
|
||||||
@@ -1,250 +0,0 @@
|
|||||||
/*!*
|
|
||||||
* 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,216 +0,0 @@
|
|||||||
/*!*
|
|
||||||
* 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%;
|
|
||||||
}
|
|
||||||
@@ -1,99 +0,0 @@
|
|||||||
/*!*
|
|
||||||
* 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;
|
|
||||||
}
|
|
||||||
@@ -1,780 +0,0 @@
|
|||||||
/*!*
|
|
||||||
* 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
/*!*
|
|
||||||
* 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,111 +0,0 @@
|
|||||||
/*!*
|
|
||||||
* 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;
|
|
||||||
}
|
|
||||||
@@ -1,171 +0,0 @@
|
|||||||
/*!*
|
|
||||||
* 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;
|
|
||||||
}
|
|
||||||
@@ -1,356 +0,0 @@
|
|||||||
/*!*
|
|
||||||
* 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); }
|
|
||||||
}
|
|
||||||
@@ -1,301 +0,0 @@
|
|||||||
/*!*
|
|
||||||
* 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
/*!*
|
|
||||||
* 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';
|
|
||||||
@@ -1,403 +0,0 @@
|
|||||||
/*!*
|
|
||||||
* 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
/*!*
|
|
||||||
* 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';
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
/*!*
|
|
||||||
* This file is part of the SplendidBear Websites' projects.
|
|
||||||
*
|
|
||||||
* Copyright (c) 2019 @ www.splendidbear.org
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view the LICENSE
|
|
||||||
* file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
.mine-beta {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
right: 0;
|
|
||||||
width: 150px;
|
|
||||||
|
|
||||||
z-index: 4;
|
|
||||||
|
|
||||||
transform: rotate(90deg);
|
|
||||||
}
|
|
||||||
Binary file not shown.
@@ -1,170 +0,0 @@
|
|||||||
<!---
|
|
||||||
- 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>
|
|
||||||
|
|
||||||
|
Before Width: | Height: | Size: 7.5 KiB |
@@ -1,22 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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 || ''}
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
/**
|
|
||||||
* This file is part of the SplendidBear Websites' projects.
|
|
||||||
*
|
|
||||||
* Copyright (c) 2026 @ www.splendidbear.org
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view the LICENSE
|
|
||||||
* file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import React, { Fragment, useMemo, useRef } from 'react';
|
|
||||||
import { string } from 'prop-types';
|
|
||||||
import { useProfileDataProvider } from '@mine-hooks/useGameDataProvider';
|
|
||||||
|
|
||||||
export const AvatarUpload = ({ uploadUrl, initialThumbUrl, initials }) => {
|
|
||||||
const inputRef = useRef(null);
|
|
||||||
const [thumbUrl, setThumbUrl] = React.useState(initialThumbUrl || null);
|
|
||||||
const { uploadAvatarMutation: { isPending, error, mutate } } = useProfileDataProvider();
|
|
||||||
|
|
||||||
const errorMessage = useMemo(() => error?.message ?? null, [error]);
|
|
||||||
|
|
||||||
const handleChange = e => {
|
|
||||||
const file = e.target.files?.[0];
|
|
||||||
if (!file) return;
|
|
||||||
|
|
||||||
mutate({ uploadUrl, file }, {
|
|
||||||
onSuccess: data => {
|
|
||||||
setThumbUrl(data.thumbUrl);
|
|
||||||
|
|
||||||
const navImg = document.querySelector('.hero-auth-avatar:not(.hero-auth-avatar--initials)');
|
|
||||||
const navInitials = document.querySelector('.hero-auth-avatar.hero-auth-avatar--initials');
|
|
||||||
|
|
||||||
if (navImg) {
|
|
||||||
navImg.src = data.thumbUrl;
|
|
||||||
} else if (navInitials) {
|
|
||||||
const img = document.createElement('img');
|
|
||||||
img.src = data.thumbUrl;
|
|
||||||
img.alt = navInitials.textContent.trim();
|
|
||||||
img.className = 'hero-auth-avatar';
|
|
||||||
navInitials.replaceWith(img);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Fragment>
|
|
||||||
<div
|
|
||||||
className={`profile-avatar${isPending ? ' profile-avatar--loading' : ''}`}
|
|
||||||
title="Click to change profile picture"
|
|
||||||
onClick={() => inputRef.current?.click()}
|
|
||||||
>
|
|
||||||
{thumbUrl
|
|
||||||
? <img src={thumbUrl} alt={initials} className="profile-avatar__img" />
|
|
||||||
: <span className="profile-avatar__initials">{initials}</span>
|
|
||||||
}
|
|
||||||
<div className="profile-avatar__overlay">
|
|
||||||
<i className="fa fa-camera" />
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
ref={inputRef}
|
|
||||||
type="file"
|
|
||||||
accept="image/jpeg,image/png,image/gif,image/webp"
|
|
||||||
style={{ display: 'none' }}
|
|
||||||
onChange={handleChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{errorMessage && <div className="profile-avatar__error">{errorMessage}</div>}
|
|
||||||
</Fragment>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
AvatarUpload.propTypes = {
|
|
||||||
uploadUrl: string.isRequired,
|
|
||||||
initialThumbUrl: string,
|
|
||||||
initials: string.isRequired,
|
|
||||||
};
|
|
||||||
@@ -1,223 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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)',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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;
|
|
||||||
@@ -1,118 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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,
|
|
||||||
};
|
|
||||||
@@ -1,428 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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,
|
|
||||||
};
|
|
||||||
@@ -1,165 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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,
|
|
||||||
};
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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,
|
|
||||||
};
|
|
||||||
@@ -1,122 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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,
|
|
||||||
};
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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,
|
|
||||||
};
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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';
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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,
|
|
||||||
};
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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,
|
|
||||||
};
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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,
|
|
||||||
};
|
|
||||||
@@ -1,123 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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,
|
|
||||||
};
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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,
|
|
||||||
};
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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,
|
|
||||||
};
|
|
||||||
@@ -1,135 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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;
|
|
||||||
@@ -1,294 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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,
|
|
||||||
};
|
|
||||||
@@ -1,104 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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,
|
|
||||||
};
|
|
||||||
@@ -1,123 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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,
|
|
||||||
};
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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,
|
|
||||||
};
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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';
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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,
|
|
||||||
};
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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,
|
|
||||||
};
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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,
|
|
||||||
};
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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,
|
|
||||||
};
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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;
|
|
||||||
@@ -1,280 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
/**
|
|
||||||
* This file is part of the SplendidBear Websites' projects.
|
|
||||||
*
|
|
||||||
* Copyright (c) 2026 @ www.splendidbear.org
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view the LICENSE
|
|
||||||
* file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export { useGame } from './GameContext';
|
|
||||||
export { GameProvider } from './GameProvider';
|
|
||||||
export { default as GameContext } from './GameContext';
|
|
||||||
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user