瀏覽代碼

Epic 1

master
Daniel Covington 1 周之前
父節點
當前提交
91aa90c86b
共有 100 個文件被更改,包括 62182 次插入39 次删除
  1. +22
    -0
      .claude/settings.json
  2. +393
    -0
      .editorconfig
  3. +5
    -0
      .gitignore
  4. +15
    -0
      .idea/.idea.CartWise/.idea/.gitignore
  5. +4
    -0
      .idea/.idea.CartWise/.idea/encodings.xml
  6. +8
    -0
      .idea/.idea.CartWise/.idea/indexLayout.xml
  7. +6
    -0
      .idea/.idea.CartWise/.idea/vcs.xml
  8. +131
    -0
      CartWise.sln
  9. +12
    -0
      Directory.Build.props
  10. +15
    -10
      README.md
  11. +1
    -0
      docs/decision-log.md
  12. +3
    -3
      docs/release-mvp-checklist.md
  13. +34
    -26
      docs/scrum-backlog.md
  14. +11
    -0
      src/CartWise.Application/CartWise.Application.csproj
  15. +7
    -0
      src/CartWise.Domain/CartWise.Domain.csproj
  16. +26
    -0
      src/CartWise.Infrastructure/CartWise.Infrastructure.csproj
  17. +13
    -0
      src/CartWise.Infrastructure/Data/CartWiseDbContext.cs
  18. +275
    -0
      src/CartWise.Infrastructure/Data/Migrations/20260810153611_InitialIdentity.Designer.cs
  19. +224
    -0
      src/CartWise.Infrastructure/Data/Migrations/20260810153611_InitialIdentity.cs
  20. +272
    -0
      src/CartWise.Infrastructure/Data/Migrations/CartWiseDbContextModelSnapshot.cs
  21. +10
    -0
      src/CartWise.Infrastructure/Identity/ApplicationUser.cs
  22. +28
    -0
      src/CartWise.Infrastructure/InfrastructureServiceCollectionExtensions.cs
  23. +0
    -0
      src/CartWise.Web/App_Data/.gitkeep
  24. +19
    -0
      src/CartWise.Web/CartWise.Web.csproj
  25. +99
    -0
      src/CartWise.Web/Controllers/AccountController.cs
  26. +26
    -0
      src/CartWise.Web/Controllers/HomeController.cs
  27. +8
    -0
      src/CartWise.Web/Models/ErrorViewModel.cs
  28. +44
    -0
      src/CartWise.Web/Program.cs
  29. +23
    -0
      src/CartWise.Web/Properties/launchSettings.json
  30. +19
    -0
      src/CartWise.Web/ViewModels/Account/LoginViewModel.cs
  31. +28
    -0
      src/CartWise.Web/ViewModels/Account/RegisterViewModel.cs
  32. +36
    -0
      src/CartWise.Web/Views/Account/Login.cshtml
  33. +42
    -0
      src/CartWise.Web/Views/Account/Register.cshtml
  34. +8
    -0
      src/CartWise.Web/Views/Home/Index.cshtml
  35. +6
    -0
      src/CartWise.Web/Views/Home/Privacy.cshtml
  36. +25
    -0
      src/CartWise.Web/Views/Shared/Error.cshtml
  37. +72
    -0
      src/CartWise.Web/Views/Shared/_Layout.cshtml
  38. +48
    -0
      src/CartWise.Web/Views/Shared/_Layout.cshtml.css
  39. +2
    -0
      src/CartWise.Web/Views/Shared/_ValidationScriptsPartial.cshtml
  40. +3
    -0
      src/CartWise.Web/Views/_ViewImports.cshtml
  41. +3
    -0
      src/CartWise.Web/Views/_ViewStart.cshtml
  42. +9
    -0
      src/CartWise.Web/appsettings.Development.json
  43. +14
    -0
      src/CartWise.Web/appsettings.json
  44. +31
    -0
      src/CartWise.Web/wwwroot/css/site.css
  45. 二進制
      src/CartWise.Web/wwwroot/favicon.ico
  46. +0
    -0
      src/CartWise.Web/wwwroot/images/.gitkeep
  47. +0
    -0
      src/CartWise.Web/wwwroot/js/components/.gitkeep
  48. +0
    -0
      src/CartWise.Web/wwwroot/js/pages/.gitkeep
  49. +0
    -0
      src/CartWise.Web/wwwroot/js/services/.gitkeep
  50. +4
    -0
      src/CartWise.Web/wwwroot/js/site.js
  51. +0
    -0
      src/CartWise.Web/wwwroot/js/utils/.gitkeep
  52. +22
    -0
      src/CartWise.Web/wwwroot/lib/bootstrap/LICENSE
  53. +4085
    -0
      src/CartWise.Web/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.css
  54. +1
    -0
      src/CartWise.Web/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.css.map
  55. +6
    -0
      src/CartWise.Web/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.min.css
  56. +1
    -0
      src/CartWise.Web/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.min.css.map
  57. +4084
    -0
      src/CartWise.Web/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.css
  58. +1
    -0
      src/CartWise.Web/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.css.map
  59. +6
    -0
      src/CartWise.Web/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.min.css
  60. +1
    -0
      src/CartWise.Web/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.min.css.map
  61. +597
    -0
      src/CartWise.Web/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.css
  62. +1
    -0
      src/CartWise.Web/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.css.map
  63. +6
    -0
      src/CartWise.Web/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.min.css
  64. +1
    -0
      src/CartWise.Web/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.min.css.map
  65. +594
    -0
      src/CartWise.Web/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.css
  66. +1
    -0
      src/CartWise.Web/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.css.map
  67. +6
    -0
      src/CartWise.Web/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.min.css
  68. +1
    -0
      src/CartWise.Web/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.min.css.map
  69. +5402
    -0
      src/CartWise.Web/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.css
  70. +1
    -0
      src/CartWise.Web/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.css.map
  71. +6
    -0
      src/CartWise.Web/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.min.css
  72. +1
    -0
      src/CartWise.Web/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.min.css.map
  73. +5393
    -0
      src/CartWise.Web/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.css
  74. +1
    -0
      src/CartWise.Web/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.css.map
  75. +6
    -0
      src/CartWise.Web/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.min.css
  76. +1
    -0
      src/CartWise.Web/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.min.css.map
  77. +12057
    -0
      src/CartWise.Web/wwwroot/lib/bootstrap/dist/css/bootstrap.css
  78. +1
    -0
      src/CartWise.Web/wwwroot/lib/bootstrap/dist/css/bootstrap.css.map
  79. +6
    -0
      src/CartWise.Web/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css
  80. +1
    -0
      src/CartWise.Web/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css.map
  81. +12030
    -0
      src/CartWise.Web/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.css
  82. +1
    -0
      src/CartWise.Web/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.css.map
  83. +6
    -0
      src/CartWise.Web/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.min.css
  84. +1
    -0
      src/CartWise.Web/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.min.css.map
  85. +6314
    -0
      src/CartWise.Web/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.js
  86. +1
    -0
      src/CartWise.Web/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.js.map
  87. +7
    -0
      src/CartWise.Web/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.min.js
  88. +1
    -0
      src/CartWise.Web/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.min.js.map
  89. +4447
    -0
      src/CartWise.Web/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.js
  90. +1
    -0
      src/CartWise.Web/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.js.map
  91. +7
    -0
      src/CartWise.Web/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.min.js
  92. +1
    -0
      src/CartWise.Web/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.min.js.map
  93. +4494
    -0
      src/CartWise.Web/wwwroot/lib/bootstrap/dist/js/bootstrap.js
  94. +1
    -0
      src/CartWise.Web/wwwroot/lib/bootstrap/dist/js/bootstrap.js.map
  95. +7
    -0
      src/CartWise.Web/wwwroot/lib/bootstrap/dist/js/bootstrap.min.js
  96. +1
    -0
      src/CartWise.Web/wwwroot/lib/bootstrap/dist/js/bootstrap.min.js.map
  97. +23
    -0
      src/CartWise.Web/wwwroot/lib/jquery-validation-unobtrusive/LICENSE.txt
  98. +435
    -0
      src/CartWise.Web/wwwroot/lib/jquery-validation-unobtrusive/dist/jquery.validate.unobtrusive.js
  99. +8
    -0
      src/CartWise.Web/wwwroot/lib/jquery-validation-unobtrusive/dist/jquery.validate.unobtrusive.min.js
  100. +22
    -0
      src/CartWise.Web/wwwroot/lib/jquery-validation/LICENSE.md

+ 22
- 0
.claude/settings.json 查看文件

@@ -0,0 +1,22 @@
{
"permissions": {
"allow": [
"Bash(dotnet --version)",
"Bash(dotnet new *)",
"Bash(dotnet sln *)",
"Read(//g//**)",
"Bash(find /c -maxdepth 2 -iname \"*.sln\")",
"Bash(dotnet list *)",
"Bash(curl -s \"https://api.nuget.org/v3-flatcontainer/sqlitepclraw.bundle_e_sqlite3/index.json\")",
"Bash(mkdir -p js/pages js/services js/components js/utils images)",
"Bash(touch js/pages/.gitkeep js/services/.gitkeep js/components/.gitkeep js/utils/.gitkeep images/.gitkeep)",
"Bash(ASPNETCORE_URLS=\"http://127.0.0.1:5187\" dotnet run --no-launch-profile)",
"Bash(curl -s -o /dev/null -w \"css status=%{http_code}\\\\n\" http://127.0.0.1:5187/css/site.css)",
"Bash(curl -s -o /dev/null -w \"bootstrap status=%{http_code}\\\\n\" http://127.0.0.1:5187/lib/bootstrap/dist/css/bootstrap.min.css)",
"Bash(dotnet tool *)",
"Bash(rm -f \"g:/development/C Sharp AI/CartWise/src/CartWise.Web/App_Data/cartwise.db\")",
"Bash(ASPNETCORE_URLS=\"http://127.0.0.1:5187\" ASPNETCORE_ENVIRONMENT=\"Development\" dotnet run --no-launch-profile)",
"Bash(xargs -I{} sh -c 'echo \"=== {} ===\"; cat {}')"
]
}
}

+ 393
- 0
.editorconfig 查看文件

@@ -0,0 +1,393 @@
root = true

# All files
[*]
indent_style = space

# Xml files
[*.xml]
indent_size = 2

# Xml project files
[*.{csproj,fsproj,vbproj,proj,slnx}]
indent_size = 2

# Xml config files
[*.{props,targets,config,nuspec}]
indent_size = 2

[*.json]
indent_size = 2

# C# files
[*.cs]

#### Core EditorConfig Options ####

# Indentation and spacing
indent_size = 4
tab_width = 4

# New line preferences
insert_final_newline = false

#### .NET Coding Conventions ####
[*.{cs,vb}]

# Organize usings
dotnet_separate_import_directive_groups = true
dotnet_sort_system_directives_first = true
file_header_template = unset

# this. and Me. preferences
dotnet_style_qualification_for_event = false:silent
dotnet_style_qualification_for_field = false:silent
dotnet_style_qualification_for_method = false:silent
dotnet_style_qualification_for_property = false:silent

# Language keywords vs BCL types preferences
dotnet_style_predefined_type_for_locals_parameters_members = true:silent
dotnet_style_predefined_type_for_member_access = true:silent

# Parentheses preferences
dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent
dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent
dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent
dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent

# Modifier preferences
dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent

# Expression-level preferences
dotnet_style_coalesce_expression = true:suggestion
dotnet_style_collection_initializer = true:suggestion
dotnet_style_explicit_tuple_names = true:suggestion
dotnet_style_namespace_match_folder = true:suggestion
dotnet_style_null_propagation = true:suggestion
dotnet_style_object_initializer = true:suggestion
dotnet_style_operator_placement_when_wrapping = beginning_of_line
dotnet_style_prefer_auto_properties = true:suggestion
dotnet_style_prefer_collection_expression = when_types_loosely_match:suggestion
dotnet_style_prefer_compound_assignment = true:suggestion
dotnet_style_prefer_conditional_expression_over_assignment = true:suggestion
dotnet_style_prefer_conditional_expression_over_return = true:suggestion
dotnet_style_prefer_foreach_explicit_cast_in_source = when_strongly_typed:suggestion
dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion
dotnet_style_prefer_inferred_tuple_names = true:suggestion
dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion
dotnet_style_prefer_simplified_boolean_expressions = true:suggestion
dotnet_style_prefer_simplified_interpolation = true:suggestion

# Field preferences
dotnet_style_readonly_field = true:warning

# Parameter preferences
dotnet_code_quality_unused_parameters = all:suggestion

# Suppression preferences
dotnet_remove_unnecessary_suppression_exclusions = none

#### C# Coding Conventions ####
[*.cs]

# var preferences
csharp_style_var_elsewhere = false:silent
csharp_style_var_for_built_in_types = false:silent
csharp_style_var_when_type_is_apparent = false:silent

# Expression-bodied members
csharp_style_expression_bodied_accessors = true:silent
csharp_style_expression_bodied_constructors = false:silent
csharp_style_expression_bodied_indexers = true:silent
csharp_style_expression_bodied_lambdas = true:suggestion
csharp_style_expression_bodied_local_functions = false:silent
csharp_style_expression_bodied_methods = false:silent
csharp_style_expression_bodied_operators = false:silent
csharp_style_expression_bodied_properties = true:silent

# Pattern matching preferences
csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion
csharp_style_prefer_extended_property_pattern = true:suggestion
csharp_style_prefer_not_pattern = true:suggestion
csharp_style_prefer_pattern_matching = true:silent
csharp_style_prefer_switch_expression = true:suggestion

# Null-checking preferences
csharp_style_conditional_delegate_call = true:suggestion

# Modifier preferences
csharp_prefer_static_anonymous_function = true:suggestion
csharp_prefer_static_local_function = true:warning
csharp_preferred_modifier_order = public,private,protected,internal,file,const,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,required,volatile,async:suggestion
csharp_style_prefer_readonly_struct = true:suggestion
csharp_style_prefer_readonly_struct_member = true:suggestion

# Code-block preferences
csharp_prefer_braces = true:silent
csharp_prefer_simple_using_statement = true:suggestion
csharp_style_namespace_declarations = file_scoped:suggestion
csharp_style_prefer_method_group_conversion = true:silent
csharp_style_prefer_primary_constructors = true:suggestion
csharp_style_prefer_top_level_statements = true:silent

# Expression-level preferences
csharp_prefer_simple_default_expression = true:suggestion
csharp_style_deconstructed_variable_declaration = true:suggestion
csharp_style_implicit_object_creation_when_type_is_apparent = true:suggestion
csharp_style_inlined_variable_declaration = true:suggestion
csharp_style_prefer_index_operator = true:suggestion
csharp_style_prefer_local_over_anonymous_function = true:suggestion
csharp_style_prefer_null_check_over_type_check = true:suggestion
csharp_style_prefer_range_operator = true:suggestion
csharp_style_prefer_tuple_swap = true:suggestion
csharp_style_prefer_utf8_string_literals = true:suggestion
csharp_style_throw_expression = true:suggestion
csharp_style_unused_value_assignment_preference = discard_variable:suggestion
csharp_style_unused_value_expression_statement_preference = discard_variable:silent

# 'using' directive preferences
csharp_using_directive_placement = outside_namespace:silent

#### C# Formatting Rules ####

# New line preferences
csharp_new_line_before_catch = true
csharp_new_line_before_else = true
csharp_new_line_before_finally = true
csharp_new_line_before_members_in_anonymous_types = true
csharp_new_line_before_members_in_object_initializers = true
csharp_new_line_before_open_brace = all
csharp_new_line_between_query_expression_clauses = true

# Indentation preferences
csharp_indent_block_contents = true
csharp_indent_braces = false
csharp_indent_case_contents = true
csharp_indent_case_contents_when_block = true
csharp_indent_labels = one_less_than_current
csharp_indent_switch_labels = true

# Space preferences
csharp_space_after_cast = false
csharp_space_after_colon_in_inheritance_clause = true
csharp_space_after_comma = true
csharp_space_after_dot = false
csharp_space_after_keywords_in_control_flow_statements = true
csharp_space_after_semicolon_in_for_statement = true
csharp_space_around_binary_operators = before_and_after
csharp_space_around_declaration_statements = false
csharp_space_before_colon_in_inheritance_clause = true
csharp_space_before_comma = false
csharp_space_before_dot = false
csharp_space_before_open_square_brackets = false
csharp_space_before_semicolon_in_for_statement = false
csharp_space_between_empty_square_brackets = false
csharp_space_between_method_call_empty_parameter_list_parentheses = false
csharp_space_between_method_call_name_and_opening_parenthesis = false
csharp_space_between_method_call_parameter_list_parentheses = false
csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
csharp_space_between_method_declaration_name_and_open_parenthesis = false
csharp_space_between_method_declaration_parameter_list_parentheses = false
csharp_space_between_parentheses = false
csharp_space_between_square_brackets = false

# Wrapping preferences
csharp_preserve_single_line_blocks = true
csharp_preserve_single_line_statements = true

#### Naming styles ####
[*.{cs,vb}]

# Naming rules

dotnet_naming_rule.types_and_namespaces_should_be_pascalcase.severity = suggestion
dotnet_naming_rule.types_and_namespaces_should_be_pascalcase.symbols = types_and_namespaces
dotnet_naming_rule.types_and_namespaces_should_be_pascalcase.style = pascalcase

dotnet_naming_rule.interfaces_should_be_ipascalcase.severity = suggestion
dotnet_naming_rule.interfaces_should_be_ipascalcase.symbols = interfaces
dotnet_naming_rule.interfaces_should_be_ipascalcase.style = ipascalcase

dotnet_naming_rule.type_parameters_should_be_tpascalcase.severity = suggestion
dotnet_naming_rule.type_parameters_should_be_tpascalcase.symbols = type_parameters
dotnet_naming_rule.type_parameters_should_be_tpascalcase.style = tpascalcase

dotnet_naming_rule.methods_should_be_pascalcase.severity = suggestion
dotnet_naming_rule.methods_should_be_pascalcase.symbols = methods
dotnet_naming_rule.methods_should_be_pascalcase.style = pascalcase

dotnet_naming_rule.properties_should_be_pascalcase.severity = suggestion
dotnet_naming_rule.properties_should_be_pascalcase.symbols = properties
dotnet_naming_rule.properties_should_be_pascalcase.style = pascalcase

dotnet_naming_rule.events_should_be_pascalcase.severity = suggestion
dotnet_naming_rule.events_should_be_pascalcase.symbols = events
dotnet_naming_rule.events_should_be_pascalcase.style = pascalcase

dotnet_naming_rule.local_variables_should_be_camelcase.severity = suggestion
dotnet_naming_rule.local_variables_should_be_camelcase.symbols = local_variables
dotnet_naming_rule.local_variables_should_be_camelcase.style = camelcase

dotnet_naming_rule.local_constants_should_be_camelcase.severity = suggestion
dotnet_naming_rule.local_constants_should_be_camelcase.symbols = local_constants
dotnet_naming_rule.local_constants_should_be_camelcase.style = camelcase

dotnet_naming_rule.parameters_should_be_camelcase.severity = suggestion
dotnet_naming_rule.parameters_should_be_camelcase.symbols = parameters
dotnet_naming_rule.parameters_should_be_camelcase.style = camelcase

dotnet_naming_rule.public_fields_should_be_pascalcase.severity = suggestion
dotnet_naming_rule.public_fields_should_be_pascalcase.symbols = public_fields
dotnet_naming_rule.public_fields_should_be_pascalcase.style = pascalcase

dotnet_naming_rule.private_fields_should_be__camelcase.severity = suggestion
dotnet_naming_rule.private_fields_should_be__camelcase.symbols = private_fields
dotnet_naming_rule.private_fields_should_be__camelcase.style = _camelcase

dotnet_naming_rule.private_static_fields_should_be_s_camelcase.severity = suggestion
dotnet_naming_rule.private_static_fields_should_be_s_camelcase.symbols = private_static_fields
dotnet_naming_rule.private_static_fields_should_be_s_camelcase.style = s_camelcase

dotnet_naming_rule.public_constant_fields_should_be_pascalcase.severity = suggestion
dotnet_naming_rule.public_constant_fields_should_be_pascalcase.symbols = public_constant_fields
dotnet_naming_rule.public_constant_fields_should_be_pascalcase.style = pascalcase

dotnet_naming_rule.private_constant_fields_should_be_pascalcase.severity = suggestion
dotnet_naming_rule.private_constant_fields_should_be_pascalcase.symbols = private_constant_fields
dotnet_naming_rule.private_constant_fields_should_be_pascalcase.style = pascalcase

dotnet_naming_rule.public_static_readonly_fields_should_be_pascalcase.severity = suggestion
dotnet_naming_rule.public_static_readonly_fields_should_be_pascalcase.symbols = public_static_readonly_fields
dotnet_naming_rule.public_static_readonly_fields_should_be_pascalcase.style = pascalcase

dotnet_naming_rule.private_static_readonly_fields_should_be_pascalcase.severity = suggestion
dotnet_naming_rule.private_static_readonly_fields_should_be_pascalcase.symbols = private_static_readonly_fields
dotnet_naming_rule.private_static_readonly_fields_should_be_pascalcase.style = pascalcase

dotnet_naming_rule.enums_should_be_pascalcase.severity = suggestion
dotnet_naming_rule.enums_should_be_pascalcase.symbols = enums
dotnet_naming_rule.enums_should_be_pascalcase.style = pascalcase

dotnet_naming_rule.local_functions_should_be_pascalcase.severity = suggestion
dotnet_naming_rule.local_functions_should_be_pascalcase.symbols = local_functions
dotnet_naming_rule.local_functions_should_be_pascalcase.style = pascalcase

dotnet_naming_rule.non_field_members_should_be_pascalcase.severity = suggestion
dotnet_naming_rule.non_field_members_should_be_pascalcase.symbols = non_field_members
dotnet_naming_rule.non_field_members_should_be_pascalcase.style = pascalcase

# Symbol specifications

dotnet_naming_symbols.interfaces.applicable_kinds = interface
dotnet_naming_symbols.interfaces.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.interfaces.required_modifiers =

dotnet_naming_symbols.enums.applicable_kinds = enum
dotnet_naming_symbols.enums.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.enums.required_modifiers =

dotnet_naming_symbols.events.applicable_kinds = event
dotnet_naming_symbols.events.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.events.required_modifiers =

dotnet_naming_symbols.methods.applicable_kinds = method
dotnet_naming_symbols.methods.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.methods.required_modifiers =

dotnet_naming_symbols.properties.applicable_kinds = property
dotnet_naming_symbols.properties.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.properties.required_modifiers =

dotnet_naming_symbols.public_fields.applicable_kinds = field
dotnet_naming_symbols.public_fields.applicable_accessibilities = public, internal
dotnet_naming_symbols.public_fields.required_modifiers =

dotnet_naming_symbols.private_fields.applicable_kinds = field
dotnet_naming_symbols.private_fields.applicable_accessibilities = private, protected, protected_internal, private_protected
dotnet_naming_symbols.private_fields.required_modifiers =

dotnet_naming_symbols.private_static_fields.applicable_kinds = field
dotnet_naming_symbols.private_static_fields.applicable_accessibilities = private, protected, protected_internal, private_protected
dotnet_naming_symbols.private_static_fields.required_modifiers = static

dotnet_naming_symbols.types_and_namespaces.applicable_kinds = namespace, class, struct, interface, enum
dotnet_naming_symbols.types_and_namespaces.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.types_and_namespaces.required_modifiers =

dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method
dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.non_field_members.required_modifiers =

dotnet_naming_symbols.type_parameters.applicable_kinds = namespace
dotnet_naming_symbols.type_parameters.applicable_accessibilities = *
dotnet_naming_symbols.type_parameters.required_modifiers =

dotnet_naming_symbols.private_constant_fields.applicable_kinds = field
dotnet_naming_symbols.private_constant_fields.applicable_accessibilities = private, protected, protected_internal, private_protected
dotnet_naming_symbols.private_constant_fields.required_modifiers = const

dotnet_naming_symbols.local_variables.applicable_kinds = local
dotnet_naming_symbols.local_variables.applicable_accessibilities = local
dotnet_naming_symbols.local_variables.required_modifiers =

dotnet_naming_symbols.local_constants.applicable_kinds = local
dotnet_naming_symbols.local_constants.applicable_accessibilities = local
dotnet_naming_symbols.local_constants.required_modifiers = const

dotnet_naming_symbols.parameters.applicable_kinds = parameter
dotnet_naming_symbols.parameters.applicable_accessibilities = *
dotnet_naming_symbols.parameters.required_modifiers =

dotnet_naming_symbols.public_constant_fields.applicable_kinds = field
dotnet_naming_symbols.public_constant_fields.applicable_accessibilities = public, internal
dotnet_naming_symbols.public_constant_fields.required_modifiers = const

dotnet_naming_symbols.public_static_readonly_fields.applicable_kinds = field
dotnet_naming_symbols.public_static_readonly_fields.applicable_accessibilities = public, internal
dotnet_naming_symbols.public_static_readonly_fields.required_modifiers = readonly, static

dotnet_naming_symbols.private_static_readonly_fields.applicable_kinds = field
dotnet_naming_symbols.private_static_readonly_fields.applicable_accessibilities = private, protected, protected_internal, private_protected
dotnet_naming_symbols.private_static_readonly_fields.required_modifiers = readonly, static

dotnet_naming_symbols.local_functions.applicable_kinds = local_function
dotnet_naming_symbols.local_functions.applicable_accessibilities = *
dotnet_naming_symbols.local_functions.required_modifiers =

# Naming styles

dotnet_naming_style.pascalcase.required_prefix =
dotnet_naming_style.pascalcase.required_suffix =
dotnet_naming_style.pascalcase.word_separator =
dotnet_naming_style.pascalcase.capitalization = pascal_case

dotnet_naming_style.ipascalcase.required_prefix = I
dotnet_naming_style.ipascalcase.required_suffix =
dotnet_naming_style.ipascalcase.word_separator =
dotnet_naming_style.ipascalcase.capitalization = pascal_case

dotnet_naming_style.tpascalcase.required_prefix = T
dotnet_naming_style.tpascalcase.required_suffix =
dotnet_naming_style.tpascalcase.word_separator =
dotnet_naming_style.tpascalcase.capitalization = pascal_case

dotnet_naming_style._camelcase.required_prefix = _
dotnet_naming_style._camelcase.required_suffix =
dotnet_naming_style._camelcase.word_separator =
dotnet_naming_style._camelcase.capitalization = camel_case

dotnet_naming_style.camelcase.required_prefix =
dotnet_naming_style.camelcase.required_suffix =
dotnet_naming_style.camelcase.word_separator =
dotnet_naming_style.camelcase.capitalization = camel_case

dotnet_naming_style.s_camelcase.required_prefix = s_
dotnet_naming_style.s_camelcase.required_suffix =
dotnet_naming_style.s_camelcase.word_separator =
dotnet_naming_style.s_camelcase.capitalization = camel_case


# Test method names use the xUnit/NUnit Method_Scenario_Expected convention.
[tests/**.cs]
dotnet_diagnostic.CA1707.severity = none

+ 5
- 0
.gitignore 查看文件

@@ -19,3 +19,8 @@ secrets.json
# Common local environment files
.env
.env.*

# Local SQLite database files
*.db
*.db-shm
*.db-wal

+ 15
- 0
.idea/.idea.CartWise/.idea/.gitignore 查看文件

@@ -0,0 +1,15 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Rider ignored files
/modules.xml
/contentModel.xml
/projectSettingsUpdater.xml
/.idea.CartWise.iml
# Ignored default folder with query files
/queries/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

+ 4
- 0
.idea/.idea.CartWise/.idea/encodings.xml 查看文件

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding" addBOMForNewFiles="with BOM under Windows, with no BOM otherwise" />
</project>

+ 8
- 0
.idea/.idea.CartWise/.idea/indexLayout.xml 查看文件

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="UserContentModel">
<attachedFolders />
<explicitIncludes />
<explicitExcludes />
</component>
</project>

+ 6
- 0
.idea/.idea.CartWise/.idea/vcs.xml 查看文件

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

+ 131
- 0
CartWise.sln 查看文件

@@ -0,0 +1,131 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{827E0CD3-B72D-47B6-A68D-7590B98EB39B}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CartWise.Web", "src\CartWise.Web\CartWise.Web.csproj", "{FDDDE4AB-208C-40D4-A321-C311A04CC73D}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CartWise.Application", "src\CartWise.Application\CartWise.Application.csproj", "{92805B6D-D1F3-4B46-BD2E-1FC1CB4C728C}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CartWise.Domain", "src\CartWise.Domain\CartWise.Domain.csproj", "{F270FA77-D1EF-4364-91E2-A529A1F8328E}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CartWise.Infrastructure", "src\CartWise.Infrastructure\CartWise.Infrastructure.csproj", "{4220F46A-4081-4E63-B94E-CB9CA45BFA4F}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{0AB3BF05-4346-4AA6-1389-037BE0695223}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CartWise.Domain.Tests", "tests\CartWise.Domain.Tests\CartWise.Domain.Tests.csproj", "{A8DDB5F7-B504-43ED-80D0-D355803F22CB}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CartWise.Application.Tests", "tests\CartWise.Application.Tests\CartWise.Application.Tests.csproj", "{5D1E916D-65A2-4F5F-AC2C-362F73DC65E8}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CartWise.Web.Tests", "tests\CartWise.Web.Tests\CartWise.Web.Tests.csproj", "{332ADE94-7947-4D34-97F0-9C97FD7684AD}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{FDDDE4AB-208C-40D4-A321-C311A04CC73D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{FDDDE4AB-208C-40D4-A321-C311A04CC73D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FDDDE4AB-208C-40D4-A321-C311A04CC73D}.Debug|x64.ActiveCfg = Debug|Any CPU
{FDDDE4AB-208C-40D4-A321-C311A04CC73D}.Debug|x64.Build.0 = Debug|Any CPU
{FDDDE4AB-208C-40D4-A321-C311A04CC73D}.Debug|x86.ActiveCfg = Debug|Any CPU
{FDDDE4AB-208C-40D4-A321-C311A04CC73D}.Debug|x86.Build.0 = Debug|Any CPU
{FDDDE4AB-208C-40D4-A321-C311A04CC73D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FDDDE4AB-208C-40D4-A321-C311A04CC73D}.Release|Any CPU.Build.0 = Release|Any CPU
{FDDDE4AB-208C-40D4-A321-C311A04CC73D}.Release|x64.ActiveCfg = Release|Any CPU
{FDDDE4AB-208C-40D4-A321-C311A04CC73D}.Release|x64.Build.0 = Release|Any CPU
{FDDDE4AB-208C-40D4-A321-C311A04CC73D}.Release|x86.ActiveCfg = Release|Any CPU
{FDDDE4AB-208C-40D4-A321-C311A04CC73D}.Release|x86.Build.0 = Release|Any CPU
{92805B6D-D1F3-4B46-BD2E-1FC1CB4C728C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{92805B6D-D1F3-4B46-BD2E-1FC1CB4C728C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{92805B6D-D1F3-4B46-BD2E-1FC1CB4C728C}.Debug|x64.ActiveCfg = Debug|Any CPU
{92805B6D-D1F3-4B46-BD2E-1FC1CB4C728C}.Debug|x64.Build.0 = Debug|Any CPU
{92805B6D-D1F3-4B46-BD2E-1FC1CB4C728C}.Debug|x86.ActiveCfg = Debug|Any CPU
{92805B6D-D1F3-4B46-BD2E-1FC1CB4C728C}.Debug|x86.Build.0 = Debug|Any CPU
{92805B6D-D1F3-4B46-BD2E-1FC1CB4C728C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{92805B6D-D1F3-4B46-BD2E-1FC1CB4C728C}.Release|Any CPU.Build.0 = Release|Any CPU
{92805B6D-D1F3-4B46-BD2E-1FC1CB4C728C}.Release|x64.ActiveCfg = Release|Any CPU
{92805B6D-D1F3-4B46-BD2E-1FC1CB4C728C}.Release|x64.Build.0 = Release|Any CPU
{92805B6D-D1F3-4B46-BD2E-1FC1CB4C728C}.Release|x86.ActiveCfg = Release|Any CPU
{92805B6D-D1F3-4B46-BD2E-1FC1CB4C728C}.Release|x86.Build.0 = Release|Any CPU
{F270FA77-D1EF-4364-91E2-A529A1F8328E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F270FA77-D1EF-4364-91E2-A529A1F8328E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F270FA77-D1EF-4364-91E2-A529A1F8328E}.Debug|x64.ActiveCfg = Debug|Any CPU
{F270FA77-D1EF-4364-91E2-A529A1F8328E}.Debug|x64.Build.0 = Debug|Any CPU
{F270FA77-D1EF-4364-91E2-A529A1F8328E}.Debug|x86.ActiveCfg = Debug|Any CPU
{F270FA77-D1EF-4364-91E2-A529A1F8328E}.Debug|x86.Build.0 = Debug|Any CPU
{F270FA77-D1EF-4364-91E2-A529A1F8328E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F270FA77-D1EF-4364-91E2-A529A1F8328E}.Release|Any CPU.Build.0 = Release|Any CPU
{F270FA77-D1EF-4364-91E2-A529A1F8328E}.Release|x64.ActiveCfg = Release|Any CPU
{F270FA77-D1EF-4364-91E2-A529A1F8328E}.Release|x64.Build.0 = Release|Any CPU
{F270FA77-D1EF-4364-91E2-A529A1F8328E}.Release|x86.ActiveCfg = Release|Any CPU
{F270FA77-D1EF-4364-91E2-A529A1F8328E}.Release|x86.Build.0 = Release|Any CPU
{4220F46A-4081-4E63-B94E-CB9CA45BFA4F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4220F46A-4081-4E63-B94E-CB9CA45BFA4F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4220F46A-4081-4E63-B94E-CB9CA45BFA4F}.Debug|x64.ActiveCfg = Debug|Any CPU
{4220F46A-4081-4E63-B94E-CB9CA45BFA4F}.Debug|x64.Build.0 = Debug|Any CPU
{4220F46A-4081-4E63-B94E-CB9CA45BFA4F}.Debug|x86.ActiveCfg = Debug|Any CPU
{4220F46A-4081-4E63-B94E-CB9CA45BFA4F}.Debug|x86.Build.0 = Debug|Any CPU
{4220F46A-4081-4E63-B94E-CB9CA45BFA4F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4220F46A-4081-4E63-B94E-CB9CA45BFA4F}.Release|Any CPU.Build.0 = Release|Any CPU
{4220F46A-4081-4E63-B94E-CB9CA45BFA4F}.Release|x64.ActiveCfg = Release|Any CPU
{4220F46A-4081-4E63-B94E-CB9CA45BFA4F}.Release|x64.Build.0 = Release|Any CPU
{4220F46A-4081-4E63-B94E-CB9CA45BFA4F}.Release|x86.ActiveCfg = Release|Any CPU
{4220F46A-4081-4E63-B94E-CB9CA45BFA4F}.Release|x86.Build.0 = Release|Any CPU
{A8DDB5F7-B504-43ED-80D0-D355803F22CB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A8DDB5F7-B504-43ED-80D0-D355803F22CB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A8DDB5F7-B504-43ED-80D0-D355803F22CB}.Debug|x64.ActiveCfg = Debug|Any CPU
{A8DDB5F7-B504-43ED-80D0-D355803F22CB}.Debug|x64.Build.0 = Debug|Any CPU
{A8DDB5F7-B504-43ED-80D0-D355803F22CB}.Debug|x86.ActiveCfg = Debug|Any CPU
{A8DDB5F7-B504-43ED-80D0-D355803F22CB}.Debug|x86.Build.0 = Debug|Any CPU
{A8DDB5F7-B504-43ED-80D0-D355803F22CB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A8DDB5F7-B504-43ED-80D0-D355803F22CB}.Release|Any CPU.Build.0 = Release|Any CPU
{A8DDB5F7-B504-43ED-80D0-D355803F22CB}.Release|x64.ActiveCfg = Release|Any CPU
{A8DDB5F7-B504-43ED-80D0-D355803F22CB}.Release|x64.Build.0 = Release|Any CPU
{A8DDB5F7-B504-43ED-80D0-D355803F22CB}.Release|x86.ActiveCfg = Release|Any CPU
{A8DDB5F7-B504-43ED-80D0-D355803F22CB}.Release|x86.Build.0 = Release|Any CPU
{5D1E916D-65A2-4F5F-AC2C-362F73DC65E8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5D1E916D-65A2-4F5F-AC2C-362F73DC65E8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5D1E916D-65A2-4F5F-AC2C-362F73DC65E8}.Debug|x64.ActiveCfg = Debug|Any CPU
{5D1E916D-65A2-4F5F-AC2C-362F73DC65E8}.Debug|x64.Build.0 = Debug|Any CPU
{5D1E916D-65A2-4F5F-AC2C-362F73DC65E8}.Debug|x86.ActiveCfg = Debug|Any CPU
{5D1E916D-65A2-4F5F-AC2C-362F73DC65E8}.Debug|x86.Build.0 = Debug|Any CPU
{5D1E916D-65A2-4F5F-AC2C-362F73DC65E8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5D1E916D-65A2-4F5F-AC2C-362F73DC65E8}.Release|Any CPU.Build.0 = Release|Any CPU
{5D1E916D-65A2-4F5F-AC2C-362F73DC65E8}.Release|x64.ActiveCfg = Release|Any CPU
{5D1E916D-65A2-4F5F-AC2C-362F73DC65E8}.Release|x64.Build.0 = Release|Any CPU
{5D1E916D-65A2-4F5F-AC2C-362F73DC65E8}.Release|x86.ActiveCfg = Release|Any CPU
{5D1E916D-65A2-4F5F-AC2C-362F73DC65E8}.Release|x86.Build.0 = Release|Any CPU
{332ADE94-7947-4D34-97F0-9C97FD7684AD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{332ADE94-7947-4D34-97F0-9C97FD7684AD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{332ADE94-7947-4D34-97F0-9C97FD7684AD}.Debug|x64.ActiveCfg = Debug|Any CPU
{332ADE94-7947-4D34-97F0-9C97FD7684AD}.Debug|x64.Build.0 = Debug|Any CPU
{332ADE94-7947-4D34-97F0-9C97FD7684AD}.Debug|x86.ActiveCfg = Debug|Any CPU
{332ADE94-7947-4D34-97F0-9C97FD7684AD}.Debug|x86.Build.0 = Debug|Any CPU
{332ADE94-7947-4D34-97F0-9C97FD7684AD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{332ADE94-7947-4D34-97F0-9C97FD7684AD}.Release|Any CPU.Build.0 = Release|Any CPU
{332ADE94-7947-4D34-97F0-9C97FD7684AD}.Release|x64.ActiveCfg = Release|Any CPU
{332ADE94-7947-4D34-97F0-9C97FD7684AD}.Release|x64.Build.0 = Release|Any CPU
{332ADE94-7947-4D34-97F0-9C97FD7684AD}.Release|x86.ActiveCfg = Release|Any CPU
{332ADE94-7947-4D34-97F0-9C97FD7684AD}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{FDDDE4AB-208C-40D4-A321-C311A04CC73D} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
{92805B6D-D1F3-4B46-BD2E-1FC1CB4C728C} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
{F270FA77-D1EF-4364-91E2-A529A1F8328E} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
{4220F46A-4081-4E63-B94E-CB9CA45BFA4F} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
{A8DDB5F7-B504-43ED-80D0-D355803F22CB} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
{5D1E916D-65A2-4F5F-AC2C-362F73DC65E8} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
{332ADE94-7947-4D34-97F0-9C97FD7684AD} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
EndGlobalSection
EndGlobal

+ 12
- 0
Directory.Build.props 查看文件

@@ -0,0 +1,12 @@
<Project>

<PropertyGroup>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<EnableNETAnalyzers>true</EnableNETAnalyzers>
<AnalysisLevel>latest-recommended</AnalysisLevel>
<AnalysisMode>Recommended</AnalysisMode>
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
</PropertyGroup>

</Project>

+ 15
- 10
README.md 查看文件

@@ -142,7 +142,14 @@ tests/

## Development Status

This repository is being initialized from the `AGENTS.md` build specification. The current focus is a solo-founder MVP targeted for local completion by **September 10, 2026**.
This repository is being built from the `AGENTS.md` build specification. The current focus is a solo-founder MVP targeted for local completion by **September 10, 2026**.

Progress so far (see `docs/scrum-backlog.md` for the authoritative task-level state):

- [x] `CW-STORY-01.1` Solution skeleton — 4 src projects + 3 test projects, correct dependency direction, nullable enabled
- [x] `CW-STORY-01.2` Application startup — MVC shell runs, SQLite wired via `CartWiseDbContext`, `wwwroot` structure organized
- [x] `CW-STORY-01.3` ASP.NET Core Identity authentication — register, sign in, sign out
- [x] `CW-STORY-01.4` Engineering baseline — shared analyzer settings, `.editorconfig`, tuned logging, smoke tests

## Project Planning

@@ -155,15 +162,9 @@ This repository is being initialized from the `AGENTS.md` build specification. T

## Getting Started

The solution scaffolding is planned but may not yet be present in this repository. Once the projects are created, the expected local development flow will be:

1. Install the .NET SDK
2. Use SQLite for local MVP development
3. Configure connection settings via environment variables, app settings, or user secrets as needed
4. Run EF Core migrations or schema creation steps
5. Start the ASP.NET Core MVC app

Example commands that will be used once the solution exists:
1. Install the .NET SDK (developed against .NET 10)
2. Restore, build, and test the solution
3. Run the web app — it uses SQLite by default (`ConnectionStrings:DefaultConnection` in `src/CartWise.Web/appsettings.json`, pointing at `App_Data/cartwise.db`)

```bash
dotnet restore
@@ -172,6 +173,10 @@ dotnet test
dotnet run --project src/CartWise.Web
```

`dotnet restore`, `dotnet build`, and `dotnet test` (in that order, against `CartWise.sln`) are the commands a CI pipeline should run — there is no separate CI-only script. `dotnet build` fails the same way locally and in CI since analyzer rules are enforced via `Directory.Build.props` (`EnableNETAnalyzers`, `AnalysisLevel=latest-recommended`) rather than a separate lint step.

Registration and sign-in are available at `/Account/Register` and `/Account/Login`. The Identity schema migration applies automatically on startup in the Development environment. Domain entities (household, list, etc.) and their first migration land in `CW-EPIC-02` (Household and Access Control).

## Backlog and Delivery

The team plans to manage this project using Scrum with:


+ 1
- 0
docs/decision-log.md 查看文件

@@ -64,6 +64,7 @@ This file records product, scope, architecture, and delivery decisions for CartW
- Reason: SQLite lowers setup friction and is better suited for a fast local-first development loop.
- Impact: Foundation stories should wire SQLite first while keeping a future PostgreSQL path possible.
- Revisit Trigger: Hosted or multi-user production deployment becomes the priority.
- Implementation Note (2026-08-10, `CW-STORY-01.2`): `CartWiseDbContext` lives in `CartWise.Infrastructure/Data` and is wired to SQLite only through `InfrastructureServiceCollectionExtensions.AddInfrastructure`, via `options.UseSqlite(...)` and the `ConnectionStrings:DefaultConnection` setting in `appsettings.json`. Moving to PostgreSQL later is expected to mean swapping `UseSqlite` for `UseNpgsql` and the connection string in that single extension method — no changes anticipated in `CartWise.Domain` or `CartWise.Application`.

### DEC-006 - Local-first release strategy
- Date: 2026-08-10


+ 3
- 3
docs/release-mvp-checklist.md 查看文件

@@ -23,10 +23,10 @@ This checklist applies only to the MVP scope currently defined in `docs/scrum-ba

## Technical Outcomes

- [ ] Solution builds locally
- [x] Solution builds locally
- [ ] Tests for implemented business rules pass locally
- [ ] SQLite is wired and used for MVP development
- [ ] Migrations or schema creation flow are documented and verified
- [x] SQLite is wired and used for MVP development
- [x] Migrations or schema creation flow are documented and verified
- [ ] Anti-forgery protection is enforced on state-changing requests
- [ ] Household authorization is enforced on every household-scoped workflow
- [ ] Validation messages are shown for key invalid inputs


+ 34
- 26
docs/scrum-backlog.md 查看文件

@@ -120,16 +120,18 @@ A story is done when:
- Nullable reference types are enabled

**Tasks**
- [ ] Create `CartWise.sln`
- [ ] Create `src/CartWise.Web`
- [ ] Create `src/CartWise.Application`
- [ ] Create `src/CartWise.Domain`
- [ ] Create `src/CartWise.Infrastructure`
- [ ] Create `tests/CartWise.Domain.Tests`
- [ ] Create `tests/CartWise.Application.Tests`
- [ ] Create `tests/CartWise.Web.Tests`
- [ ] Add project references
- [ ] Enable nullable reference types
- [x] Create `CartWise.sln`
- [x] Create `src/CartWise.Web`
- [x] Create `src/CartWise.Application`
- [x] Create `src/CartWise.Domain`
- [x] Create `src/CartWise.Infrastructure`
- [x] Create `tests/CartWise.Domain.Tests`
- [x] Create `tests/CartWise.Application.Tests`
- [x] Create `tests/CartWise.Web.Tests`
- [x] Add project references
- [x] Enable nullable reference types

**Status:** Done — solution builds and baseline template tests pass across all three test projects. `CartWise.Web` references `CartWise.Infrastructure` in addition to `CartWise.Application` for DI/startup registration (allowed per `AGENTS.md` §22 Phase 0).

### `CW-STORY-01.2` Configure application startup
**Release:** MVP
@@ -145,13 +147,15 @@ A story is done when:
- PostgreSQL migration path remains possible later

**Tasks**
- [ ] Configure MVC services and middleware
- [ ] Add base layout and navigation shell
- [ ] Organize `wwwroot` structure
- [ ] Add configuration bindings
- [ ] Wire SQLite connection string for MVP
- [ ] Note PostgreSQL transition path in docs
- [ ] Verify local app startup
- [x] Configure MVC services and middleware
- [x] Add base layout and navigation shell
- [x] Organize `wwwroot` structure
- [x] Add configuration bindings
- [x] Wire SQLite connection string for MVP
- [x] Note PostgreSQL transition path in docs
- [x] Verify local app startup

**Status:** Done — app starts and serves the rebranded layout and static assets on a local port (verified via `dotnet run` + curl smoke check). `CartWiseDbContext` (empty for now — entities land in `CW-EPIC-02`) is wired to SQLite through `CartWise.Infrastructure.InfrastructureServiceCollectionExtensions.AddInfrastructure`, using `ConnectionStrings:DefaultConnection` in `appsettings.json`. PostgreSQL transition path documented in `docs/decision-log.md` under `DEC-005`.

### `CW-STORY-01.3` Add Identity authentication
**Release:** MVP
@@ -165,11 +169,13 @@ A story is done when:
- Identity persistence is configured

**Tasks**
- [ ] Add ASP.NET Core Identity
- [ ] Create `ApplicationUser`
- [ ] Configure Identity persistence
- [ ] Scaffold auth UI
- [ ] Verify login and logout flow
- [x] Add ASP.NET Core Identity
- [x] Create `ApplicationUser`
- [x] Configure Identity persistence
- [x] Scaffold auth UI
- [x] Verify login and logout flow

**Status:** Done — `ApplicationUser : IdentityUser` (`DisplayName`, `CreatedUtc`) lives in `CartWise.Infrastructure.Identity`; `CartWiseDbContext` is now `IdentityDbContext<ApplicationUser>`. Identity is registered via `AddInfrastructure` (`AddIdentity` + `AddEntityFrameworkStores<CartWiseDbContext>`). Auth UI is a conventional MVC `AccountController` (`Register`/`Login`/`Logout`) with matching Razor views under `Views/Account`, anti-forgery on all POSTs, open-redirect guarded via `Url.IsLocalUrl`. `HomeController.Privacy` carries `[Authorize]` as the protected-route proof. Initial `InitialIdentity` EF Core migration created in `CartWise.Infrastructure/Data/Migrations`; the Development environment auto-applies pending migrations on startup (`Program.cs`) so local setup needs no manual `dotnet ef database update` step. Verified end-to-end via a live `dotnet run` + curl pass: anonymous access to a protected route redirects to `/Account/Login?ReturnUrl=...`, register auto-signs-in, logout clears the session, login re-authenticates, and a wrong password is rejected with a clear error.

### `CW-STORY-01.4` Establish engineering baseline
**Release:** MVP
@@ -183,10 +189,12 @@ A story is done when:
- Logging defaults are configured

**Tasks**
- [ ] Add analyzer and formatting settings
- [ ] Configure structured logging baseline
- [ ] Add smoke test coverage
- [ ] Add CI-oriented build and test commands to docs
- [x] Add analyzer and formatting settings
- [x] Configure structured logging baseline
- [x] Add smoke test coverage
- [x] Add CI-oriented build and test commands to docs

**Status:** Done — `Directory.Build.props` centralizes `Nullable`, `ImplicitUsings`, and .NET analyzers (`EnableNETAnalyzers`, `AnalysisLevel=latest-recommended`, `EnforceCodeStyleInBuild`) across all 7 projects; per-project `.csproj` files no longer duplicate those settings. Root `.editorconfig` added (generated via `dotnet new editorconfig`), with a scoped override so `CA1707` doesn't fight the xUnit `Method_Scenario_Expected` naming convention under `tests/`. Logging defaults tuned in `appsettings.json` (quiet `Microsoft.EntityFrameworkCore`/`Identity` categories) and `appsettings.Development.json` (`Microsoft.EntityFrameworkCore.Database.Command: Information` to see generated SQL locally). Smoke coverage added where there's real behavior to smoke — a `CartWiseWebApplicationFactory`-based integration suite (`tests/CartWise.Web.Tests/SmokeTests.cs`) boots the full app (DI, EF Core/SQLite, Identity, routing) against an isolated temp SQLite file and asserts: home page loads, an anonymous request to an `[Authorize]` route redirects to `/Account/Login`, and both auth pages load. `CartWise.Domain.Tests`/`CartWise.Application.Tests` still carry only the template placeholder test — there's no real domain/application logic yet to smoke-test; meaningful coverage there starts with `CW-STORY-02.1`. CI-oriented commands documented in `README.md` (`dotnet restore/build/test` against `CartWise.sln`; analyzers enforced at build time, not a separate lint step).

---



+ 11
- 0
src/CartWise.Application/CartWise.Application.csproj 查看文件

@@ -0,0 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">

<ItemGroup>
<ProjectReference Include="..\CartWise.Domain\CartWise.Domain.csproj" />
</ItemGroup>

<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
</PropertyGroup>

</Project>

+ 7
- 0
src/CartWise.Domain/CartWise.Domain.csproj 查看文件

@@ -0,0 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
</PropertyGroup>

</Project>

+ 26
- 0
src/CartWise.Infrastructure/CartWise.Infrastructure.csproj 查看文件

@@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">

<ItemGroup>
<ProjectReference Include="..\CartWise.Application\CartWise.Application.csproj" />
<ProjectReference Include="..\CartWise.Domain\CartWise.Domain.csproj" />
</ItemGroup>

<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="10.0.10" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.10">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.10" />
<PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="2.1.12" />
</ItemGroup>

<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
</PropertyGroup>

</Project>

+ 13
- 0
src/CartWise.Infrastructure/Data/CartWiseDbContext.cs 查看文件

@@ -0,0 +1,13 @@
using CartWise.Infrastructure.Identity;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;

namespace CartWise.Infrastructure.Data;

public class CartWiseDbContext : IdentityDbContext<ApplicationUser>
{
public CartWiseDbContext(DbContextOptions<CartWiseDbContext> options)
: base(options)
{
}
}

+ 275
- 0
src/CartWise.Infrastructure/Data/Migrations/20260810153611_InitialIdentity.Designer.cs 查看文件

@@ -0,0 +1,275 @@
// <auto-generated />
using System;
using CartWise.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;

#nullable disable

namespace CartWise.Infrastructure.Data.Migrations
{
[DbContext(typeof(CartWiseDbContext))]
[Migration("20260810153611_InitialIdentity")]
partial class InitialIdentity
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "10.0.10");

modelBuilder.Entity("CartWise.Infrastructure.Identity.ApplicationUser", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT");

b.Property<int>("AccessFailedCount")
.HasColumnType("INTEGER");

b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("TEXT");

b.Property<DateTime>("CreatedUtc")
.HasColumnType("TEXT");

b.Property<string>("DisplayName")
.IsRequired()
.HasColumnType("TEXT");

b.Property<string>("Email")
.HasMaxLength(256)
.HasColumnType("TEXT");

b.Property<bool>("EmailConfirmed")
.HasColumnType("INTEGER");

b.Property<bool>("LockoutEnabled")
.HasColumnType("INTEGER");

b.Property<DateTimeOffset?>("LockoutEnd")
.HasColumnType("TEXT");

b.Property<string>("NormalizedEmail")
.HasMaxLength(256)
.HasColumnType("TEXT");

b.Property<string>("NormalizedUserName")
.HasMaxLength(256)
.HasColumnType("TEXT");

b.Property<string>("PasswordHash")
.HasColumnType("TEXT");

b.Property<string>("PhoneNumber")
.HasColumnType("TEXT");

b.Property<bool>("PhoneNumberConfirmed")
.HasColumnType("INTEGER");

b.Property<string>("SecurityStamp")
.HasColumnType("TEXT");

b.Property<bool>("TwoFactorEnabled")
.HasColumnType("INTEGER");

b.Property<string>("UserName")
.HasMaxLength(256)
.HasColumnType("TEXT");

b.HasKey("Id");

b.HasIndex("NormalizedEmail")
.HasDatabaseName("EmailIndex");

b.HasIndex("NormalizedUserName")
.IsUnique()
.HasDatabaseName("UserNameIndex");

b.ToTable("AspNetUsers", (string)null);
});

modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT");

b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("TEXT");

b.Property<string>("Name")
.HasMaxLength(256)
.HasColumnType("TEXT");

b.Property<string>("NormalizedName")
.HasMaxLength(256)
.HasColumnType("TEXT");

b.HasKey("Id");

b.HasIndex("NormalizedName")
.IsUnique()
.HasDatabaseName("RoleNameIndex");

b.ToTable("AspNetRoles", (string)null);
});

modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");

b.Property<string>("ClaimType")
.HasColumnType("TEXT");

b.Property<string>("ClaimValue")
.HasColumnType("TEXT");

b.Property<string>("RoleId")
.IsRequired()
.HasColumnType("TEXT");

b.HasKey("Id");

b.HasIndex("RoleId");

b.ToTable("AspNetRoleClaims", (string)null);
});

modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");

b.Property<string>("ClaimType")
.HasColumnType("TEXT");

b.Property<string>("ClaimValue")
.HasColumnType("TEXT");

b.Property<string>("UserId")
.IsRequired()
.HasColumnType("TEXT");

b.HasKey("Id");

b.HasIndex("UserId");

b.ToTable("AspNetUserClaims", (string)null);
});

modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.Property<string>("LoginProvider")
.HasColumnType("TEXT");

b.Property<string>("ProviderKey")
.HasColumnType("TEXT");

b.Property<string>("ProviderDisplayName")
.HasColumnType("TEXT");

b.Property<string>("UserId")
.IsRequired()
.HasColumnType("TEXT");

b.HasKey("LoginProvider", "ProviderKey");

b.HasIndex("UserId");

b.ToTable("AspNetUserLogins", (string)null);
});

modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("TEXT");

b.Property<string>("RoleId")
.HasColumnType("TEXT");

b.HasKey("UserId", "RoleId");

b.HasIndex("RoleId");

b.ToTable("AspNetUserRoles", (string)null);
});

modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("TEXT");

b.Property<string>("LoginProvider")
.HasColumnType("TEXT");

b.Property<string>("Name")
.HasColumnType("TEXT");

b.Property<string>("Value")
.HasColumnType("TEXT");

b.HasKey("UserId", "LoginProvider", "Name");

b.ToTable("AspNetUserTokens", (string)null);
});

modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});

modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.HasOne("CartWise.Infrastructure.Identity.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});

modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.HasOne("CartWise.Infrastructure.Identity.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});

modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();

b.HasOne("CartWise.Infrastructure.Identity.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});

modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.HasOne("CartWise.Infrastructure.Identity.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
#pragma warning restore 612, 618
}
}
}

+ 224
- 0
src/CartWise.Infrastructure/Data/Migrations/20260810153611_InitialIdentity.cs 查看文件

@@ -0,0 +1,224 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;

#nullable disable

namespace CartWise.Infrastructure.Data.Migrations
{
/// <inheritdoc />
public partial class InitialIdentity : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "AspNetRoles",
columns: table => new
{
Id = table.Column<string>(type: "TEXT", nullable: false),
Name = table.Column<string>(type: "TEXT", maxLength: 256, nullable: true),
NormalizedName = table.Column<string>(type: "TEXT", maxLength: 256, nullable: true),
ConcurrencyStamp = table.Column<string>(type: "TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_AspNetRoles", x => x.Id);
});

migrationBuilder.CreateTable(
name: "AspNetUsers",
columns: table => new
{
Id = table.Column<string>(type: "TEXT", nullable: false),
DisplayName = table.Column<string>(type: "TEXT", nullable: false),
CreatedUtc = table.Column<DateTime>(type: "TEXT", nullable: false),
UserName = table.Column<string>(type: "TEXT", maxLength: 256, nullable: true),
NormalizedUserName = table.Column<string>(type: "TEXT", maxLength: 256, nullable: true),
Email = table.Column<string>(type: "TEXT", maxLength: 256, nullable: true),
NormalizedEmail = table.Column<string>(type: "TEXT", maxLength: 256, nullable: true),
EmailConfirmed = table.Column<bool>(type: "INTEGER", nullable: false),
PasswordHash = table.Column<string>(type: "TEXT", nullable: true),
SecurityStamp = table.Column<string>(type: "TEXT", nullable: true),
ConcurrencyStamp = table.Column<string>(type: "TEXT", nullable: true),
PhoneNumber = table.Column<string>(type: "TEXT", nullable: true),
PhoneNumberConfirmed = table.Column<bool>(type: "INTEGER", nullable: false),
TwoFactorEnabled = table.Column<bool>(type: "INTEGER", nullable: false),
LockoutEnd = table.Column<DateTimeOffset>(type: "TEXT", nullable: true),
LockoutEnabled = table.Column<bool>(type: "INTEGER", nullable: false),
AccessFailedCount = table.Column<int>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_AspNetUsers", x => x.Id);
});

migrationBuilder.CreateTable(
name: "AspNetRoleClaims",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
RoleId = table.Column<string>(type: "TEXT", nullable: false),
ClaimType = table.Column<string>(type: "TEXT", nullable: true),
ClaimValue = table.Column<string>(type: "TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_AspNetRoleClaims", x => x.Id);
table.ForeignKey(
name: "FK_AspNetRoleClaims_AspNetRoles_RoleId",
column: x => x.RoleId,
principalTable: "AspNetRoles",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});

migrationBuilder.CreateTable(
name: "AspNetUserClaims",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
UserId = table.Column<string>(type: "TEXT", nullable: false),
ClaimType = table.Column<string>(type: "TEXT", nullable: true),
ClaimValue = table.Column<string>(type: "TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_AspNetUserClaims", x => x.Id);
table.ForeignKey(
name: "FK_AspNetUserClaims_AspNetUsers_UserId",
column: x => x.UserId,
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});

migrationBuilder.CreateTable(
name: "AspNetUserLogins",
columns: table => new
{
LoginProvider = table.Column<string>(type: "TEXT", nullable: false),
ProviderKey = table.Column<string>(type: "TEXT", nullable: false),
ProviderDisplayName = table.Column<string>(type: "TEXT", nullable: true),
UserId = table.Column<string>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_AspNetUserLogins", x => new { x.LoginProvider, x.ProviderKey });
table.ForeignKey(
name: "FK_AspNetUserLogins_AspNetUsers_UserId",
column: x => x.UserId,
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});

migrationBuilder.CreateTable(
name: "AspNetUserRoles",
columns: table => new
{
UserId = table.Column<string>(type: "TEXT", nullable: false),
RoleId = table.Column<string>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_AspNetUserRoles", x => new { x.UserId, x.RoleId });
table.ForeignKey(
name: "FK_AspNetUserRoles_AspNetRoles_RoleId",
column: x => x.RoleId,
principalTable: "AspNetRoles",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_AspNetUserRoles_AspNetUsers_UserId",
column: x => x.UserId,
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});

migrationBuilder.CreateTable(
name: "AspNetUserTokens",
columns: table => new
{
UserId = table.Column<string>(type: "TEXT", nullable: false),
LoginProvider = table.Column<string>(type: "TEXT", nullable: false),
Name = table.Column<string>(type: "TEXT", nullable: false),
Value = table.Column<string>(type: "TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_AspNetUserTokens", x => new { x.UserId, x.LoginProvider, x.Name });
table.ForeignKey(
name: "FK_AspNetUserTokens_AspNetUsers_UserId",
column: x => x.UserId,
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});

migrationBuilder.CreateIndex(
name: "IX_AspNetRoleClaims_RoleId",
table: "AspNetRoleClaims",
column: "RoleId");

migrationBuilder.CreateIndex(
name: "RoleNameIndex",
table: "AspNetRoles",
column: "NormalizedName",
unique: true);

migrationBuilder.CreateIndex(
name: "IX_AspNetUserClaims_UserId",
table: "AspNetUserClaims",
column: "UserId");

migrationBuilder.CreateIndex(
name: "IX_AspNetUserLogins_UserId",
table: "AspNetUserLogins",
column: "UserId");

migrationBuilder.CreateIndex(
name: "IX_AspNetUserRoles_RoleId",
table: "AspNetUserRoles",
column: "RoleId");

migrationBuilder.CreateIndex(
name: "EmailIndex",
table: "AspNetUsers",
column: "NormalizedEmail");

migrationBuilder.CreateIndex(
name: "UserNameIndex",
table: "AspNetUsers",
column: "NormalizedUserName",
unique: true);
}

/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "AspNetRoleClaims");

migrationBuilder.DropTable(
name: "AspNetUserClaims");

migrationBuilder.DropTable(
name: "AspNetUserLogins");

migrationBuilder.DropTable(
name: "AspNetUserRoles");

migrationBuilder.DropTable(
name: "AspNetUserTokens");

migrationBuilder.DropTable(
name: "AspNetRoles");

migrationBuilder.DropTable(
name: "AspNetUsers");
}
}
}

+ 272
- 0
src/CartWise.Infrastructure/Data/Migrations/CartWiseDbContextModelSnapshot.cs 查看文件

@@ -0,0 +1,272 @@
// <auto-generated />
using System;
using CartWise.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;

#nullable disable

namespace CartWise.Infrastructure.Data.Migrations
{
[DbContext(typeof(CartWiseDbContext))]
partial class CartWiseDbContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "10.0.10");

modelBuilder.Entity("CartWise.Infrastructure.Identity.ApplicationUser", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT");

b.Property<int>("AccessFailedCount")
.HasColumnType("INTEGER");

b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("TEXT");

b.Property<DateTime>("CreatedUtc")
.HasColumnType("TEXT");

b.Property<string>("DisplayName")
.IsRequired()
.HasColumnType("TEXT");

b.Property<string>("Email")
.HasMaxLength(256)
.HasColumnType("TEXT");

b.Property<bool>("EmailConfirmed")
.HasColumnType("INTEGER");

b.Property<bool>("LockoutEnabled")
.HasColumnType("INTEGER");

b.Property<DateTimeOffset?>("LockoutEnd")
.HasColumnType("TEXT");

b.Property<string>("NormalizedEmail")
.HasMaxLength(256)
.HasColumnType("TEXT");

b.Property<string>("NormalizedUserName")
.HasMaxLength(256)
.HasColumnType("TEXT");

b.Property<string>("PasswordHash")
.HasColumnType("TEXT");

b.Property<string>("PhoneNumber")
.HasColumnType("TEXT");

b.Property<bool>("PhoneNumberConfirmed")
.HasColumnType("INTEGER");

b.Property<string>("SecurityStamp")
.HasColumnType("TEXT");

b.Property<bool>("TwoFactorEnabled")
.HasColumnType("INTEGER");

b.Property<string>("UserName")
.HasMaxLength(256)
.HasColumnType("TEXT");

b.HasKey("Id");

b.HasIndex("NormalizedEmail")
.HasDatabaseName("EmailIndex");

b.HasIndex("NormalizedUserName")
.IsUnique()
.HasDatabaseName("UserNameIndex");

b.ToTable("AspNetUsers", (string)null);
});

modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT");

b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("TEXT");

b.Property<string>("Name")
.HasMaxLength(256)
.HasColumnType("TEXT");

b.Property<string>("NormalizedName")
.HasMaxLength(256)
.HasColumnType("TEXT");

b.HasKey("Id");

b.HasIndex("NormalizedName")
.IsUnique()
.HasDatabaseName("RoleNameIndex");

b.ToTable("AspNetRoles", (string)null);
});

modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");

b.Property<string>("ClaimType")
.HasColumnType("TEXT");

b.Property<string>("ClaimValue")
.HasColumnType("TEXT");

b.Property<string>("RoleId")
.IsRequired()
.HasColumnType("TEXT");

b.HasKey("Id");

b.HasIndex("RoleId");

b.ToTable("AspNetRoleClaims", (string)null);
});

modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");

b.Property<string>("ClaimType")
.HasColumnType("TEXT");

b.Property<string>("ClaimValue")
.HasColumnType("TEXT");

b.Property<string>("UserId")
.IsRequired()
.HasColumnType("TEXT");

b.HasKey("Id");

b.HasIndex("UserId");

b.ToTable("AspNetUserClaims", (string)null);
});

modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.Property<string>("LoginProvider")
.HasColumnType("TEXT");

b.Property<string>("ProviderKey")
.HasColumnType("TEXT");

b.Property<string>("ProviderDisplayName")
.HasColumnType("TEXT");

b.Property<string>("UserId")
.IsRequired()
.HasColumnType("TEXT");

b.HasKey("LoginProvider", "ProviderKey");

b.HasIndex("UserId");

b.ToTable("AspNetUserLogins", (string)null);
});

modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("TEXT");

b.Property<string>("RoleId")
.HasColumnType("TEXT");

b.HasKey("UserId", "RoleId");

b.HasIndex("RoleId");

b.ToTable("AspNetUserRoles", (string)null);
});

modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("TEXT");

b.Property<string>("LoginProvider")
.HasColumnType("TEXT");

b.Property<string>("Name")
.HasColumnType("TEXT");

b.Property<string>("Value")
.HasColumnType("TEXT");

b.HasKey("UserId", "LoginProvider", "Name");

b.ToTable("AspNetUserTokens", (string)null);
});

modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});

modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.HasOne("CartWise.Infrastructure.Identity.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});

modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.HasOne("CartWise.Infrastructure.Identity.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});

modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();

b.HasOne("CartWise.Infrastructure.Identity.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});

modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.HasOne("CartWise.Infrastructure.Identity.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
#pragma warning restore 612, 618
}
}
}

+ 10
- 0
src/CartWise.Infrastructure/Identity/ApplicationUser.cs 查看文件

@@ -0,0 +1,10 @@
using Microsoft.AspNetCore.Identity;

namespace CartWise.Infrastructure.Identity;

public class ApplicationUser : IdentityUser
{
public string DisplayName { get; set; } = string.Empty;

public DateTime CreatedUtc { get; set; }
}

+ 28
- 0
src/CartWise.Infrastructure/InfrastructureServiceCollectionExtensions.cs 查看文件

@@ -0,0 +1,28 @@
using CartWise.Infrastructure.Data;
using CartWise.Infrastructure.Identity;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

namespace CartWise.Infrastructure;

public static class InfrastructureServiceCollectionExtensions
{
public static IServiceCollection AddInfrastructure(this IServiceCollection services, IConfiguration configuration)
{
var connectionString = configuration.GetConnectionString("DefaultConnection")
?? throw new InvalidOperationException("Connection string 'DefaultConnection' is not configured.");

services.AddDbContext<CartWiseDbContext>(options => options.UseSqlite(connectionString));

services.AddIdentity<ApplicationUser, IdentityRole>(options =>
{
options.SignIn.RequireConfirmedAccount = false;
})
.AddEntityFrameworkStores<CartWiseDbContext>()
.AddDefaultTokenProviders();

return services;
}
}

+ 0
- 0
src/CartWise.Web/App_Data/.gitkeep 查看文件


+ 19
- 0
src/CartWise.Web/CartWise.Web.csproj 查看文件

@@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<ItemGroup>
<ProjectReference Include="..\CartWise.Application\CartWise.Application.csproj" />
<ProjectReference Include="..\CartWise.Infrastructure\CartWise.Infrastructure.csproj" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.10">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>

<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
</PropertyGroup>

</Project>

+ 99
- 0
src/CartWise.Web/Controllers/AccountController.cs 查看文件

@@ -0,0 +1,99 @@
using CartWise.Infrastructure.Identity;
using CartWise.Web.ViewModels.Account;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;

namespace CartWise.Web.Controllers;

public class AccountController : Controller
{
private readonly UserManager<ApplicationUser> _userManager;
private readonly SignInManager<ApplicationUser> _signInManager;

public AccountController(UserManager<ApplicationUser> userManager, SignInManager<ApplicationUser> signInManager)
{
_userManager = userManager;
_signInManager = signInManager;
}

[HttpGet]
public IActionResult Register(string? returnUrl = null)
{
return View(new RegisterViewModel { ReturnUrl = returnUrl });
}

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Register(RegisterViewModel model)
{
if (!ModelState.IsValid)
{
return View(model);
}

var user = new ApplicationUser
{
UserName = model.Email,
Email = model.Email,
DisplayName = model.DisplayName,
CreatedUtc = DateTime.UtcNow
};

var result = await _userManager.CreateAsync(user, model.Password);
if (!result.Succeeded)
{
foreach (var error in result.Errors)
{
ModelState.AddModelError(string.Empty, error.Description);
}

return View(model);
}

await _signInManager.SignInAsync(user, isPersistent: false);
return RedirectToLocal(model.ReturnUrl);
}

[HttpGet]
public IActionResult Login(string? returnUrl = null)
{
return View(new LoginViewModel { ReturnUrl = returnUrl });
}

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Login(LoginViewModel model)
{
if (!ModelState.IsValid)
{
return View(model);
}

var result = await _signInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, lockoutOnFailure: false);
if (!result.Succeeded)
{
ModelState.AddModelError(string.Empty, "Invalid login attempt.");
return View(model);
}

return RedirectToLocal(model.ReturnUrl);
}

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Logout()
{
await _signInManager.SignOutAsync();
return RedirectToAction("Index", "Home");
}

private IActionResult RedirectToLocal(string? returnUrl)
{
if (!string.IsNullOrEmpty(returnUrl) && Url.IsLocalUrl(returnUrl))
{
return Redirect(returnUrl);
}

return RedirectToAction("Index", "Home");
}
}

+ 26
- 0
src/CartWise.Web/Controllers/HomeController.cs 查看文件

@@ -0,0 +1,26 @@
using System.Diagnostics;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using CartWise.Web.Models;

namespace CartWise.Web.Controllers;

public class HomeController : Controller
{
public IActionResult Index()
{
return View();
}

[Authorize]
public IActionResult Privacy()
{
return View();
}

[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public IActionResult Error()
{
return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
}
}

+ 8
- 0
src/CartWise.Web/Models/ErrorViewModel.cs 查看文件

@@ -0,0 +1,8 @@
namespace CartWise.Web.Models;

public class ErrorViewModel
{
public string? RequestId { get; set; }

public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
}

+ 44
- 0
src/CartWise.Web/Program.cs 查看文件

@@ -0,0 +1,44 @@
using CartWise.Infrastructure;
using CartWise.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddControllersWithViews();
builder.Services.AddInfrastructure(builder.Configuration);

var app = builder.Build();

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Home/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
else
{
// Local-first MVP: keep the SQLite schema current without a manual migration step.
using var scope = app.Services.CreateScope();
scope.ServiceProvider.GetRequiredService<CartWiseDbContext>().Database.Migrate();
}

app.UseHttpsRedirection();
app.UseRouting();

app.UseAuthentication();
app.UseAuthorization();

app.MapStaticAssets();

app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}")
.WithStaticAssets();


app.Run();

// Exposed so Microsoft.AspNetCore.Mvc.Testing (WebApplicationFactory<Program>) can bootstrap the app in tests.
public partial class Program;

+ 23
- 0
src/CartWise.Web/Properties/launchSettings.json 查看文件

@@ -0,0 +1,23 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "http://localhost:5022",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "https://localhost:7057;http://localhost:5022",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

+ 19
- 0
src/CartWise.Web/ViewModels/Account/LoginViewModel.cs 查看文件

@@ -0,0 +1,19 @@
using System.ComponentModel.DataAnnotations;

namespace CartWise.Web.ViewModels.Account;

public class LoginViewModel
{
[Required]
[EmailAddress]
public string Email { get; set; } = string.Empty;

[Required]
[DataType(DataType.Password)]
public string Password { get; set; } = string.Empty;

[Display(Name = "Remember me")]
public bool RememberMe { get; set; }

public string? ReturnUrl { get; set; }
}

+ 28
- 0
src/CartWise.Web/ViewModels/Account/RegisterViewModel.cs 查看文件

@@ -0,0 +1,28 @@
using System.ComponentModel.DataAnnotations;

namespace CartWise.Web.ViewModels.Account;

public class RegisterViewModel
{
[Required]
[Display(Name = "Display name")]
[StringLength(100, MinimumLength = 1)]
public string DisplayName { get; set; } = string.Empty;

[Required]
[EmailAddress]
public string Email { get; set; } = string.Empty;

[Required]
[StringLength(100, MinimumLength = 8)]
[DataType(DataType.Password)]
public string Password { get; set; } = string.Empty;

[Required]
[DataType(DataType.Password)]
[Display(Name = "Confirm password")]
[Compare(nameof(Password), ErrorMessage = "The password and confirmation password do not match.")]
public string ConfirmPassword { get; set; } = string.Empty;

public string? ReturnUrl { get; set; }
}

+ 36
- 0
src/CartWise.Web/Views/Account/Login.cshtml 查看文件

@@ -0,0 +1,36 @@
@model CartWise.Web.ViewModels.Account.LoginViewModel
@{
ViewData["Title"] = "Sign in";
}

<h1>@ViewData["Title"]</h1>

<div class="row">
<div class="col-md-6">
<form asp-controller="Account" asp-action="Login" asp-route-returnUrl="@Model.ReturnUrl" method="post">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="form-group mb-3">
<label asp-for="Email" class="form-label"></label>
<input asp-for="Email" class="form-control" autocomplete="username" />
<span asp-validation-for="Email" class="text-danger"></span>
</div>
<div class="form-group mb-3">
<label asp-for="Password" class="form-label"></label>
<input asp-for="Password" class="form-control" autocomplete="current-password" />
<span asp-validation-for="Password" class="text-danger"></span>
</div>
<div class="form-check mb-3">
<input asp-for="RememberMe" class="form-check-input" />
<label asp-for="RememberMe" class="form-check-label"></label>
</div>
<button type="submit" class="btn btn-primary">Sign in</button>
</form>
<p class="mt-3">
Need an account? <a asp-controller="Account" asp-action="Register" asp-route-returnUrl="@Model.ReturnUrl">Create one</a>
</p>
</div>
</div>

@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}

+ 42
- 0
src/CartWise.Web/Views/Account/Register.cshtml 查看文件

@@ -0,0 +1,42 @@
@model CartWise.Web.ViewModels.Account.RegisterViewModel
@{
ViewData["Title"] = "Create account";
}

<h1>@ViewData["Title"]</h1>

<div class="row">
<div class="col-md-6">
<form asp-controller="Account" asp-action="Register" asp-route-returnUrl="@Model.ReturnUrl" method="post">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="form-group mb-3">
<label asp-for="DisplayName" class="form-label"></label>
<input asp-for="DisplayName" class="form-control" autocomplete="name" />
<span asp-validation-for="DisplayName" class="text-danger"></span>
</div>
<div class="form-group mb-3">
<label asp-for="Email" class="form-label"></label>
<input asp-for="Email" class="form-control" autocomplete="username" />
<span asp-validation-for="Email" class="text-danger"></span>
</div>
<div class="form-group mb-3">
<label asp-for="Password" class="form-label"></label>
<input asp-for="Password" class="form-control" autocomplete="new-password" />
<span asp-validation-for="Password" class="text-danger"></span>
</div>
<div class="form-group mb-3">
<label asp-for="ConfirmPassword" class="form-label"></label>
<input asp-for="ConfirmPassword" class="form-control" autocomplete="new-password" />
<span asp-validation-for="ConfirmPassword" class="text-danger"></span>
</div>
<button type="submit" class="btn btn-primary">Create account</button>
</form>
<p class="mt-3">
Already have an account? <a asp-controller="Account" asp-action="Login" asp-route-returnUrl="@Model.ReturnUrl">Sign in</a>
</p>
</div>
</div>

@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}

+ 8
- 0
src/CartWise.Web/Views/Home/Index.cshtml 查看文件

@@ -0,0 +1,8 @@
@{
ViewData["Title"] = "Home Page";
}

<div class="text-center">
<h1 class="display-4">Welcome</h1>
<p>Learn about <a href="https://learn.microsoft.com/aspnet/core">building Web apps with ASP.NET Core</a>.</p>
</div>

+ 6
- 0
src/CartWise.Web/Views/Home/Privacy.cshtml 查看文件

@@ -0,0 +1,6 @@
@{
ViewData["Title"] = "Privacy Policy";
}
<h1>@ViewData["Title"]</h1>

<p>Use this page to detail your site's privacy policy.</p>

+ 25
- 0
src/CartWise.Web/Views/Shared/Error.cshtml 查看文件

@@ -0,0 +1,25 @@
@model ErrorViewModel
@{
ViewData["Title"] = "Error";
}

<h1 class="text-danger">Error.</h1>
<h2 class="text-danger">An error occurred while processing your request.</h2>

@if (Model.ShowRequestId)
{
<p>
<strong>Request ID:</strong> <code>@Model.RequestId</code>
</p>
}

<h3>Development Mode</h3>
<p>
Swapping to <strong>Development</strong> environment will display more detailed information about the error that occurred.
</p>
<p>
<strong>The Development environment shouldn't be enabled for deployed applications.</strong>
It can result in displaying sensitive information from exceptions to end users.
For local debugging, enable the <strong>Development</strong> environment by setting the <strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong>
and restarting the app.
</p>

+ 72
- 0
src/CartWise.Web/Views/Shared/_Layout.cshtml 查看文件

@@ -0,0 +1,72 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@ViewData["Title"] - CartWise</title>
<script type="importmap"></script>
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" />
<link rel="stylesheet" href="~/css/site.css" asp-append-version="true" />
<link rel="stylesheet" href="~/CartWise.Web.styles.css" asp-append-version="true" />
</head>
<body>
<header>
<nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow mb-3">
<div class="container-fluid">
<a class="navbar-brand" asp-area="" asp-controller="Home" asp-action="Index">CartWise</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target=".navbar-collapse" aria-controls="navbarSupportedContent"
aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="navbar-collapse collapse d-sm-inline-flex justify-content-between">
<ul class="navbar-nav flex-grow-1">
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Index">Home</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
</li>
</ul>
<ul class="navbar-nav">
@if (User.Identity?.IsAuthenticated == true)
{
<li class="nav-item">
<span class="nav-link text-dark">Hi, @User.Identity.Name</span>
</li>
<li class="nav-item">
<form asp-controller="Account" asp-action="Logout" method="post" class="form-inline">
<button type="submit" class="nav-link btn btn-link text-dark">Sign out</button>
</form>
</li>
}
else
{
<li class="nav-item">
<a class="nav-link text-dark" asp-controller="Account" asp-action="Login">Sign in</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark" asp-controller="Account" asp-action="Register">Create account</a>
</li>
}
</ul>
</div>
</div>
</nav>
</header>
<div class="container">
<main role="main" class="pb-3">
@RenderBody()
</main>
</div>

<footer class="border-top footer text-muted">
<div class="container">
&copy; 2026 - CartWise - <a asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
</div>
</footer>
<script src="~/lib/jquery/dist/jquery.min.js"></script>
<script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
<script src="~/js/site.js" asp-append-version="true"></script>
@await RenderSectionAsync("Scripts", required: false)
</body>
</html>

+ 48
- 0
src/CartWise.Web/Views/Shared/_Layout.cshtml.css 查看文件

@@ -0,0 +1,48 @@
/* Please see documentation at https://learn.microsoft.com/aspnet/core/client-side/bundling-and-minification
for details on configuring this project to bundle and minify static web assets. */

a.navbar-brand {
white-space: normal;
text-align: center;
word-break: break-all;
}

a {
color: #0077cc;
}

.btn-primary {
color: #fff;
background-color: #1b6ec2;
border-color: #1861ac;
}

.nav-pills .nav-link.active, .nav-pills .show > .nav-link {
color: #fff;
background-color: #1b6ec2;
border-color: #1861ac;
}

.border-top {
border-top: 1px solid #e5e5e5;
}
.border-bottom {
border-bottom: 1px solid #e5e5e5;
}

.box-shadow {
box-shadow: 0 .25rem .75rem rgba(0, 0, 0, .05);
}

button.accept-policy {
font-size: 1rem;
line-height: inherit;
}

.footer {
position: absolute;
bottom: 0;
width: 100%;
white-space: nowrap;
line-height: 60px;
}

+ 2
- 0
src/CartWise.Web/Views/Shared/_ValidationScriptsPartial.cshtml 查看文件

@@ -0,0 +1,2 @@
<script src="~/lib/jquery-validation/dist/jquery.validate.min.js"></script>
<script src="~/lib/jquery-validation-unobtrusive/dist/jquery.validate.unobtrusive.min.js"></script>

+ 3
- 0
src/CartWise.Web/Views/_ViewImports.cshtml 查看文件

@@ -0,0 +1,3 @@
@using CartWise.Web
@using CartWise.Web.Models
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

+ 3
- 0
src/CartWise.Web/Views/_ViewStart.cshtml 查看文件

@@ -0,0 +1,3 @@
@{
Layout = "_Layout";
}

+ 9
- 0
src/CartWise.Web/appsettings.Development.json 查看文件

@@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning",
"Microsoft.EntityFrameworkCore.Database.Command": "Information"
}
}
}

+ 14
- 0
src/CartWise.Web/appsettings.json 查看文件

@@ -0,0 +1,14 @@
{
"ConnectionStrings": {
"DefaultConnection": "Data Source=App_Data/cartwise.db"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning",
"Microsoft.AspNetCore.Identity": "Warning",
"Microsoft.EntityFrameworkCore": "Warning"
}
},
"AllowedHosts": "*"
}

+ 31
- 0
src/CartWise.Web/wwwroot/css/site.css 查看文件

@@ -0,0 +1,31 @@
html {
font-size: 14px;
}

@media (min-width: 768px) {
html {
font-size: 16px;
}
}

.btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus {
box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb;
}

html {
position: relative;
min-height: 100%;
}

body {
margin-bottom: 60px;
}

.form-floating > .form-control-plaintext::placeholder, .form-floating > .form-control::placeholder {
color: var(--bs-secondary-color);
text-align: end;
}

.form-floating > .form-control-plaintext:focus::placeholder, .form-floating > .form-control:focus::placeholder {
text-align: start;
}

二進制
src/CartWise.Web/wwwroot/favicon.ico 查看文件

Before After

+ 0
- 0
src/CartWise.Web/wwwroot/images/.gitkeep 查看文件


+ 0
- 0
src/CartWise.Web/wwwroot/js/components/.gitkeep 查看文件


+ 0
- 0
src/CartWise.Web/wwwroot/js/pages/.gitkeep 查看文件


+ 0
- 0
src/CartWise.Web/wwwroot/js/services/.gitkeep 查看文件


+ 4
- 0
src/CartWise.Web/wwwroot/js/site.js 查看文件

@@ -0,0 +1,4 @@
// Please see documentation at https://learn.microsoft.com/aspnet/core/client-side/bundling-and-minification
// for details on configuring this project to bundle and minify static web assets.

// Write your JavaScript code.

+ 0
- 0
src/CartWise.Web/wwwroot/js/utils/.gitkeep 查看文件


+ 22
- 0
src/CartWise.Web/wwwroot/lib/bootstrap/LICENSE 查看文件

@@ -0,0 +1,22 @@
The MIT License (MIT)

Copyright (c) 2011-2021 Twitter, Inc.
Copyright (c) 2011-2021 The Bootstrap Authors

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

+ 4085
- 0
src/CartWise.Web/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.css
文件差異過大導致無法顯示
查看文件


+ 1
- 0
src/CartWise.Web/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.css.map
文件差異過大導致無法顯示
查看文件


+ 6
- 0
src/CartWise.Web/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.min.css
文件差異過大導致無法顯示
查看文件


+ 1
- 0
src/CartWise.Web/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.min.css.map
文件差異過大導致無法顯示
查看文件


+ 4084
- 0
src/CartWise.Web/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.css
文件差異過大導致無法顯示
查看文件


+ 1
- 0
src/CartWise.Web/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.css.map
文件差異過大導致無法顯示
查看文件


+ 6
- 0
src/CartWise.Web/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.min.css
文件差異過大導致無法顯示
查看文件


+ 1
- 0
src/CartWise.Web/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.min.css.map
文件差異過大導致無法顯示
查看文件


+ 597
- 0
src/CartWise.Web/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.css 查看文件

@@ -0,0 +1,597 @@
/*!
* Bootstrap Reboot v5.3.3 (https://getbootstrap.com/)
* Copyright 2011-2024 The Bootstrap Authors
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
*/
:root,
[data-bs-theme=light] {
--bs-blue: #0d6efd;
--bs-indigo: #6610f2;
--bs-purple: #6f42c1;
--bs-pink: #d63384;
--bs-red: #dc3545;
--bs-orange: #fd7e14;
--bs-yellow: #ffc107;
--bs-green: #198754;
--bs-teal: #20c997;
--bs-cyan: #0dcaf0;
--bs-black: #000;
--bs-white: #fff;
--bs-gray: #6c757d;
--bs-gray-dark: #343a40;
--bs-gray-100: #f8f9fa;
--bs-gray-200: #e9ecef;
--bs-gray-300: #dee2e6;
--bs-gray-400: #ced4da;
--bs-gray-500: #adb5bd;
--bs-gray-600: #6c757d;
--bs-gray-700: #495057;
--bs-gray-800: #343a40;
--bs-gray-900: #212529;
--bs-primary: #0d6efd;
--bs-secondary: #6c757d;
--bs-success: #198754;
--bs-info: #0dcaf0;
--bs-warning: #ffc107;
--bs-danger: #dc3545;
--bs-light: #f8f9fa;
--bs-dark: #212529;
--bs-primary-rgb: 13, 110, 253;
--bs-secondary-rgb: 108, 117, 125;
--bs-success-rgb: 25, 135, 84;
--bs-info-rgb: 13, 202, 240;
--bs-warning-rgb: 255, 193, 7;
--bs-danger-rgb: 220, 53, 69;
--bs-light-rgb: 248, 249, 250;
--bs-dark-rgb: 33, 37, 41;
--bs-primary-text-emphasis: #052c65;
--bs-secondary-text-emphasis: #2b2f32;
--bs-success-text-emphasis: #0a3622;
--bs-info-text-emphasis: #055160;
--bs-warning-text-emphasis: #664d03;
--bs-danger-text-emphasis: #58151c;
--bs-light-text-emphasis: #495057;
--bs-dark-text-emphasis: #495057;
--bs-primary-bg-subtle: #cfe2ff;
--bs-secondary-bg-subtle: #e2e3e5;
--bs-success-bg-subtle: #d1e7dd;
--bs-info-bg-subtle: #cff4fc;
--bs-warning-bg-subtle: #fff3cd;
--bs-danger-bg-subtle: #f8d7da;
--bs-light-bg-subtle: #fcfcfd;
--bs-dark-bg-subtle: #ced4da;
--bs-primary-border-subtle: #9ec5fe;
--bs-secondary-border-subtle: #c4c8cb;
--bs-success-border-subtle: #a3cfbb;
--bs-info-border-subtle: #9eeaf9;
--bs-warning-border-subtle: #ffe69c;
--bs-danger-border-subtle: #f1aeb5;
--bs-light-border-subtle: #e9ecef;
--bs-dark-border-subtle: #adb5bd;
--bs-white-rgb: 255, 255, 255;
--bs-black-rgb: 0, 0, 0;
--bs-font-sans-serif: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", "Liberation Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
--bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
--bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));
--bs-body-font-family: var(--bs-font-sans-serif);
--bs-body-font-size: 1rem;
--bs-body-font-weight: 400;
--bs-body-line-height: 1.5;
--bs-body-color: #212529;
--bs-body-color-rgb: 33, 37, 41;
--bs-body-bg: #fff;
--bs-body-bg-rgb: 255, 255, 255;
--bs-emphasis-color: #000;
--bs-emphasis-color-rgb: 0, 0, 0;
--bs-secondary-color: rgba(33, 37, 41, 0.75);
--bs-secondary-color-rgb: 33, 37, 41;
--bs-secondary-bg: #e9ecef;
--bs-secondary-bg-rgb: 233, 236, 239;
--bs-tertiary-color: rgba(33, 37, 41, 0.5);
--bs-tertiary-color-rgb: 33, 37, 41;
--bs-tertiary-bg: #f8f9fa;
--bs-tertiary-bg-rgb: 248, 249, 250;
--bs-heading-color: inherit;
--bs-link-color: #0d6efd;
--bs-link-color-rgb: 13, 110, 253;
--bs-link-decoration: underline;
--bs-link-hover-color: #0a58ca;
--bs-link-hover-color-rgb: 10, 88, 202;
--bs-code-color: #d63384;
--bs-highlight-color: #212529;
--bs-highlight-bg: #fff3cd;
--bs-border-width: 1px;
--bs-border-style: solid;
--bs-border-color: #dee2e6;
--bs-border-color-translucent: rgba(0, 0, 0, 0.175);
--bs-border-radius: 0.375rem;
--bs-border-radius-sm: 0.25rem;
--bs-border-radius-lg: 0.5rem;
--bs-border-radius-xl: 1rem;
--bs-border-radius-xxl: 2rem;
--bs-border-radius-2xl: var(--bs-border-radius-xxl);
--bs-border-radius-pill: 50rem;
--bs-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
--bs-box-shadow-sm: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
--bs-box-shadow-lg: 0 1rem 3rem rgba(0, 0, 0, 0.175);
--bs-box-shadow-inset: inset 0 1px 2px rgba(0, 0, 0, 0.075);
--bs-focus-ring-width: 0.25rem;
--bs-focus-ring-opacity: 0.25;
--bs-focus-ring-color: rgba(13, 110, 253, 0.25);
--bs-form-valid-color: #198754;
--bs-form-valid-border-color: #198754;
--bs-form-invalid-color: #dc3545;
--bs-form-invalid-border-color: #dc3545;
}

[data-bs-theme=dark] {
color-scheme: dark;
--bs-body-color: #dee2e6;
--bs-body-color-rgb: 222, 226, 230;
--bs-body-bg: #212529;
--bs-body-bg-rgb: 33, 37, 41;
--bs-emphasis-color: #fff;
--bs-emphasis-color-rgb: 255, 255, 255;
--bs-secondary-color: rgba(222, 226, 230, 0.75);
--bs-secondary-color-rgb: 222, 226, 230;
--bs-secondary-bg: #343a40;
--bs-secondary-bg-rgb: 52, 58, 64;
--bs-tertiary-color: rgba(222, 226, 230, 0.5);
--bs-tertiary-color-rgb: 222, 226, 230;
--bs-tertiary-bg: #2b3035;
--bs-tertiary-bg-rgb: 43, 48, 53;
--bs-primary-text-emphasis: #6ea8fe;
--bs-secondary-text-emphasis: #a7acb1;
--bs-success-text-emphasis: #75b798;
--bs-info-text-emphasis: #6edff6;
--bs-warning-text-emphasis: #ffda6a;
--bs-danger-text-emphasis: #ea868f;
--bs-light-text-emphasis: #f8f9fa;
--bs-dark-text-emphasis: #dee2e6;
--bs-primary-bg-subtle: #031633;
--bs-secondary-bg-subtle: #161719;
--bs-success-bg-subtle: #051b11;
--bs-info-bg-subtle: #032830;
--bs-warning-bg-subtle: #332701;
--bs-danger-bg-subtle: #2c0b0e;
--bs-light-bg-subtle: #343a40;
--bs-dark-bg-subtle: #1a1d20;
--bs-primary-border-subtle: #084298;
--bs-secondary-border-subtle: #41464b;
--bs-success-border-subtle: #0f5132;
--bs-info-border-subtle: #087990;
--bs-warning-border-subtle: #997404;
--bs-danger-border-subtle: #842029;
--bs-light-border-subtle: #495057;
--bs-dark-border-subtle: #343a40;
--bs-heading-color: inherit;
--bs-link-color: #6ea8fe;
--bs-link-hover-color: #8bb9fe;
--bs-link-color-rgb: 110, 168, 254;
--bs-link-hover-color-rgb: 139, 185, 254;
--bs-code-color: #e685b5;
--bs-highlight-color: #dee2e6;
--bs-highlight-bg: #664d03;
--bs-border-color: #495057;
--bs-border-color-translucent: rgba(255, 255, 255, 0.15);
--bs-form-valid-color: #75b798;
--bs-form-valid-border-color: #75b798;
--bs-form-invalid-color: #ea868f;
--bs-form-invalid-border-color: #ea868f;
}

*,
*::before,
*::after {
box-sizing: border-box;
}

@media (prefers-reduced-motion: no-preference) {
:root {
scroll-behavior: smooth;
}
}

body {
margin: 0;
font-family: var(--bs-body-font-family);
font-size: var(--bs-body-font-size);
font-weight: var(--bs-body-font-weight);
line-height: var(--bs-body-line-height);
color: var(--bs-body-color);
text-align: var(--bs-body-text-align);
background-color: var(--bs-body-bg);
-webkit-text-size-adjust: 100%;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}

hr {
margin: 1rem 0;
color: inherit;
border: 0;
border-top: var(--bs-border-width) solid;
opacity: 0.25;
}

h6, h5, h4, h3, h2, h1 {
margin-top: 0;
margin-bottom: 0.5rem;
font-weight: 500;
line-height: 1.2;
color: var(--bs-heading-color);
}

h1 {
font-size: calc(1.375rem + 1.5vw);
}
@media (min-width: 1200px) {
h1 {
font-size: 2.5rem;
}
}

h2 {
font-size: calc(1.325rem + 0.9vw);
}
@media (min-width: 1200px) {
h2 {
font-size: 2rem;
}
}

h3 {
font-size: calc(1.3rem + 0.6vw);
}
@media (min-width: 1200px) {
h3 {
font-size: 1.75rem;
}
}

h4 {
font-size: calc(1.275rem + 0.3vw);
}
@media (min-width: 1200px) {
h4 {
font-size: 1.5rem;
}
}

h5 {
font-size: 1.25rem;
}

h6 {
font-size: 1rem;
}

p {
margin-top: 0;
margin-bottom: 1rem;
}

abbr[title] {
-webkit-text-decoration: underline dotted;
text-decoration: underline dotted;
cursor: help;
-webkit-text-decoration-skip-ink: none;
text-decoration-skip-ink: none;
}

address {
margin-bottom: 1rem;
font-style: normal;
line-height: inherit;
}

ol,
ul {
padding-left: 2rem;
}

ol,
ul,
dl {
margin-top: 0;
margin-bottom: 1rem;
}

ol ol,
ul ul,
ol ul,
ul ol {
margin-bottom: 0;
}

dt {
font-weight: 700;
}

dd {
margin-bottom: 0.5rem;
margin-left: 0;
}

blockquote {
margin: 0 0 1rem;
}

b,
strong {
font-weight: bolder;
}

small {
font-size: 0.875em;
}

mark {
padding: 0.1875em;
color: var(--bs-highlight-color);
background-color: var(--bs-highlight-bg);
}

sub,
sup {
position: relative;
font-size: 0.75em;
line-height: 0;
vertical-align: baseline;
}

sub {
bottom: -0.25em;
}

sup {
top: -0.5em;
}

a {
color: rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 1));
text-decoration: underline;
}
a:hover {
--bs-link-color-rgb: var(--bs-link-hover-color-rgb);
}

a:not([href]):not([class]), a:not([href]):not([class]):hover {
color: inherit;
text-decoration: none;
}

pre,
code,
kbd,
samp {
font-family: var(--bs-font-monospace);
font-size: 1em;
}

pre {
display: block;
margin-top: 0;
margin-bottom: 1rem;
overflow: auto;
font-size: 0.875em;
}
pre code {
font-size: inherit;
color: inherit;
word-break: normal;
}

code {
font-size: 0.875em;
color: var(--bs-code-color);
word-wrap: break-word;
}
a > code {
color: inherit;
}

kbd {
padding: 0.1875rem 0.375rem;
font-size: 0.875em;
color: var(--bs-body-bg);
background-color: var(--bs-body-color);
border-radius: 0.25rem;
}
kbd kbd {
padding: 0;
font-size: 1em;
}

figure {
margin: 0 0 1rem;
}

img,
svg {
vertical-align: middle;
}

table {
caption-side: bottom;
border-collapse: collapse;
}

caption {
padding-top: 0.5rem;
padding-bottom: 0.5rem;
color: var(--bs-secondary-color);
text-align: left;
}

th {
text-align: inherit;
text-align: -webkit-match-parent;
}

thead,
tbody,
tfoot,
tr,
td,
th {
border-color: inherit;
border-style: solid;
border-width: 0;
}

label {
display: inline-block;
}

button {
border-radius: 0;
}

button:focus:not(:focus-visible) {
outline: 0;
}

input,
button,
select,
optgroup,
textarea {
margin: 0;
font-family: inherit;
font-size: inherit;
line-height: inherit;
}

button,
select {
text-transform: none;
}

[role=button] {
cursor: pointer;
}

select {
word-wrap: normal;
}
select:disabled {
opacity: 1;
}

[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator {
display: none !important;
}

button,
[type=button],
[type=reset],
[type=submit] {
-webkit-appearance: button;
}
button:not(:disabled),
[type=button]:not(:disabled),
[type=reset]:not(:disabled),
[type=submit]:not(:disabled) {
cursor: pointer;
}

::-moz-focus-inner {
padding: 0;
border-style: none;
}

textarea {
resize: vertical;
}

fieldset {
min-width: 0;
padding: 0;
margin: 0;
border: 0;
}

legend {
float: left;
width: 100%;
padding: 0;
margin-bottom: 0.5rem;
font-size: calc(1.275rem + 0.3vw);
line-height: inherit;
}
@media (min-width: 1200px) {
legend {
font-size: 1.5rem;
}
}
legend + * {
clear: left;
}

::-webkit-datetime-edit-fields-wrapper,
::-webkit-datetime-edit-text,
::-webkit-datetime-edit-minute,
::-webkit-datetime-edit-hour-field,
::-webkit-datetime-edit-day-field,
::-webkit-datetime-edit-month-field,
::-webkit-datetime-edit-year-field {
padding: 0;
}

::-webkit-inner-spin-button {
height: auto;
}

[type=search] {
-webkit-appearance: textfield;
outline-offset: -2px;
}

/* rtl:raw:
[type="tel"],
[type="url"],
[type="email"],
[type="number"] {
direction: ltr;
}
*/
::-webkit-search-decoration {
-webkit-appearance: none;
}

::-webkit-color-swatch-wrapper {
padding: 0;
}

::-webkit-file-upload-button {
font: inherit;
-webkit-appearance: button;
}

::file-selector-button {
font: inherit;
-webkit-appearance: button;
}

output {
display: inline-block;
}

iframe {
border: 0;
}

summary {
display: list-item;
cursor: pointer;
}

progress {
vertical-align: baseline;
}

[hidden] {
display: none !important;
}

/*# sourceMappingURL=bootstrap-reboot.css.map */

+ 1
- 0
src/CartWise.Web/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.css.map
文件差異過大導致無法顯示
查看文件


+ 6
- 0
src/CartWise.Web/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.min.css
文件差異過大導致無法顯示
查看文件


+ 1
- 0
src/CartWise.Web/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.min.css.map
文件差異過大導致無法顯示
查看文件


+ 594
- 0
src/CartWise.Web/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.css 查看文件

@@ -0,0 +1,594 @@
/*!
* Bootstrap Reboot v5.3.3 (https://getbootstrap.com/)
* Copyright 2011-2024 The Bootstrap Authors
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
*/
:root,
[data-bs-theme=light] {
--bs-blue: #0d6efd;
--bs-indigo: #6610f2;
--bs-purple: #6f42c1;
--bs-pink: #d63384;
--bs-red: #dc3545;
--bs-orange: #fd7e14;
--bs-yellow: #ffc107;
--bs-green: #198754;
--bs-teal: #20c997;
--bs-cyan: #0dcaf0;
--bs-black: #000;
--bs-white: #fff;
--bs-gray: #6c757d;
--bs-gray-dark: #343a40;
--bs-gray-100: #f8f9fa;
--bs-gray-200: #e9ecef;
--bs-gray-300: #dee2e6;
--bs-gray-400: #ced4da;
--bs-gray-500: #adb5bd;
--bs-gray-600: #6c757d;
--bs-gray-700: #495057;
--bs-gray-800: #343a40;
--bs-gray-900: #212529;
--bs-primary: #0d6efd;
--bs-secondary: #6c757d;
--bs-success: #198754;
--bs-info: #0dcaf0;
--bs-warning: #ffc107;
--bs-danger: #dc3545;
--bs-light: #f8f9fa;
--bs-dark: #212529;
--bs-primary-rgb: 13, 110, 253;
--bs-secondary-rgb: 108, 117, 125;
--bs-success-rgb: 25, 135, 84;
--bs-info-rgb: 13, 202, 240;
--bs-warning-rgb: 255, 193, 7;
--bs-danger-rgb: 220, 53, 69;
--bs-light-rgb: 248, 249, 250;
--bs-dark-rgb: 33, 37, 41;
--bs-primary-text-emphasis: #052c65;
--bs-secondary-text-emphasis: #2b2f32;
--bs-success-text-emphasis: #0a3622;
--bs-info-text-emphasis: #055160;
--bs-warning-text-emphasis: #664d03;
--bs-danger-text-emphasis: #58151c;
--bs-light-text-emphasis: #495057;
--bs-dark-text-emphasis: #495057;
--bs-primary-bg-subtle: #cfe2ff;
--bs-secondary-bg-subtle: #e2e3e5;
--bs-success-bg-subtle: #d1e7dd;
--bs-info-bg-subtle: #cff4fc;
--bs-warning-bg-subtle: #fff3cd;
--bs-danger-bg-subtle: #f8d7da;
--bs-light-bg-subtle: #fcfcfd;
--bs-dark-bg-subtle: #ced4da;
--bs-primary-border-subtle: #9ec5fe;
--bs-secondary-border-subtle: #c4c8cb;
--bs-success-border-subtle: #a3cfbb;
--bs-info-border-subtle: #9eeaf9;
--bs-warning-border-subtle: #ffe69c;
--bs-danger-border-subtle: #f1aeb5;
--bs-light-border-subtle: #e9ecef;
--bs-dark-border-subtle: #adb5bd;
--bs-white-rgb: 255, 255, 255;
--bs-black-rgb: 0, 0, 0;
--bs-font-sans-serif: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", "Liberation Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
--bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
--bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));
--bs-body-font-family: var(--bs-font-sans-serif);
--bs-body-font-size: 1rem;
--bs-body-font-weight: 400;
--bs-body-line-height: 1.5;
--bs-body-color: #212529;
--bs-body-color-rgb: 33, 37, 41;
--bs-body-bg: #fff;
--bs-body-bg-rgb: 255, 255, 255;
--bs-emphasis-color: #000;
--bs-emphasis-color-rgb: 0, 0, 0;
--bs-secondary-color: rgba(33, 37, 41, 0.75);
--bs-secondary-color-rgb: 33, 37, 41;
--bs-secondary-bg: #e9ecef;
--bs-secondary-bg-rgb: 233, 236, 239;
--bs-tertiary-color: rgba(33, 37, 41, 0.5);
--bs-tertiary-color-rgb: 33, 37, 41;
--bs-tertiary-bg: #f8f9fa;
--bs-tertiary-bg-rgb: 248, 249, 250;
--bs-heading-color: inherit;
--bs-link-color: #0d6efd;
--bs-link-color-rgb: 13, 110, 253;
--bs-link-decoration: underline;
--bs-link-hover-color: #0a58ca;
--bs-link-hover-color-rgb: 10, 88, 202;
--bs-code-color: #d63384;
--bs-highlight-color: #212529;
--bs-highlight-bg: #fff3cd;
--bs-border-width: 1px;
--bs-border-style: solid;
--bs-border-color: #dee2e6;
--bs-border-color-translucent: rgba(0, 0, 0, 0.175);
--bs-border-radius: 0.375rem;
--bs-border-radius-sm: 0.25rem;
--bs-border-radius-lg: 0.5rem;
--bs-border-radius-xl: 1rem;
--bs-border-radius-xxl: 2rem;
--bs-border-radius-2xl: var(--bs-border-radius-xxl);
--bs-border-radius-pill: 50rem;
--bs-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
--bs-box-shadow-sm: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
--bs-box-shadow-lg: 0 1rem 3rem rgba(0, 0, 0, 0.175);
--bs-box-shadow-inset: inset 0 1px 2px rgba(0, 0, 0, 0.075);
--bs-focus-ring-width: 0.25rem;
--bs-focus-ring-opacity: 0.25;
--bs-focus-ring-color: rgba(13, 110, 253, 0.25);
--bs-form-valid-color: #198754;
--bs-form-valid-border-color: #198754;
--bs-form-invalid-color: #dc3545;
--bs-form-invalid-border-color: #dc3545;
}

[data-bs-theme=dark] {
color-scheme: dark;
--bs-body-color: #dee2e6;
--bs-body-color-rgb: 222, 226, 230;
--bs-body-bg: #212529;
--bs-body-bg-rgb: 33, 37, 41;
--bs-emphasis-color: #fff;
--bs-emphasis-color-rgb: 255, 255, 255;
--bs-secondary-color: rgba(222, 226, 230, 0.75);
--bs-secondary-color-rgb: 222, 226, 230;
--bs-secondary-bg: #343a40;
--bs-secondary-bg-rgb: 52, 58, 64;
--bs-tertiary-color: rgba(222, 226, 230, 0.5);
--bs-tertiary-color-rgb: 222, 226, 230;
--bs-tertiary-bg: #2b3035;
--bs-tertiary-bg-rgb: 43, 48, 53;
--bs-primary-text-emphasis: #6ea8fe;
--bs-secondary-text-emphasis: #a7acb1;
--bs-success-text-emphasis: #75b798;
--bs-info-text-emphasis: #6edff6;
--bs-warning-text-emphasis: #ffda6a;
--bs-danger-text-emphasis: #ea868f;
--bs-light-text-emphasis: #f8f9fa;
--bs-dark-text-emphasis: #dee2e6;
--bs-primary-bg-subtle: #031633;
--bs-secondary-bg-subtle: #161719;
--bs-success-bg-subtle: #051b11;
--bs-info-bg-subtle: #032830;
--bs-warning-bg-subtle: #332701;
--bs-danger-bg-subtle: #2c0b0e;
--bs-light-bg-subtle: #343a40;
--bs-dark-bg-subtle: #1a1d20;
--bs-primary-border-subtle: #084298;
--bs-secondary-border-subtle: #41464b;
--bs-success-border-subtle: #0f5132;
--bs-info-border-subtle: #087990;
--bs-warning-border-subtle: #997404;
--bs-danger-border-subtle: #842029;
--bs-light-border-subtle: #495057;
--bs-dark-border-subtle: #343a40;
--bs-heading-color: inherit;
--bs-link-color: #6ea8fe;
--bs-link-hover-color: #8bb9fe;
--bs-link-color-rgb: 110, 168, 254;
--bs-link-hover-color-rgb: 139, 185, 254;
--bs-code-color: #e685b5;
--bs-highlight-color: #dee2e6;
--bs-highlight-bg: #664d03;
--bs-border-color: #495057;
--bs-border-color-translucent: rgba(255, 255, 255, 0.15);
--bs-form-valid-color: #75b798;
--bs-form-valid-border-color: #75b798;
--bs-form-invalid-color: #ea868f;
--bs-form-invalid-border-color: #ea868f;
}

*,
*::before,
*::after {
box-sizing: border-box;
}

@media (prefers-reduced-motion: no-preference) {
:root {
scroll-behavior: smooth;
}
}

body {
margin: 0;
font-family: var(--bs-body-font-family);
font-size: var(--bs-body-font-size);
font-weight: var(--bs-body-font-weight);
line-height: var(--bs-body-line-height);
color: var(--bs-body-color);
text-align: var(--bs-body-text-align);
background-color: var(--bs-body-bg);
-webkit-text-size-adjust: 100%;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}

hr {
margin: 1rem 0;
color: inherit;
border: 0;
border-top: var(--bs-border-width) solid;
opacity: 0.25;
}

h6, h5, h4, h3, h2, h1 {
margin-top: 0;
margin-bottom: 0.5rem;
font-weight: 500;
line-height: 1.2;
color: var(--bs-heading-color);
}

h1 {
font-size: calc(1.375rem + 1.5vw);
}
@media (min-width: 1200px) {
h1 {
font-size: 2.5rem;
}
}

h2 {
font-size: calc(1.325rem + 0.9vw);
}
@media (min-width: 1200px) {
h2 {
font-size: 2rem;
}
}

h3 {
font-size: calc(1.3rem + 0.6vw);
}
@media (min-width: 1200px) {
h3 {
font-size: 1.75rem;
}
}

h4 {
font-size: calc(1.275rem + 0.3vw);
}
@media (min-width: 1200px) {
h4 {
font-size: 1.5rem;
}
}

h5 {
font-size: 1.25rem;
}

h6 {
font-size: 1rem;
}

p {
margin-top: 0;
margin-bottom: 1rem;
}

abbr[title] {
-webkit-text-decoration: underline dotted;
text-decoration: underline dotted;
cursor: help;
-webkit-text-decoration-skip-ink: none;
text-decoration-skip-ink: none;
}

address {
margin-bottom: 1rem;
font-style: normal;
line-height: inherit;
}

ol,
ul {
padding-right: 2rem;
}

ol,
ul,
dl {
margin-top: 0;
margin-bottom: 1rem;
}

ol ol,
ul ul,
ol ul,
ul ol {
margin-bottom: 0;
}

dt {
font-weight: 700;
}

dd {
margin-bottom: 0.5rem;
margin-right: 0;
}

blockquote {
margin: 0 0 1rem;
}

b,
strong {
font-weight: bolder;
}

small {
font-size: 0.875em;
}

mark {
padding: 0.1875em;
color: var(--bs-highlight-color);
background-color: var(--bs-highlight-bg);
}

sub,
sup {
position: relative;
font-size: 0.75em;
line-height: 0;
vertical-align: baseline;
}

sub {
bottom: -0.25em;
}

sup {
top: -0.5em;
}

a {
color: rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 1));
text-decoration: underline;
}
a:hover {
--bs-link-color-rgb: var(--bs-link-hover-color-rgb);
}

a:not([href]):not([class]), a:not([href]):not([class]):hover {
color: inherit;
text-decoration: none;
}

pre,
code,
kbd,
samp {
font-family: var(--bs-font-monospace);
font-size: 1em;
}

pre {
display: block;
margin-top: 0;
margin-bottom: 1rem;
overflow: auto;
font-size: 0.875em;
}
pre code {
font-size: inherit;
color: inherit;
word-break: normal;
}

code {
font-size: 0.875em;
color: var(--bs-code-color);
word-wrap: break-word;
}
a > code {
color: inherit;
}

kbd {
padding: 0.1875rem 0.375rem;
font-size: 0.875em;
color: var(--bs-body-bg);
background-color: var(--bs-body-color);
border-radius: 0.25rem;
}
kbd kbd {
padding: 0;
font-size: 1em;
}

figure {
margin: 0 0 1rem;
}

img,
svg {
vertical-align: middle;
}

table {
caption-side: bottom;
border-collapse: collapse;
}

caption {
padding-top: 0.5rem;
padding-bottom: 0.5rem;
color: var(--bs-secondary-color);
text-align: right;
}

th {
text-align: inherit;
text-align: -webkit-match-parent;
}

thead,
tbody,
tfoot,
tr,
td,
th {
border-color: inherit;
border-style: solid;
border-width: 0;
}

label {
display: inline-block;
}

button {
border-radius: 0;
}

button:focus:not(:focus-visible) {
outline: 0;
}

input,
button,
select,
optgroup,
textarea {
margin: 0;
font-family: inherit;
font-size: inherit;
line-height: inherit;
}

button,
select {
text-transform: none;
}

[role=button] {
cursor: pointer;
}

select {
word-wrap: normal;
}
select:disabled {
opacity: 1;
}

[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator {
display: none !important;
}

button,
[type=button],
[type=reset],
[type=submit] {
-webkit-appearance: button;
}
button:not(:disabled),
[type=button]:not(:disabled),
[type=reset]:not(:disabled),
[type=submit]:not(:disabled) {
cursor: pointer;
}

::-moz-focus-inner {
padding: 0;
border-style: none;
}

textarea {
resize: vertical;
}

fieldset {
min-width: 0;
padding: 0;
margin: 0;
border: 0;
}

legend {
float: right;
width: 100%;
padding: 0;
margin-bottom: 0.5rem;
font-size: calc(1.275rem + 0.3vw);
line-height: inherit;
}
@media (min-width: 1200px) {
legend {
font-size: 1.5rem;
}
}
legend + * {
clear: right;
}

::-webkit-datetime-edit-fields-wrapper,
::-webkit-datetime-edit-text,
::-webkit-datetime-edit-minute,
::-webkit-datetime-edit-hour-field,
::-webkit-datetime-edit-day-field,
::-webkit-datetime-edit-month-field,
::-webkit-datetime-edit-year-field {
padding: 0;
}

::-webkit-inner-spin-button {
height: auto;
}

[type=search] {
-webkit-appearance: textfield;
outline-offset: -2px;
}

[type="tel"],
[type="url"],
[type="email"],
[type="number"] {
direction: ltr;
}
::-webkit-search-decoration {
-webkit-appearance: none;
}

::-webkit-color-swatch-wrapper {
padding: 0;
}

::-webkit-file-upload-button {
font: inherit;
-webkit-appearance: button;
}

::file-selector-button {
font: inherit;
-webkit-appearance: button;
}

output {
display: inline-block;
}

iframe {
border: 0;
}

summary {
display: list-item;
cursor: pointer;
}

progress {
vertical-align: baseline;
}

[hidden] {
display: none !important;
}
/*# sourceMappingURL=bootstrap-reboot.rtl.css.map */

+ 1
- 0
src/CartWise.Web/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.css.map
文件差異過大導致無法顯示
查看文件


+ 6
- 0
src/CartWise.Web/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.min.css
文件差異過大導致無法顯示
查看文件


+ 1
- 0
src/CartWise.Web/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.min.css.map
文件差異過大導致無法顯示
查看文件


+ 5402
- 0
src/CartWise.Web/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.css
文件差異過大導致無法顯示
查看文件


+ 1
- 0
src/CartWise.Web/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.css.map
文件差異過大導致無法顯示
查看文件


+ 6
- 0
src/CartWise.Web/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.min.css
文件差異過大導致無法顯示
查看文件


+ 1
- 0
src/CartWise.Web/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.min.css.map
文件差異過大導致無法顯示
查看文件


+ 5393
- 0
src/CartWise.Web/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.css
文件差異過大導致無法顯示
查看文件


+ 1
- 0
src/CartWise.Web/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.css.map
文件差異過大導致無法顯示
查看文件


+ 6
- 0
src/CartWise.Web/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.min.css
文件差異過大導致無法顯示
查看文件


+ 1
- 0
src/CartWise.Web/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.min.css.map
文件差異過大導致無法顯示
查看文件


+ 12057
- 0
src/CartWise.Web/wwwroot/lib/bootstrap/dist/css/bootstrap.css
文件差異過大導致無法顯示
查看文件


+ 1
- 0
src/CartWise.Web/wwwroot/lib/bootstrap/dist/css/bootstrap.css.map
文件差異過大導致無法顯示
查看文件


+ 6
- 0
src/CartWise.Web/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css
文件差異過大導致無法顯示
查看文件


+ 1
- 0
src/CartWise.Web/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css.map
文件差異過大導致無法顯示
查看文件


+ 12030
- 0
src/CartWise.Web/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.css
文件差異過大導致無法顯示
查看文件


+ 1
- 0
src/CartWise.Web/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.css.map
文件差異過大導致無法顯示
查看文件


+ 6
- 0
src/CartWise.Web/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.min.css
文件差異過大導致無法顯示
查看文件


+ 1
- 0
src/CartWise.Web/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.min.css.map
文件差異過大導致無法顯示
查看文件


+ 6314
- 0
src/CartWise.Web/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.js
文件差異過大導致無法顯示
查看文件


+ 1
- 0
src/CartWise.Web/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.js.map
文件差異過大導致無法顯示
查看文件


+ 7
- 0
src/CartWise.Web/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.min.js
文件差異過大導致無法顯示
查看文件


+ 1
- 0
src/CartWise.Web/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.min.js.map
文件差異過大導致無法顯示
查看文件


+ 4447
- 0
src/CartWise.Web/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.js
文件差異過大導致無法顯示
查看文件


+ 1
- 0
src/CartWise.Web/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.js.map
文件差異過大導致無法顯示
查看文件


+ 7
- 0
src/CartWise.Web/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.min.js
文件差異過大導致無法顯示
查看文件


+ 1
- 0
src/CartWise.Web/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.min.js.map
文件差異過大導致無法顯示
查看文件


+ 4494
- 0
src/CartWise.Web/wwwroot/lib/bootstrap/dist/js/bootstrap.js
文件差異過大導致無法顯示
查看文件


+ 1
- 0
src/CartWise.Web/wwwroot/lib/bootstrap/dist/js/bootstrap.js.map
文件差異過大導致無法顯示
查看文件


+ 7
- 0
src/CartWise.Web/wwwroot/lib/bootstrap/dist/js/bootstrap.min.js
文件差異過大導致無法顯示
查看文件


+ 1
- 0
src/CartWise.Web/wwwroot/lib/bootstrap/dist/js/bootstrap.min.js.map
文件差異過大導致無法顯示
查看文件


+ 23
- 0
src/CartWise.Web/wwwroot/lib/jquery-validation-unobtrusive/LICENSE.txt 查看文件

@@ -0,0 +1,23 @@
The MIT License (MIT)

Copyright (c) .NET Foundation and Contributors

All rights reserved.

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

+ 435
- 0
src/CartWise.Web/wwwroot/lib/jquery-validation-unobtrusive/dist/jquery.validate.unobtrusive.js 查看文件

@@ -0,0 +1,435 @@
/**
* @license
* Unobtrusive validation support library for jQuery and jQuery Validate
* Copyright (c) .NET Foundation. All rights reserved.
* Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
* @version v4.0.0
*/

/*jslint white: true, browser: true, onevar: true, undef: true, nomen: true, eqeqeq: true, plusplus: true, bitwise: true, regexp: true, newcap: true, immed: true, strict: false */
/*global document: false, jQuery: false */

(function (factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define("jquery.validate.unobtrusive", ['jquery-validation'], factory);
} else if (typeof module === 'object' && module.exports) {
// CommonJS-like environments that support module.exports
module.exports = factory(require('jquery-validation'));
} else {
// Browser global
jQuery.validator.unobtrusive = factory(jQuery);
}
}(function ($) {
var $jQval = $.validator,
adapters,
data_validation = "unobtrusiveValidation";

function setValidationValues(options, ruleName, value) {
options.rules[ruleName] = value;
if (options.message) {
options.messages[ruleName] = options.message;
}
}

function splitAndTrim(value) {
return value.replace(/^\s+|\s+$/g, "").split(/\s*,\s*/g);
}

function escapeAttributeValue(value) {
// As mentioned on http://api.jquery.com/category/selectors/
return value.replace(/([!"#$%&'()*+,./:;<=>?@\[\\\]^`{|}~])/g, "\\$1");
}

function getModelPrefix(fieldName) {
return fieldName.substr(0, fieldName.lastIndexOf(".") + 1);
}

function appendModelPrefix(value, prefix) {
if (value.indexOf("*.") === 0) {
value = value.replace("*.", prefix);
}
return value;
}

function onError(error, inputElement) { // 'this' is the form element
var container = $(this).find("[data-valmsg-for='" + escapeAttributeValue(inputElement[0].name) + "']"),
replaceAttrValue = container.attr("data-valmsg-replace"),
replace = replaceAttrValue ? $.parseJSON(replaceAttrValue) !== false : null;

container.removeClass("field-validation-valid").addClass("field-validation-error");
error.data("unobtrusiveContainer", container);

if (replace) {
container.empty();
error.removeClass("input-validation-error").appendTo(container);
}
else {
error.hide();
}
}

function onErrors(event, validator) { // 'this' is the form element
var container = $(this).find("[data-valmsg-summary=true]"),
list = container.find("ul");

if (list && list.length && validator.errorList.length) {
list.empty();
container.addClass("validation-summary-errors").removeClass("validation-summary-valid");

$.each(validator.errorList, function () {
$("<li />").html(this.message).appendTo(list);
});
}
}

function onSuccess(error) { // 'this' is the form element
var container = error.data("unobtrusiveContainer");

if (container) {
var replaceAttrValue = container.attr("data-valmsg-replace"),
replace = replaceAttrValue ? $.parseJSON(replaceAttrValue) : null;

container.addClass("field-validation-valid").removeClass("field-validation-error");
error.removeData("unobtrusiveContainer");

if (replace) {
container.empty();
}
}
}

function onReset(event) { // 'this' is the form element
var $form = $(this),
key = '__jquery_unobtrusive_validation_form_reset';
if ($form.data(key)) {
return;
}
// Set a flag that indicates we're currently resetting the form.
$form.data(key, true);
try {
$form.data("validator").resetForm();
} finally {
$form.removeData(key);
}

$form.find(".validation-summary-errors")
.addClass("validation-summary-valid")
.removeClass("validation-summary-errors");
$form.find(".field-validation-error")
.addClass("field-validation-valid")
.removeClass("field-validation-error")
.removeData("unobtrusiveContainer")
.find(">*") // If we were using valmsg-replace, get the underlying error
.removeData("unobtrusiveContainer");
}

function validationInfo(form) {
var $form = $(form),
result = $form.data(data_validation),
onResetProxy = $.proxy(onReset, form),
defaultOptions = $jQval.unobtrusive.options || {},
execInContext = function (name, args) {
var func = defaultOptions[name];
func && $.isFunction(func) && func.apply(form, args);
};

if (!result) {
result = {
options: { // options structure passed to jQuery Validate's validate() method
errorClass: defaultOptions.errorClass || "input-validation-error",
errorElement: defaultOptions.errorElement || "span",
errorPlacement: function () {
onError.apply(form, arguments);
execInContext("errorPlacement", arguments);
},
invalidHandler: function () {
onErrors.apply(form, arguments);
execInContext("invalidHandler", arguments);
},
messages: {},
rules: {},
success: function () {
onSuccess.apply(form, arguments);
execInContext("success", arguments);
}
},
attachValidation: function () {
$form
.off("reset." + data_validation, onResetProxy)
.on("reset." + data_validation, onResetProxy)
.validate(this.options);
},
validate: function () { // a validation function that is called by unobtrusive Ajax
$form.validate();
return $form.valid();
}
};
$form.data(data_validation, result);
}

return result;
}

$jQval.unobtrusive = {
adapters: [],

parseElement: function (element, skipAttach) {
/// <summary>
/// Parses a single HTML element for unobtrusive validation attributes.
/// </summary>
/// <param name="element" domElement="true">The HTML element to be parsed.</param>
/// <param name="skipAttach" type="Boolean">[Optional] true to skip attaching the
/// validation to the form. If parsing just this single element, you should specify true.
/// If parsing several elements, you should specify false, and manually attach the validation
/// to the form when you are finished. The default is false.</param>
var $element = $(element),
form = $element.parents("form")[0],
valInfo, rules, messages;

if (!form) { // Cannot do client-side validation without a form
return;
}

valInfo = validationInfo(form);
valInfo.options.rules[element.name] = rules = {};
valInfo.options.messages[element.name] = messages = {};

$.each(this.adapters, function () {
var prefix = "data-val-" + this.name,
message = $element.attr(prefix),
paramValues = {};

if (message !== undefined) { // Compare against undefined, because an empty message is legal (and falsy)
prefix += "-";

$.each(this.params, function () {
paramValues[this] = $element.attr(prefix + this);
});

this.adapt({
element: element,
form: form,
message: message,
params: paramValues,
rules: rules,
messages: messages
});
}
});

$.extend(rules, { "__dummy__": true });

if (!skipAttach) {
valInfo.attachValidation();
}
},

parse: function (selector) {
/// <summary>
/// Parses all the HTML elements in the specified selector. It looks for input elements decorated
/// with the [data-val=true] attribute value and enables validation according to the data-val-*
/// attribute values.
/// </summary>
/// <param name="selector" type="String">Any valid jQuery selector.</param>

// $forms includes all forms in selector's DOM hierarchy (parent, children and self) that have at least one
// element with data-val=true
var $selector = $(selector),
$forms = $selector.parents()
.addBack()
.filter("form")
.add($selector.find("form"))
.has("[data-val=true]");

$selector.find("[data-val=true]").each(function () {
$jQval.unobtrusive.parseElement(this, true);
});

$forms.each(function () {
var info = validationInfo(this);
if (info) {
info.attachValidation();
}
});
}
};

adapters = $jQval.unobtrusive.adapters;

adapters.add = function (adapterName, params, fn) {
/// <summary>Adds a new adapter to convert unobtrusive HTML into a jQuery Validate validation.</summary>
/// <param name="adapterName" type="String">The name of the adapter to be added. This matches the name used
/// in the data-val-nnnn HTML attribute (where nnnn is the adapter name).</param>
/// <param name="params" type="Array" optional="true">[Optional] An array of parameter names (strings) that will
/// be extracted from the data-val-nnnn-mmmm HTML attributes (where nnnn is the adapter name, and
/// mmmm is the parameter name).</param>
/// <param name="fn" type="Function">The function to call, which adapts the values from the HTML
/// attributes into jQuery Validate rules and/or messages.</param>
/// <returns type="jQuery.validator.unobtrusive.adapters" />
if (!fn) { // Called with no params, just a function
fn = params;
params = [];
}
this.push({ name: adapterName, params: params, adapt: fn });
return this;
};

adapters.addBool = function (adapterName, ruleName) {
/// <summary>Adds a new adapter to convert unobtrusive HTML into a jQuery Validate validation, where
/// the jQuery Validate validation rule has no parameter values.</summary>
/// <param name="adapterName" type="String">The name of the adapter to be added. This matches the name used
/// in the data-val-nnnn HTML attribute (where nnnn is the adapter name).</param>
/// <param name="ruleName" type="String" optional="true">[Optional] The name of the jQuery Validate rule. If not provided, the value
/// of adapterName will be used instead.</param>
/// <returns type="jQuery.validator.unobtrusive.adapters" />
return this.add(adapterName, function (options) {
setValidationValues(options, ruleName || adapterName, true);
});
};

adapters.addMinMax = function (adapterName, minRuleName, maxRuleName, minMaxRuleName, minAttribute, maxAttribute) {
/// <summary>Adds a new adapter to convert unobtrusive HTML into a jQuery Validate validation, where
/// the jQuery Validate validation has three potential rules (one for min-only, one for max-only, and
/// one for min-and-max). The HTML parameters are expected to be named -min and -max.</summary>
/// <param name="adapterName" type="String">The name of the adapter to be added. This matches the name used
/// in the data-val-nnnn HTML attribute (where nnnn is the adapter name).</param>
/// <param name="minRuleName" type="String">The name of the jQuery Validate rule to be used when you only
/// have a minimum value.</param>
/// <param name="maxRuleName" type="String">The name of the jQuery Validate rule to be used when you only
/// have a maximum value.</param>
/// <param name="minMaxRuleName" type="String">The name of the jQuery Validate rule to be used when you
/// have both a minimum and maximum value.</param>
/// <param name="minAttribute" type="String" optional="true">[Optional] The name of the HTML attribute that
/// contains the minimum value. The default is "min".</param>
/// <param name="maxAttribute" type="String" optional="true">[Optional] The name of the HTML attribute that
/// contains the maximum value. The default is "max".</param>
/// <returns type="jQuery.validator.unobtrusive.adapters" />
return this.add(adapterName, [minAttribute || "min", maxAttribute || "max"], function (options) {
var min = options.params.min,
max = options.params.max;

if (min && max) {
setValidationValues(options, minMaxRuleName, [min, max]);
}
else if (min) {
setValidationValues(options, minRuleName, min);
}
else if (max) {
setValidationValues(options, maxRuleName, max);
}
});
};

adapters.addSingleVal = function (adapterName, attribute, ruleName) {
/// <summary>Adds a new adapter to convert unobtrusive HTML into a jQuery Validate validation, where
/// the jQuery Validate validation rule has a single value.</summary>
/// <param name="adapterName" type="String">The name of the adapter to be added. This matches the name used
/// in the data-val-nnnn HTML attribute(where nnnn is the adapter name).</param>
/// <param name="attribute" type="String">[Optional] The name of the HTML attribute that contains the value.
/// The default is "val".</param>
/// <param name="ruleName" type="String" optional="true">[Optional] The name of the jQuery Validate rule. If not provided, the value
/// of adapterName will be used instead.</param>
/// <returns type="jQuery.validator.unobtrusive.adapters" />
return this.add(adapterName, [attribute || "val"], function (options) {
setValidationValues(options, ruleName || adapterName, options.params[attribute]);
});
};

$jQval.addMethod("__dummy__", function (value, element, params) {
return true;
});

$jQval.addMethod("regex", function (value, element, params) {
var match;
if (this.optional(element)) {
return true;
}

match = new RegExp(params).exec(value);
return (match && (match.index === 0) && (match[0].length === value.length));
});

$jQval.addMethod("nonalphamin", function (value, element, nonalphamin) {
var match;
if (nonalphamin) {
match = value.match(/\W/g);
match = match && match.length >= nonalphamin;
}
return match;
});

if ($jQval.methods.extension) {
adapters.addSingleVal("accept", "mimtype");
adapters.addSingleVal("extension", "extension");
} else {
// for backward compatibility, when the 'extension' validation method does not exist, such as with versions
// of JQuery Validation plugin prior to 1.10, we should use the 'accept' method for
// validating the extension, and ignore mime-type validations as they are not supported.
adapters.addSingleVal("extension", "extension", "accept");
}

adapters.addSingleVal("regex", "pattern");
adapters.addBool("creditcard").addBool("date").addBool("digits").addBool("email").addBool("number").addBool("url");
adapters.addMinMax("length", "minlength", "maxlength", "rangelength").addMinMax("range", "min", "max", "range");
adapters.addMinMax("minlength", "minlength").addMinMax("maxlength", "minlength", "maxlength");
adapters.add("equalto", ["other"], function (options) {
var prefix = getModelPrefix(options.element.name),
other = options.params.other,
fullOtherName = appendModelPrefix(other, prefix),
element = $(options.form).find(":input").filter("[name='" + escapeAttributeValue(fullOtherName) + "']")[0];

setValidationValues(options, "equalTo", element);
});
adapters.add("required", function (options) {
// jQuery Validate equates "required" with "mandatory" for checkbox elements
if (options.element.tagName.toUpperCase() !== "INPUT" || options.element.type.toUpperCase() !== "CHECKBOX") {
setValidationValues(options, "required", true);
}
});
adapters.add("remote", ["url", "type", "additionalfields"], function (options) {
var value = {
url: options.params.url,
type: options.params.type || "GET",
data: {}
},
prefix = getModelPrefix(options.element.name);

$.each(splitAndTrim(options.params.additionalfields || options.element.name), function (i, fieldName) {
var paramName = appendModelPrefix(fieldName, prefix);
value.data[paramName] = function () {
var field = $(options.form).find(":input").filter("[name='" + escapeAttributeValue(paramName) + "']");
// For checkboxes and radio buttons, only pick up values from checked fields.
if (field.is(":checkbox")) {
return field.filter(":checked").val() || field.filter(":hidden").val() || '';
}
else if (field.is(":radio")) {
return field.filter(":checked").val() || '';
}
return field.val();
};
});

setValidationValues(options, "remote", value);
});
adapters.add("password", ["min", "nonalphamin", "regex"], function (options) {
if (options.params.min) {
setValidationValues(options, "minlength", options.params.min);
}
if (options.params.nonalphamin) {
setValidationValues(options, "nonalphamin", options.params.nonalphamin);
}
if (options.params.regex) {
setValidationValues(options, "regex", options.params.regex);
}
});
adapters.add("fileextensions", ["extensions"], function (options) {
setValidationValues(options, "extension", options.params.extensions);
});

$(function () {
$jQval.unobtrusive.parse(document);
});

return $jQval.unobtrusive;
}));

+ 8
- 0
src/CartWise.Web/wwwroot/lib/jquery-validation-unobtrusive/dist/jquery.validate.unobtrusive.min.js
文件差異過大導致無法顯示
查看文件


+ 22
- 0
src/CartWise.Web/wwwroot/lib/jquery-validation/LICENSE.md 查看文件

@@ -0,0 +1,22 @@
The MIT License (MIT)
=====================

Copyright Jörn Zaefferer

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

部分文件因文件數量過多而無法顯示

Loading…
取消
儲存

Powered by TurnKey Linux.