├── .editorconfig ├── .gitignore ├── Cofoundry.Samples.SPASite.sln ├── InitData ├── Images │ ├── 10.jpg │ ├── 11.jpg │ ├── 12.jpg │ ├── 13.jpg │ ├── 14.jpg │ ├── 15.jpg │ ├── 16.jpg │ ├── 17.jpg │ ├── 18.jpg │ ├── 19.jpg │ ├── 20.jpg │ ├── 21.jpg │ ├── 6.jpg │ ├── 7.jpg │ ├── 8.jpg │ ├── 9.jpg │ └── cc-images.txt └── Init.sql ├── LICENSE ├── README.md ├── readme ├── AdminCatCreate.png ├── AdminCatList.png ├── Homepage.jpg ├── SpaCatsDomain.png └── SpaCatsWeb.png └── src ├── Cofoundry.Samples.SPASite.Domain ├── Cofoundry.Samples.SPASite.Domain.csproj ├── Data │ ├── Bootstrap │ │ └── DataDependencyRegistration.cs │ ├── Cats │ │ ├── CatLike.cs │ │ ├── CatLikeCount.cs │ │ ├── CatLikeCountMap.cs │ │ └── CatLikeMap.cs │ └── SPASiteDbContext.cs ├── Domain │ ├── Breeds │ │ ├── Definition │ │ │ ├── BreedCustomEntityDefinition.cs │ │ │ └── BreedDataModel.cs │ │ ├── Models │ │ │ └── Breed.cs │ │ └── Queries │ │ │ ├── GetAllBreedsQuery.cs │ │ │ ├── GetAllBreedsQueryHandler.cs │ │ │ ├── GetBreedByIdQuery.cs │ │ │ └── GetBreedByIdQueryHandler.cs │ ├── Cats │ │ ├── Commands │ │ │ ├── SetCatLikedCommand.cs │ │ │ └── SetCatLikedCommandHandler.cs │ │ ├── Definition │ │ │ ├── CatCustomEntityDefinition.cs │ │ │ └── CatDataModel.cs │ │ ├── Models │ │ │ ├── CatDetails.cs │ │ │ └── CatSummary.cs │ │ └── Queries │ │ │ ├── GetCatDetailsByIdQuery.cs │ │ │ ├── GetCatDetailsByIdQueryHandler.cs │ │ │ ├── GetCatSummariesByMemberLikedQuery.cs │ │ │ ├── GetCatSummariesByMemberLikedQueryHandler.cs │ │ │ ├── SearchCatSummariesQuery.cs │ │ │ └── SearchCatSummariesQueryHandler.cs │ ├── Features │ │ ├── Definition │ │ │ ├── FeatureCustomEntityDefinition.cs │ │ │ └── FeatureDataModel.cs │ │ ├── Model │ │ │ └── Feature.cs │ │ └── Queries │ │ │ ├── GetAllFeaturesQuery.cs │ │ │ ├── GetAllFeaturesQueryHandler.cs │ │ │ ├── GetFeaturesByIdRangeQuery.cs │ │ │ └── GetFeaturesByIdRangeQueryHandler.cs │ ├── Members │ │ ├── Commands │ │ │ ├── RegisterMemberAndLogInCommand.cs │ │ │ ├── RegisterMemberAndLogInCommandHandler.cs │ │ │ ├── SignMemberInCommand.cs │ │ │ ├── SignMemberInCommandHandler.cs │ │ │ ├── SignMemberOutCommand.cs │ │ │ └── SignMemberOutCommandHandler.cs │ │ ├── Definition │ │ │ ├── MemberRole.cs │ │ │ └── MemberUserArea.cs │ │ ├── Models │ │ │ └── MemberSummary.cs │ │ └── Queries │ │ │ ├── GetCurrentMemberSummaryQuery.cs │ │ │ └── GetCurrentMemberSummaryQueryHandler.cs │ └── ReadMe.md ├── Install │ ├── Db │ │ ├── Schema │ │ │ └── 0001.sql │ │ └── StoredProcedures │ │ │ └── app.CatLike_SetLiked.sql │ └── UpdatePackageFactory.cs ├── MailTemplates │ ├── Bootstrap │ │ └── AssemblyResourceRegistration.cs │ ├── NewUserWelcomeMailTemplate.cs │ ├── NewUserWelcomeMail_Html.cshtml │ ├── NewUserWelcomeMail_Text.cshtml │ ├── NewUserWelcomeMail_html.cshtml │ ├── NewUserWelcomeMail_text.cshtml │ └── _ViewImports.cshtml └── Usings.cs └── Cofoundry.Samples.SPASite ├── Api ├── AuthApiController.cs ├── BreedsApiController.cs ├── CatsApiController.cs ├── CurrentUserApiController.cs ├── FeaturesApiController.cs └── ReadMe.md ├── ClientApp ├── README.md ├── babel.config.js ├── dist │ ├── css │ │ ├── CatDetails.e8c33d00.css │ │ ├── Login~Register.76e18863.css │ │ └── app.81c98cd1.css │ ├── favicon.png │ ├── img │ │ ├── cofoundry-logo.6a7dc3a6.png │ │ └── spacats-logo.602f6e89.png │ ├── index.html │ └── js │ │ ├── 404.5e643d04.js │ │ ├── CatDetails.98ad2d1f.js │ │ ├── Login.8041039d.js │ │ ├── Login~Register.c7c8d3ab.js │ │ ├── Register.99e7157e.js │ │ ├── app.4e13b28a.js │ │ └── chunk-vendors.86339dbf.js ├── package-lock.json ├── package.json ├── public │ ├── favicon.png │ └── index.html ├── src │ ├── App.vue │ ├── api │ │ ├── auth.js │ │ ├── axiosHelper.js │ │ ├── cats.js │ │ └── currentMember.js │ ├── assets │ │ ├── cc-images.txt │ │ ├── cofoundry-logo.png │ │ ├── heart-icon.png │ │ └── spacats-logo.png │ ├── components │ │ ├── CatGrid.vue │ │ ├── CatItem.vue │ │ ├── ContentPanel.vue │ │ ├── FormActions.vue │ │ ├── FormGroup.vue │ │ ├── ImageAsset.vue │ │ ├── LikesCounter.vue │ │ ├── Loader.vue │ │ ├── SiteFooter.vue │ │ ├── SiteNav.vue │ │ ├── SubmitButton.vue │ │ └── ValidationSummary.vue │ ├── main.js │ ├── router.js │ ├── scss │ │ ├── mixins.scss │ │ ├── normalize.scss │ │ └── variables.scss │ ├── store │ │ ├── index.js │ │ └── modules │ │ │ ├── auth.js │ │ │ └── cats.js │ └── views │ │ ├── CatDetails.vue │ │ ├── Home.vue │ │ ├── Login.vue │ │ ├── NotFound.vue │ │ └── Register.vue └── vue.config.js ├── Cofoundry.Samples.SPASite.csproj ├── Program.cs ├── Properties └── launchSettings.json ├── Usings.cs ├── appsettings.Development.json └── appsettings.json /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | indent_style = space 3 | charset = utf-8 4 | trim_trailing_whitespace = true 5 | insert_final_newline = true 6 | 7 | [*.cs] 8 | indent_size = 4 9 | dotnet_sort_system_directives_first = true 10 | 11 | # Don't use this. qualifier 12 | dotnet_style_qualification_for_field = false:suggestion 13 | dotnet_style_qualification_for_property = false:suggestion 14 | 15 | # use int x = .. over Int32 16 | dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion 17 | 18 | # use int.MaxValue over Int32.MaxValue 19 | dotnet_style_predefined_type_for_member_access = true:suggestion 20 | 21 | # Require var all the time. 22 | csharp_style_var_for_built_in_types = true:suggestion 23 | csharp_style_var_when_type_is_apparent = true:suggestion 24 | csharp_style_var_elsewhere = true:suggestion 25 | 26 | # Disallow throw expressions. 27 | csharp_style_throw_expression = false:suggestion 28 | 29 | # Newline settings 30 | csharp_new_line_before_open_brace = all 31 | csharp_new_line_before_else = true 32 | csharp_new_line_before_catch = true 33 | csharp_new_line_before_finally = true 34 | csharp_new_line_before_members_in_object_initializers = true 35 | csharp_new_line_before_members_in_anonymous_types = true 36 | 37 | # Namespace settings 38 | csharp_style_namespace_declarations = file_scoped 39 | 40 | # Brace settings 41 | csharp_prefer_braces = true # Prefer curly braces even for one line of code 42 | 43 | # name all constant fields using PascalCase 44 | dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = suggestion 45 | dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields 46 | dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style 47 | dotnet_naming_symbols.constant_fields.applicable_kinds = field 48 | dotnet_naming_symbols.constant_fields.required_modifiers = const 49 | dotnet_naming_style.pascal_case_style.capitalization = pascal_case 50 | 51 | # internal and private fields should be _camelCase 52 | dotnet_naming_rule.camel_case_for_private_internal_fields.severity = suggestion 53 | dotnet_naming_rule.camel_case_for_private_internal_fields.symbols = private_internal_fields 54 | dotnet_naming_rule.camel_case_for_private_internal_fields.style = camel_case_underscore_style 55 | dotnet_naming_symbols.private_internal_fields.applicable_kinds = field 56 | dotnet_naming_symbols.private_internal_fields.applicable_accessibilities = private, internal 57 | dotnet_naming_style.camel_case_underscore_style.required_prefix = _ 58 | dotnet_naming_style.camel_case_underscore_style.capitalization = camel_case 59 | 60 | [*.{xml,config,*proj,nuspec,props,resx,targets,yml,tasks}] 61 | indent_size = 2 62 | 63 | # Xml config files 64 | [*.{props,targets,ruleset,config,nuspec,resx,vsixmanifest,vsct}] 65 | indent_size = 2 66 | 67 | [*.json] 68 | indent_size = 2 69 | 70 | [*.{ps1,psm1}] 71 | indent_size = 4 72 | 73 | [*.sh] 74 | indent_size = 4 75 | end_of_line = lf 76 | 77 | [*.{razor,cshtml}] 78 | charset = utf-8-bom 79 | 80 | [*.{cs,vb}] 81 | 82 | # SYSLIB1054: Use 'LibraryImportAttribute' instead of 'DllImportAttribute' to generate P/Invoke marshalling code at compile time 83 | dotnet_diagnostic.SYSLIB1054.severity = warning 84 | 85 | # CA1018: Mark attributes with AttributeUsageAttribute 86 | dotnet_diagnostic.CA1018.severity = warning 87 | 88 | # CA1047: Do not declare protected member in sealed type 89 | dotnet_diagnostic.CA1047.severity = warning 90 | 91 | # CA1305: Specify IFormatProvider 92 | dotnet_diagnostic.CA1305.severity = warning 93 | 94 | # CA1507: Use nameof to express symbol names 95 | dotnet_diagnostic.CA1507.severity = warning 96 | 97 | # CA1510: Use ArgumentNullException throw helper 98 | dotnet_diagnostic.CA1510.severity = warning 99 | 100 | # CA1511: Use ArgumentException throw helper 101 | dotnet_diagnostic.CA1511.severity = warning 102 | 103 | # CA1512: Use ArgumentOutOfRangeException throw helper 104 | dotnet_diagnostic.CA1512.severity = warning 105 | 106 | # CA1513: Use ObjectDisposedException throw helper 107 | dotnet_diagnostic.CA1513.severity = warning 108 | 109 | # CA1725: Parameter names should match base declaration 110 | dotnet_diagnostic.CA1725.severity = suggestion 111 | 112 | # CA1802: Use literals where appropriate 113 | dotnet_diagnostic.CA1802.severity = warning 114 | 115 | # CA1805: Do not initialize unnecessarily 116 | dotnet_diagnostic.CA1805.severity = warning 117 | 118 | # CA1810: Do not initialize unnecessarily 119 | dotnet_diagnostic.CA1810.severity = warning 120 | 121 | # CA1821: Remove empty Finalizers 122 | dotnet_diagnostic.CA1821.severity = warning 123 | 124 | # CA1822: Make member static 125 | dotnet_diagnostic.CA1822.severity = warning 126 | dotnet_code_quality.CA1822.api_surface = private, internal 127 | 128 | # CA1823: Avoid unused private fields 129 | dotnet_diagnostic.CA1823.severity = warning 130 | 131 | # CA1825: Avoid zero-length array allocations 132 | dotnet_diagnostic.CA1825.severity = warning 133 | 134 | # CA1826: Do not use Enumerable methods on indexable collections. Instead use the collection directly 135 | dotnet_diagnostic.CA1826.severity = warning 136 | 137 | # CA1827: Do not use Count() or LongCount() when Any() can be used 138 | dotnet_diagnostic.CA1827.severity = warning 139 | 140 | # CA1828: Do not use CountAsync() or LongCountAsync() when AnyAsync() can be used 141 | dotnet_diagnostic.CA1828.severity = warning 142 | 143 | # CA1829: Use Length/Count property instead of Count() when available 144 | dotnet_diagnostic.CA1829.severity = warning 145 | 146 | # CA1830: Prefer strongly-typed Append and Insert method overloads on StringBuilder 147 | dotnet_diagnostic.CA1830.severity = warning 148 | 149 | # CA1831: Use AsSpan or AsMemory instead of Range-based indexers when appropriate 150 | dotnet_diagnostic.CA1831.severity = warning 151 | 152 | # CA1832: Use AsSpan or AsMemory instead of Range-based indexers when appropriate 153 | dotnet_diagnostic.CA1832.severity = warning 154 | 155 | # CA1833: Use AsSpan or AsMemory instead of Range-based indexers when appropriate 156 | dotnet_diagnostic.CA1833.severity = warning 157 | 158 | # CA1834: Consider using 'StringBuilder.Append(char)' when applicable 159 | dotnet_diagnostic.CA1834.severity = warning 160 | 161 | # CA1835: Prefer the 'Memory'-based overloads for 'ReadAsync' and 'WriteAsync' 162 | dotnet_diagnostic.CA1835.severity = warning 163 | 164 | # CA1836: Prefer IsEmpty over Count 165 | dotnet_diagnostic.CA1836.severity = warning 166 | 167 | # CA1837: Use 'Environment.ProcessId' 168 | dotnet_diagnostic.CA1837.severity = warning 169 | 170 | # CA1838: Avoid 'StringBuilder' parameters for P/Invokes 171 | dotnet_diagnostic.CA1838.severity = warning 172 | 173 | # CA1839: Use 'Environment.ProcessPath' 174 | dotnet_diagnostic.CA1839.severity = warning 175 | 176 | # CA1840: Use 'Environment.CurrentManagedThreadId' 177 | dotnet_diagnostic.CA1840.severity = warning 178 | 179 | # CA1841: Prefer Dictionary.Contains methods 180 | dotnet_diagnostic.CA1841.severity = warning 181 | 182 | # CA1842: Do not use 'WhenAll' with a single task 183 | dotnet_diagnostic.CA1842.severity = warning 184 | 185 | # CA1843: Do not use 'WaitAll' with a single task 186 | dotnet_diagnostic.CA1843.severity = warning 187 | 188 | # CA1844: Provide memory-based overrides of async methods when subclassing 'Stream' 189 | dotnet_diagnostic.CA1844.severity = warning 190 | 191 | # CA1845: Use span-based 'string.Concat' 192 | dotnet_diagnostic.CA1845.severity = warning 193 | 194 | # CA1846: Prefer AsSpan over Substring 195 | dotnet_diagnostic.CA1846.severity = warning 196 | 197 | # CA1847: Use string.Contains(char) instead of string.Contains(string) with single characters 198 | dotnet_diagnostic.CA1847.severity = warning 199 | 200 | # CA1854: Prefer the IDictionary.TryGetValue(TKey, out TValue) method 201 | dotnet_diagnostic.CA1854.severity = warning 202 | 203 | # CA1855: Prefer 'Clear' over 'Fill' 204 | dotnet_diagnostic.CA1855.severity = warning 205 | 206 | # CA1856: Incorrect usage of ConstantExpected attribute 207 | dotnet_diagnostic.CA1856.severity = error 208 | 209 | # CA1857: A constant is expected for the parameter 210 | dotnet_diagnostic.CA1857.severity = warning 211 | 212 | # CA1858: Use 'StartsWith' instead of 'IndexOf' 213 | dotnet_diagnostic.CA1858.severity = warning 214 | 215 | # CA2008: Do not create tasks without passing a TaskScheduler 216 | dotnet_diagnostic.CA2008.severity = warning 217 | 218 | # CA2009: Do not call ToImmutableCollection on an ImmutableCollection value 219 | dotnet_diagnostic.CA2009.severity = warning 220 | 221 | # CA2011: Avoid infinite recursion 222 | dotnet_diagnostic.CA2011.severity = warning 223 | 224 | # CA2012: Use ValueTask correctly 225 | dotnet_diagnostic.CA2012.severity = warning 226 | 227 | # CA2013: Do not use ReferenceEquals with value types 228 | dotnet_diagnostic.CA2013.severity = warning 229 | 230 | # CA2014: Do not use stackalloc in loops. 231 | dotnet_diagnostic.CA2014.severity = warning 232 | 233 | # CA2016: Forward the 'CancellationToken' parameter to methods that take one 234 | dotnet_diagnostic.CA2016.severity = warning 235 | 236 | # CA2200: Rethrow to preserve stack details 237 | dotnet_diagnostic.CA2200.severity = warning 238 | 239 | # CA2208: Instantiate argument exceptions correctly 240 | dotnet_diagnostic.CA2208.severity = warning 241 | 242 | # CA2245: Do not assign a property to itself 243 | dotnet_diagnostic.CA2245.severity = warning 244 | 245 | # CA2246: Assigning symbol and its member in the same statement 246 | dotnet_diagnostic.CA2246.severity = warning 247 | 248 | # CA2249: Use string.Contains instead of string.IndexOf to improve readability. 249 | dotnet_diagnostic.CA2249.severity = warning 250 | 251 | # IDE0005: Remove unnecessary usings 252 | dotnet_diagnostic.IDE0005.severity = warning 253 | 254 | # IDE0011: Curly braces to surround blocks of code 255 | dotnet_diagnostic.IDE0011.severity = warning 256 | 257 | # IDE0020: Use pattern matching to avoid is check followed by a cast (with variable) 258 | dotnet_diagnostic.IDE0020.severity = warning 259 | 260 | # IDE0029: Use coalesce expression (non-nullable types) 261 | dotnet_diagnostic.IDE0029.severity = warning 262 | 263 | # IDE0030: Use coalesce expression (nullable types) 264 | dotnet_diagnostic.IDE0030.severity = warning 265 | 266 | # IDE0031: Use null propagation 267 | dotnet_diagnostic.IDE0031.severity = warning 268 | 269 | # IDE0035: Remove unreachable code 270 | dotnet_diagnostic.IDE0035.severity = warning 271 | 272 | # IDE0036: Order modifiers 273 | csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:suggestion 274 | dotnet_diagnostic.IDE0036.severity = warning 275 | 276 | # IDE0038: Use pattern matching to avoid is check followed by a cast (without variable) 277 | dotnet_diagnostic.IDE0038.severity = warning 278 | 279 | # IDE0043: Format string contains invalid placeholder 280 | dotnet_diagnostic.IDE0043.severity = warning 281 | 282 | # IDE0044: Make field readonly 283 | dotnet_diagnostic.IDE0044.severity = warning 284 | 285 | # IDE0051: Remove unused private members 286 | dotnet_diagnostic.IDE0051.severity = warning 287 | 288 | # IDE0055: All formatting rules 289 | dotnet_diagnostic.IDE0055.severity = suggestion 290 | 291 | # IDE0059: Unnecessary assignment to a value 292 | dotnet_diagnostic.IDE0059.severity = warning 293 | 294 | # IDE0060: Remove unused parameter 295 | dotnet_code_quality_unused_parameters = non_public 296 | dotnet_diagnostic.IDE0060.severity = warning 297 | 298 | # IDE0062: Make local function static 299 | dotnet_diagnostic.IDE0062.severity = warning 300 | 301 | # IDE0161: Convert to file-scoped namespace 302 | dotnet_diagnostic.IDE0161.severity = warning 303 | 304 | # IDE0200: Lambda expression can be removed 305 | dotnet_diagnostic.IDE0200.severity = warning 306 | 307 | # IDE2000: Disallow multiple blank lines 308 | dotnet_style_allow_multiple_blank_lines_experimental = false 309 | dotnet_diagnostic.IDE2000.severity = warning 310 | 311 | # IDE0063: Use simple 'using' statement 312 | dotnet_diagnostic.IDE0063.severity = silent 313 | 314 | # IDE0270: Use coalesce expression 315 | dotnet_diagnostic.IDE0270.severity = silent 316 | 317 | # IDE0290: Use primary constructor 318 | dotnet_diagnostic.IDE0290.severity = silent -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.sln.docstates 8 | 9 | # Build results 10 | 11 | [Dd]ebug/ 12 | [Rr]elease/ 13 | x64/ 14 | [Bb]in/ 15 | [Oo]bj/ 16 | 17 | *_i.c 18 | *_p.c 19 | *.ilk 20 | *.meta 21 | *.obj 22 | *.pch 23 | *.pdb 24 | *.pgc 25 | *.pgd 26 | *.rsp 27 | *.sbr 28 | *.tlb 29 | *.tli 30 | *.tlh 31 | *.tmp 32 | *.tmp_proj 33 | *.log 34 | *.vspscc 35 | *.vssscc 36 | .builds 37 | *.pidb 38 | *.log 39 | *.scc 40 | 41 | # ReSharper is a .NET coding add-in 42 | _ReSharper*/ 43 | *.[Rr]e[Ss]harper 44 | 45 | # Click-Once directory 46 | publish/ 47 | 48 | # Publish Web Output 49 | *.Publish.xml 50 | *.pubxml 51 | 52 | # NuGet Packages Directory 53 | ## TODO: If you have NuGet Package Restore enabled, uncomment the next line 54 | packages/ 55 | 56 | # Others 57 | [Ss]tyle[Cc]op.* 58 | ~$* 59 | *~ 60 | *.dbmdl 61 | *.[Pp]ublish.xml 62 | *.pfx 63 | *.publishsettings 64 | 65 | # SQL Server files 66 | App_Data/*.mdf 67 | App_Data/*.ldf 68 | 69 | # ========================= 70 | # Windows detritus 71 | # ========================= 72 | 73 | # Windows image file caches 74 | Thumbs.db 75 | ehthumbs.db 76 | 77 | # Mac crap 78 | .DS_Store 79 | 80 | # Cake Build 81 | /tools 82 | /artifacts 83 | 84 | .vs 85 | node_modules/ 86 | appsettings.local.json 87 | .vscode 88 | 89 | **/App_Data/Files/ 90 | **/App_Data/Emails/ 91 | 92 | .idea 93 | .sass-cache 94 | jspm_packages 95 | npm-debug.* 96 | link-checker-results.txt 97 | app/**/*.js 98 | *.js.map 99 | e2e/**/*.js 100 | e2e/**/*.js.map 101 | _test-output 102 | _temp 103 | src/Cofoundry.Samples.SPASite/grunt/.sass-cache 104 | -------------------------------------------------------------------------------- /Cofoundry.Samples.SPASite.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.29905.134 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Cofoundry.Samples.SPASite.Domain", "src\Cofoundry.Samples.SPASite.Domain\Cofoundry.Samples.SPASite.Domain.csproj", "{21E17F9F-79A3-4AFE-A119-7EE71F150634}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Cofoundry.Samples.SPASite", "src\Cofoundry.Samples.SPASite\Cofoundry.Samples.SPASite.csproj", "{7A3FB263-7844-4008-9F6B-488145C95CA3}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Release|Any CPU = Release|Any CPU 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {21E17F9F-79A3-4AFE-A119-7EE71F150634}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 17 | {21E17F9F-79A3-4AFE-A119-7EE71F150634}.Debug|Any CPU.Build.0 = Debug|Any CPU 18 | {21E17F9F-79A3-4AFE-A119-7EE71F150634}.Release|Any CPU.ActiveCfg = Release|Any CPU 19 | {21E17F9F-79A3-4AFE-A119-7EE71F150634}.Release|Any CPU.Build.0 = Release|Any CPU 20 | {7A3FB263-7844-4008-9F6B-488145C95CA3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {7A3FB263-7844-4008-9F6B-488145C95CA3}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {7A3FB263-7844-4008-9F6B-488145C95CA3}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {7A3FB263-7844-4008-9F6B-488145C95CA3}.Release|Any CPU.Build.0 = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | GlobalSection(ExtensibilityGlobals) = postSolution 29 | SolutionGuid = {9D945901-0AAF-43C5-859D-4C3ED67601C7} 30 | EndGlobalSection 31 | EndGlobal 32 | -------------------------------------------------------------------------------- /InitData/Images/10.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cofoundry-cms/Cofoundry.Samples.SPASite/ba0f8aaf565dc9b42a172ceac5b14f8aa06dd371/InitData/Images/10.jpg -------------------------------------------------------------------------------- /InitData/Images/11.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cofoundry-cms/Cofoundry.Samples.SPASite/ba0f8aaf565dc9b42a172ceac5b14f8aa06dd371/InitData/Images/11.jpg -------------------------------------------------------------------------------- /InitData/Images/12.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cofoundry-cms/Cofoundry.Samples.SPASite/ba0f8aaf565dc9b42a172ceac5b14f8aa06dd371/InitData/Images/12.jpg -------------------------------------------------------------------------------- /InitData/Images/13.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cofoundry-cms/Cofoundry.Samples.SPASite/ba0f8aaf565dc9b42a172ceac5b14f8aa06dd371/InitData/Images/13.jpg -------------------------------------------------------------------------------- /InitData/Images/14.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cofoundry-cms/Cofoundry.Samples.SPASite/ba0f8aaf565dc9b42a172ceac5b14f8aa06dd371/InitData/Images/14.jpg -------------------------------------------------------------------------------- /InitData/Images/15.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cofoundry-cms/Cofoundry.Samples.SPASite/ba0f8aaf565dc9b42a172ceac5b14f8aa06dd371/InitData/Images/15.jpg -------------------------------------------------------------------------------- /InitData/Images/16.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cofoundry-cms/Cofoundry.Samples.SPASite/ba0f8aaf565dc9b42a172ceac5b14f8aa06dd371/InitData/Images/16.jpg -------------------------------------------------------------------------------- /InitData/Images/17.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cofoundry-cms/Cofoundry.Samples.SPASite/ba0f8aaf565dc9b42a172ceac5b14f8aa06dd371/InitData/Images/17.jpg -------------------------------------------------------------------------------- /InitData/Images/18.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cofoundry-cms/Cofoundry.Samples.SPASite/ba0f8aaf565dc9b42a172ceac5b14f8aa06dd371/InitData/Images/18.jpg -------------------------------------------------------------------------------- /InitData/Images/19.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cofoundry-cms/Cofoundry.Samples.SPASite/ba0f8aaf565dc9b42a172ceac5b14f8aa06dd371/InitData/Images/19.jpg -------------------------------------------------------------------------------- /InitData/Images/20.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cofoundry-cms/Cofoundry.Samples.SPASite/ba0f8aaf565dc9b42a172ceac5b14f8aa06dd371/InitData/Images/20.jpg -------------------------------------------------------------------------------- /InitData/Images/21.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cofoundry-cms/Cofoundry.Samples.SPASite/ba0f8aaf565dc9b42a172ceac5b14f8aa06dd371/InitData/Images/21.jpg -------------------------------------------------------------------------------- /InitData/Images/6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cofoundry-cms/Cofoundry.Samples.SPASite/ba0f8aaf565dc9b42a172ceac5b14f8aa06dd371/InitData/Images/6.jpg -------------------------------------------------------------------------------- /InitData/Images/7.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cofoundry-cms/Cofoundry.Samples.SPASite/ba0f8aaf565dc9b42a172ceac5b14f8aa06dd371/InitData/Images/7.jpg -------------------------------------------------------------------------------- /InitData/Images/8.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cofoundry-cms/Cofoundry.Samples.SPASite/ba0f8aaf565dc9b42a172ceac5b14f8aa06dd371/InitData/Images/8.jpg -------------------------------------------------------------------------------- /InitData/Images/9.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cofoundry-cms/Cofoundry.Samples.SPASite/ba0f8aaf565dc9b42a172ceac5b14f8aa06dd371/InitData/Images/9.jpg -------------------------------------------------------------------------------- /InitData/Images/cc-images.txt: -------------------------------------------------------------------------------- 1 | All images used under terms of creative commons. 2 | 3 | Original Images 4 | Jans Canon - https://www.flickr.com/photos/43158397@N02/4083241988 5 | Paul Sullivan - https://www.flickr.com/photos/pfsullivan_1056/8729452606 6 | Ollie Harridge - https://www.flickr.com/photos/olliethebastard/5439049117 7 | Ollie Harridge - https://www.flickr.com/photos/olliethebastard/5439051427 8 | ReflectedSerendipity - https://www.flickr.com/photos/sjdunphy/9294248537 9 | ReflectedSerendipity - https://www.flickr.com/photos/sjdunphy/9297024076 10 | http://www.pauldingcountyareafoundation.net/scottish-fold-cat-ddvudi5k0z5f.html -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Bufftail Ltd 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cofoundry.Samples.SPASite 2 | 3 | This sample shows how to use Cofoundry to create a SPA (Single Page Application) with WebApi endpoints as well as demonstrating some advanced Cofoundry features. The application is also separated into two projects demonstrating an example of how you might structure your domain layer to keep this separate from your web layer. 4 | 5 | This sample uses Vue.js as the SPA framework, but this is easily swapped out for another SPA framework as all interactions are made over a REST API. 6 | 7 | Notable features include: 8 | 9 | - Startup registration 10 | - Web Api, use of `IApiResponseHelper` and managing command validation errors in a SPA 11 | - Structuring commands and queries using CQS 12 | - Multiple related custom entities - Cats, Breeds and Features 13 | - A member area with a sign-up and login process 14 | - Using an Entity Framework DbContext to represent custom database tables 15 | - Executing stored procedures using `IEntityFrameworkSqlExecutor` 16 | - Integrating custom entity data with Entity Framework data access 17 | - Using the auto-updater to run SQL scripts 18 | - Email notifications & email templating 19 | - Registering services with the DI container 20 | 21 | #### To get started: 22 | 23 | 1. Create a database named 'Cofoundry.Samples.SPASite' and check the Cofoundry connection string in the app.settings file is correct for your SQL Server instance 24 | 2. Run the website and navigate to *"/admin"*, which will display the setup screen 25 | 3. Enter an application name and setup your user account. Submit the form to complete the site setup. 26 | 4. Either sign in and enter your own data or follow the steps below to import some test data 27 | 28 | #### Importing test data: 29 | 30 | To get you started we've put together some optional test data: 31 | 32 | 1. Run `InitData\Init.sql` script against your db to populate some initial cats and features 33 | 2. Copy the images from *"\InitData\Images"* to *"\src\Cofoundry.Samples.SPASite\App_Data\Files\Images"* 34 | 3. Either restart the site, or go to the *settings* module in the admin panel and clear the cache. 35 | 36 | ## App Overview 37 | 38 | *SPA Cats* is a simple site that lets you browse and rate cats. Cat data can be entered into the Cofoundry CMS admin panel, which is then displayed on the homepage. Users can browse the data, register as a member and vote for their favorite cat. 39 | 40 | ![SPA Cats Homepage](readme/Homepage.jpg) 41 | 42 | ### Managing Content 43 | 44 | To manage content you'll need to log in to the admin panel by navigating to **/admin**. 45 | 46 | #### Adding New Cats 47 | 48 | ![Domain solution structure](readme/AdminCatList.png) 49 | 50 | Once logged in, navigate to **Content > Cats**. This section is generated automatically based on the [`CatCustomEntityDefinition`](https://github.com/cofoundry-cms/Cofoundry.Samples.SPASite/blob/master/src/Cofoundry.Samples.SPASite.Domain/Domain/Cats/Definition/CatCustomEntityDefinition.cs) class in the Domain project. Click on the **Create** button to add a new Cat. 51 | 52 | ![Domain solution structure](readme/AdminCatCreate.png) 53 | 54 | The data entry form is generated based on the [`CatDataModel`](https://github.com/cofoundry-cms/Cofoundry.Samples.SPASite/blob/master/src/Cofoundry.Samples.SPASite.Domain/Domain/Cats/Definition/CatDataModel.cs), a simple class with annotated properties. The Cat custom entity type has versioning enabled, so we can either save the new cat as a draft version or make it live by publishing it. 55 | 56 | The two other custom entities *Breeds* and *Features* can be managed in the same way. 57 | 58 | ### Code 59 | 60 | In this example we demonstrate separating your application into two projects to represent two distinct layers. For a simpler example which keeps all files in one project see [Cofoundry.Samples.SimpleSite](https://github.com/cofoundry-cms/Cofoundry.Samples.SimpleSite). 61 | 62 | #### Cofoundry.Samples.SpaSite.Domain 63 | 64 | Contains domain logic and data access. 65 | 66 | ![Domain solution structure](readme/SpaCatsDomain.png) 67 | 68 | - **Data:** We use some custom SQL tables to store cat popularity data. An Entity Framework DbContext is used to access the custom tables. 69 | - **Domain:** The domain contains all the models, [queries and commands](https://github.com/cofoundry-cms/cofoundry/wiki/CQS) that we use to retrieve and store data. It also contains the [Custom Entity Definitions](https://github.com/cofoundry-cms/cofoundry/wiki/Custom-Entities) that define the *Breed*, *Cat* and *Features* custom entities, and the [User Area Definition](https://github.com/cofoundry-cms/cofoundry/wiki/User-Areas) that defines the *Members* login area. Structuring our code in this way gives us a clean separation between our domain logic layer and our application layer. 70 | - **Install:** Here we take advantage of the [Auto Update](https://github.com/cofoundry-cms/cofoundry/wiki/Auto-Update) feature in Cofoundry to run SQL scripts that create our custom tables and stored procedures. 71 | - **MailTemplates:** We store our [mail templates](https://github.com/cofoundry-cms/cofoundry/wiki/Mail) in the domain layer so they can be used from inside our commands. Because we are including the template cshtml files as embedded resources here, we need to include an `AssemblyResourceRegistration` which is located in the bootstrap folder. 72 | 73 | #### Cofoundry.Samples.SpaSite.Web 74 | 75 | Because all our logic is in the domain layer, our website project is fairly simple. 76 | 77 | ![Web solution structure](readme/SpaCatsWeb.png) 78 | 79 | - **Api:** Contains our web api controllers. These are quite lightweight and mostly just wrap our domain queries and commands. 80 | - **App_Data:** This folder contains any asset files uploaded to the CMS. 81 | - **ClientApp:** SPA Cats is written in [Vue.js](https://vuejs.org/), but you could use any framework you like. The ClientApp folder contains all the Vue.js front-end code and build files. 82 | - **Program.cs:** [Cofoundry startup and registration](https://github.com/cofoundry-cms/cofoundry/wiki/Website-Startup) is handled via the standard .NET Core Program.cs file in the application root. There's some additional code here to bootstrap the Vue project using the [VueCliMiddleware](https://github.com/EEParker/aspnetcore-vueclimiddleware) NuGet package. -------------------------------------------------------------------------------- /readme/AdminCatCreate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cofoundry-cms/Cofoundry.Samples.SPASite/ba0f8aaf565dc9b42a172ceac5b14f8aa06dd371/readme/AdminCatCreate.png -------------------------------------------------------------------------------- /readme/AdminCatList.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cofoundry-cms/Cofoundry.Samples.SPASite/ba0f8aaf565dc9b42a172ceac5b14f8aa06dd371/readme/AdminCatList.png -------------------------------------------------------------------------------- /readme/Homepage.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cofoundry-cms/Cofoundry.Samples.SPASite/ba0f8aaf565dc9b42a172ceac5b14f8aa06dd371/readme/Homepage.jpg -------------------------------------------------------------------------------- /readme/SpaCatsDomain.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cofoundry-cms/Cofoundry.Samples.SPASite/ba0f8aaf565dc9b42a172ceac5b14f8aa06dd371/readme/SpaCatsDomain.png -------------------------------------------------------------------------------- /readme/SpaCatsWeb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cofoundry-cms/Cofoundry.Samples.SPASite/ba0f8aaf565dc9b42a172ceac5b14f8aa06dd371/readme/SpaCatsWeb.png -------------------------------------------------------------------------------- /src/Cofoundry.Samples.SPASite.Domain/Cofoundry.Samples.SPASite.Domain.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/Cofoundry.Samples.SPASite.Domain/Data/Bootstrap/DataDependencyRegistration.cs: -------------------------------------------------------------------------------- 1 | using Cofoundry.Core.DependencyInjection; 2 | 3 | namespace Cofoundry.Samples.SPASite.Data; 4 | 5 | /// 6 | /// An IDependencyRegistration class allows us to automatically register 7 | /// services with the DI container in a modular way. 8 | /// 9 | /// See https://www.cofoundry.org/docs/framework/dependency-injection 10 | /// 11 | public class DataDependencyRegistration : IDependencyRegistration 12 | { 13 | public void Register(IContainerRegister container) 14 | { 15 | container.RegisterScoped(); 16 | } 17 | } -------------------------------------------------------------------------------- /src/Cofoundry.Samples.SPASite.Domain/Data/Cats/CatLike.cs: -------------------------------------------------------------------------------- 1 | namespace Cofoundry.Samples.SPASite.Data; 2 | 3 | public class CatLike 4 | { 5 | public int CatCustomEntityId { get; set; } 6 | 7 | public int UserId { get; set; } 8 | 9 | public DateTime CreateDate { get; set; } 10 | } -------------------------------------------------------------------------------- /src/Cofoundry.Samples.SPASite.Domain/Data/Cats/CatLikeCount.cs: -------------------------------------------------------------------------------- 1 | namespace Cofoundry.Samples.SPASite.Data; 2 | 3 | public class CatLikeCount 4 | { 5 | public int CatCustomEntityId { get; set; } 6 | 7 | public int TotalLikes { get; set; } 8 | } -------------------------------------------------------------------------------- /src/Cofoundry.Samples.SPASite.Domain/Data/Cats/CatLikeCountMap.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 2 | 3 | namespace Cofoundry.Samples.SPASite.Data; 4 | 5 | public class CatLikeCountMap : IEntityTypeConfiguration 6 | { 7 | public void Configure(EntityTypeBuilder builder) 8 | { 9 | builder.ToTable(nameof(CatLikeCount), DbConstants.DefaultAppSchema); 10 | 11 | builder.HasKey(e => e.CatCustomEntityId); 12 | } 13 | } -------------------------------------------------------------------------------- /src/Cofoundry.Samples.SPASite.Domain/Data/Cats/CatLikeMap.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 2 | 3 | namespace Cofoundry.Samples.SPASite.Data; 4 | 5 | public class CatLikeMap : IEntityTypeConfiguration 6 | { 7 | public void Configure(EntityTypeBuilder builder) 8 | { 9 | builder.ToTable(nameof(CatLike), DbConstants.DefaultAppSchema); 10 | 11 | builder.HasKey(e => new { e.CatCustomEntityId, e.UserId }); 12 | } 13 | } -------------------------------------------------------------------------------- /src/Cofoundry.Samples.SPASite.Domain/Data/SPASiteDbContext.cs: -------------------------------------------------------------------------------- 1 | using Cofoundry.Domain.Data; 2 | 3 | namespace Cofoundry.Samples.SPASite.Data; 4 | 5 | /// 6 | /// This is a code-first EF DbContext that uses a handful of Cofoundry helpers 7 | /// to make setting it up a bit easier. You can of course do data access any way you like. 8 | /// 9 | /// See https://www.cofoundry.org/docs/framework/entity-framework-and-dbcontext-tools 10 | /// 11 | public class SPASiteDbContext : DbContext 12 | { 13 | private readonly DatabaseSettings _databaseSettings; 14 | 15 | public SPASiteDbContext(DatabaseSettings databaseSettings) 16 | { 17 | _databaseSettings = databaseSettings; 18 | } 19 | 20 | protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) 21 | { 22 | optionsBuilder.UseSqlServer(_databaseSettings.ConnectionString); 23 | } 24 | 25 | /// 26 | /// We use the Cofoundry suggested schema config here which makes "app" the default schema. 27 | /// 28 | protected override void OnModelCreating(ModelBuilder modelBuilder) 29 | { 30 | modelBuilder 31 | .HasAppSchema() 32 | .ApplyConfiguration(new CatLikeMap()) 33 | .ApplyConfiguration(new CatLikeCountMap()); 34 | } 35 | 36 | public DbSet CatLikes { get; set; } 37 | public DbSet CatLikeCounts { get; set; } 38 | } 39 | -------------------------------------------------------------------------------- /src/Cofoundry.Samples.SPASite.Domain/Domain/Breeds/Definition/BreedCustomEntityDefinition.cs: -------------------------------------------------------------------------------- 1 | namespace Cofoundry.Samples.SPASite.Domain; 2 | 3 | /// 4 | /// Each custom entity requires a definition class which provides settings 5 | /// describing the entity and how it should behave. 6 | /// 7 | public class BreedCustomEntityDefinition : ICustomEntityDefinition 8 | { 9 | /// 10 | /// This constant is a convention that allows us to reference this definition code 11 | /// in other parts of the application (e.g. querying) 12 | /// 13 | public const string Code = "SPABRD"; 14 | 15 | /// 16 | /// Unique 6 letter code representing the module (the convention is to use uppercase) 17 | /// 18 | public string CustomEntityDefinitionCode => Code; 19 | 20 | /// 21 | /// Singlar name of the entity 22 | /// 23 | public string Name => "Breed"; 24 | 25 | /// 26 | /// Plural name of the entity 27 | /// 28 | public string NamePlural => "Breeds"; 29 | 30 | /// 31 | /// A short description that shows up as a tooltip for the admin 32 | /// panel. 33 | /// 34 | public string Description => "A breed of cat to categorize a cat"; 35 | 36 | /// 37 | /// Indicates whether the UrlSlug property should be treated 38 | /// as a unique property and be validated as such. 39 | /// 40 | public bool ForceUrlSlugUniqueness => true; 41 | 42 | /// 43 | /// Indicates whether the url slug should be autogenerated. If this 44 | /// is selected then the user will not be shown the UrlSlug property 45 | /// and it will be auto-generated based on the title. 46 | /// 47 | public bool AutoGenerateUrlSlug => true; 48 | 49 | /// 50 | /// Indicates whether this custom entity should always be published when 51 | /// saved, provided the user has permissions to do so. Useful if this isn't 52 | /// the sort of entity that needs a draft state workflow 53 | /// 54 | public bool AutoPublish => true; 55 | 56 | /// 57 | /// Indicates whether the entities are partitioned by locale 58 | /// 59 | public bool HasLocale => false; 60 | } -------------------------------------------------------------------------------- /src/Cofoundry.Samples.SPASite.Domain/Domain/Breeds/Definition/BreedDataModel.cs: -------------------------------------------------------------------------------- 1 | namespace Cofoundry.Samples.SPASite.Domain; 2 | 3 | /// 4 | /// Every custom entity needs a data model, even if it has no properties defined. 5 | /// 6 | public class BreedDataModel : ICustomEntityDataModel 7 | { 8 | } -------------------------------------------------------------------------------- /src/Cofoundry.Samples.SPASite.Domain/Domain/Breeds/Models/Breed.cs: -------------------------------------------------------------------------------- 1 | namespace Cofoundry.Samples.SPASite.Domain; 2 | 3 | public class Breed 4 | { 5 | public required int BreedId { get; set; } 6 | 7 | public required string Title { get; set; } 8 | } 9 | -------------------------------------------------------------------------------- /src/Cofoundry.Samples.SPASite.Domain/Domain/Breeds/Queries/GetAllBreedsQuery.cs: -------------------------------------------------------------------------------- 1 | namespace Cofoundry.Samples.SPASite.Domain; 2 | 3 | /// 4 | /// A query handler always requires a query definition, even if there are 5 | /// no parameters. 6 | /// 7 | public class GetAllBreedsQuery : IQuery> 8 | { 9 | } -------------------------------------------------------------------------------- /src/Cofoundry.Samples.SPASite.Domain/Domain/Breeds/Queries/GetAllBreedsQueryHandler.cs: -------------------------------------------------------------------------------- 1 | namespace Cofoundry.Samples.SPASite.Domain; 2 | 3 | /// 4 | /// This query uses IIgnorePermissionCheckHandler because the permissions 5 | /// are already checked by the underlying custom entity query. 6 | /// 7 | public class GetAllBreedsQueryHandler 8 | : IQueryHandler> 9 | , IIgnorePermissionCheckHandler 10 | { 11 | private readonly IContentRepository _contentRepository; 12 | 13 | public GetAllBreedsQueryHandler( 14 | IContentRepository contentRepository 15 | ) 16 | { 17 | _contentRepository = contentRepository; 18 | } 19 | 20 | public async Task> ExecuteAsync(GetAllBreedsQuery query, IExecutionContext executionContext) 21 | { 22 | var breeds = await _contentRepository 23 | .CustomEntities() 24 | .GetByDefinition() 25 | .AsRenderSummaries() 26 | .MapItem(MapBreed) 27 | .ExecuteAsync(); 28 | 29 | return breeds; 30 | } 31 | 32 | /// 33 | /// For simplicity this logic is just repeated between handlers, but to 34 | /// reduce repetition you could use a library like AutoMapper or break out 35 | /// the logic into a seperate mapper class and inject it in. 36 | /// 37 | private Breed MapBreed(CustomEntityRenderSummary customEntity) 38 | { 39 | var breed = new Breed 40 | { 41 | BreedId = customEntity.CustomEntityId, 42 | Title = customEntity.Title 43 | }; 44 | 45 | return breed; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Cofoundry.Samples.SPASite.Domain/Domain/Breeds/Queries/GetBreedByIdQuery.cs: -------------------------------------------------------------------------------- 1 | namespace Cofoundry.Samples.SPASite.Domain; 2 | 3 | public class GetBreedByIdQuery : IQuery 4 | { 5 | public GetBreedByIdQuery() { } 6 | 7 | public GetBreedByIdQuery(int id) 8 | { 9 | BreedId = id; 10 | } 11 | 12 | public int BreedId { get; set; } 13 | } 14 | -------------------------------------------------------------------------------- /src/Cofoundry.Samples.SPASite.Domain/Domain/Breeds/Queries/GetBreedByIdQueryHandler.cs: -------------------------------------------------------------------------------- 1 | namespace Cofoundry.Samples.SPASite.Domain; 2 | 3 | public class GetBreedByIdQueryHandler 4 | : IQueryHandler 5 | , IIgnorePermissionCheckHandler 6 | { 7 | private readonly IContentRepository _contentRepository; 8 | 9 | public GetBreedByIdQueryHandler( 10 | IContentRepository contentRepository 11 | ) 12 | { 13 | _contentRepository = contentRepository; 14 | } 15 | 16 | public async Task ExecuteAsync(GetBreedByIdQuery query, IExecutionContext executionContext) 17 | { 18 | var breed = await _contentRepository 19 | .CustomEntities() 20 | .GetById(query.BreedId) 21 | .AsRenderSummary() 22 | .MapWhenNotNull(MapBreed) 23 | .ExecuteAsync(); 24 | 25 | return breed; 26 | } 27 | 28 | /// 29 | /// For simplicity this logic is just repeated between handlers, but to 30 | /// reduce repetition you could use a library like AutoMapper or break out 31 | /// the logic into a seperate mapper class and inject it in. 32 | /// 33 | private Breed? MapBreed(CustomEntityRenderSummary customEntity) 34 | { 35 | var breed = new Breed 36 | { 37 | BreedId = customEntity.CustomEntityId, 38 | Title = customEntity.Title 39 | }; 40 | 41 | return breed; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Cofoundry.Samples.SPASite.Domain/Domain/Cats/Commands/SetCatLikedCommand.cs: -------------------------------------------------------------------------------- 1 | using Cofoundry.Core.Validation; 2 | using System.ComponentModel.DataAnnotations; 3 | 4 | namespace Cofoundry.Samples.SPASite.Domain; 5 | 6 | /// 7 | /// Here we can use ILoggableCommand to mark this class as 8 | /// loggable. The default logger only logs to the debugger, 9 | /// but you can use a plugin to extend this functionality 10 | /// 11 | public class SetCatLikedCommand : ICommand, ILoggableCommand 12 | { 13 | [PositiveInteger] 14 | [Required] 15 | public int CatId { get; set; } 16 | 17 | public bool IsLiked { get; set; } 18 | } -------------------------------------------------------------------------------- /src/Cofoundry.Samples.SPASite.Domain/Domain/Cats/Commands/SetCatLikedCommandHandler.cs: -------------------------------------------------------------------------------- 1 | using Cofoundry.Core.EntityFramework; 2 | using Cofoundry.Samples.SPASite.Data; 3 | using Microsoft.Data.SqlClient; 4 | 5 | namespace Cofoundry.Samples.SPASite.Domain; 6 | 7 | /// 8 | /// This handler uses 9 | /// to make sure a user signed in before allowing them to set the cat 10 | /// as liked. We could use 11 | /// to be more specific here and create a specific permission for the action, 12 | /// but that isn't neccessary here because any signed in user 13 | /// can perform this action. 14 | /// 15 | /// For more on permissions see https://www.cofoundry.org/docs/framework/roles-and-permissions 16 | /// 17 | public class SetCatLikedCommandHandler 18 | : ICommandHandler 19 | , ISignedInPermissionCheckHandler 20 | { 21 | private readonly IEntityFrameworkSqlExecutor _entityFrameworkSqlExecutor; 22 | private readonly SPASiteDbContext _spaSiteDbContext; 23 | 24 | public SetCatLikedCommandHandler( 25 | IEntityFrameworkSqlExecutor entityFrameworkSqlExecutor, 26 | SPASiteDbContext spaSiteDbContext 27 | ) 28 | { 29 | _entityFrameworkSqlExecutor = entityFrameworkSqlExecutor; 30 | _spaSiteDbContext = spaSiteDbContext; 31 | } 32 | 33 | public Task ExecuteAsync(SetCatLikedCommand command, IExecutionContext executionContext) 34 | { 35 | // We could use the EF DbContext here, but it's faster to make this change using a 36 | // stored procedure. We use IEntityFrameworkSqlExecutor here to simplify this, but 37 | // you could also use EF directly, Dapper or mix in any other data access approach. 38 | // For more info see https://www.cofoundry.org/docs/framework/entity-framework-and-dbcontext-tools#executing-stored-procedures--raw-sql 39 | 40 | return _entityFrameworkSqlExecutor 41 | .ExecuteCommandAsync(_spaSiteDbContext, 42 | "app.CatLike_SetLiked", 43 | new SqlParameter("@CatId", command.CatId), 44 | new SqlParameter("@UserId", executionContext.UserContext.UserId), 45 | new SqlParameter("@IsLiked", command.IsLiked), 46 | new SqlParameter("@CreateDate", executionContext.ExecutionDate) 47 | ); 48 | } 49 | } 50 | 51 | -------------------------------------------------------------------------------- /src/Cofoundry.Samples.SPASite.Domain/Domain/Cats/Definition/CatCustomEntityDefinition.cs: -------------------------------------------------------------------------------- 1 | namespace Cofoundry.Samples.SPASite.Domain; 2 | 3 | /// 4 | /// Each custom entity requires a definition class which provides settings 5 | /// describing the entity and how it should behave. 6 | /// 7 | /// This definition uses ICustomisedTermCustomEntityDefinition to change 8 | /// the display name for the title property to be 'Name'. 9 | /// 10 | public class CatCustomEntityDefinition 11 | : ICustomEntityDefinition 12 | , ICustomizedTermCustomEntityDefinition 13 | { 14 | /// 15 | /// This constant is a convention that allows us to reference this definition code 16 | /// in other parts of the application (e.g. querying) 17 | /// 18 | public const string Code = "SPACAT"; 19 | 20 | /// 21 | /// Unique 6 letter code representing the module (the convention is to use uppercase) 22 | /// 23 | public string CustomEntityDefinitionCode => Code; 24 | 25 | /// 26 | /// Singlar name of the entity 27 | /// 28 | public string Name => "Cat"; 29 | 30 | /// 31 | /// Plural name of the entity 32 | /// 33 | public string NamePlural => "Cats"; 34 | 35 | /// 36 | /// A short description that shows up as a tooltip for the admin 37 | /// panel. 38 | /// 39 | public string Description => "Each cat can be rated by the public."; 40 | 41 | /// 42 | /// Indicates whether the UrlSlug property should be treated 43 | /// as a unique property and be validated as such. 44 | /// 45 | public bool ForceUrlSlugUniqueness => false; 46 | 47 | /// 48 | /// Indicates whether the url slug should be autogenerated. If this 49 | /// is selected then the user will not be shown the UrlSlug property 50 | /// and it will be auto-generated based on the title. 51 | /// 52 | public bool AutoGenerateUrlSlug => true; 53 | 54 | /// 55 | /// Indicates whether this custom entity should always be published when 56 | /// saved, provided the user has permissions to do so. Useful if this isn't 57 | /// the sort of entity that needs a draft state workflow 58 | /// 59 | public bool AutoPublish => false; 60 | 61 | /// 62 | /// Indicates whether the entities are partitioned by locale 63 | /// 64 | public bool HasLocale => false; 65 | 66 | /// 67 | /// Implementing ICustomisedTermCustomEntityDefinition gives us additional customization 68 | /// settings, allowing us to give custom labels for properties such as Title and UrlSlug. 69 | /// Here we want the in-built Title property to be used as the cat name. 70 | /// 71 | public Dictionary CustomTerms => new() 72 | { 73 | { CustomizableCustomEntityTermKeys.Title, "Name" } 74 | }; 75 | } 76 | -------------------------------------------------------------------------------- /src/Cofoundry.Samples.SPASite.Domain/Domain/Cats/Definition/CatDataModel.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace Cofoundry.Samples.SPASite.Domain; 4 | 5 | /// 6 | /// Data model classes can use data annotations to describe the data 7 | /// and provide hints to the admin UI as to how the property should be 8 | /// displayed. 9 | /// 10 | /// In this data model we link out to images and other custom entities. 11 | /// Although the data model serialized as json in the database, the 12 | /// relationships are stored separately which allows us to provide a certain 13 | /// amount of data integrity. 14 | /// 15 | public class CatDataModel : ICustomEntityDataModel 16 | { 17 | [Display(Description = "A short description or tag-line to describe the cat")] 18 | public string? Description { get; set; } 19 | 20 | [Display(Name = "Breed", Description = "Identity the breed of cat if possible")] 21 | [CustomEntity(BreedCustomEntityDefinition.Code)] 22 | public int? BreedId { get; set; } 23 | 24 | [Display(Name = "Features", Description = "Extra features or properties that help categorize this cat")] 25 | [CustomEntityCollection(FeatureCustomEntityDefinition.Code)] 26 | public int[] FeatureIds { get; set; } = []; 27 | 28 | [Display(Name = "Images", Description = "The top image will be the main image that displays in the grid")] 29 | [ImageCollection] 30 | public int[] ImageAssetIds { get; set; } = []; 31 | } 32 | -------------------------------------------------------------------------------- /src/Cofoundry.Samples.SPASite.Domain/Domain/Cats/Models/CatDetails.cs: -------------------------------------------------------------------------------- 1 | namespace Cofoundry.Samples.SPASite.Domain; 2 | 3 | /// 4 | /// The difference between the CatDetails and CatSummary model 5 | /// is small, but it illustrates how the CQS lets us tailor 6 | /// models returned from queries to fit different situations. 7 | /// 8 | public class CatDetails 9 | { 10 | public required int CatId { get; set; } 11 | 12 | public required string Name { get; set; } 13 | 14 | public required string? Description { get; set; } 15 | 16 | public required int TotalLikes { get; set; } 17 | 18 | public required Breed? Breed { get; set; } 19 | 20 | public required IReadOnlyCollection Features { get; set; } 21 | 22 | public required IReadOnlyCollection Images { get; set; } 23 | } 24 | -------------------------------------------------------------------------------- /src/Cofoundry.Samples.SPASite.Domain/Domain/Cats/Models/CatSummary.cs: -------------------------------------------------------------------------------- 1 | namespace Cofoundry.Samples.SPASite.Domain; 2 | 3 | /// 4 | /// The difference between the CatDetails and CatSummary model 5 | /// is small, but it illustrates how the CQS lets us tailor 6 | /// models returned from queries to fit different situations. 7 | /// 8 | public class CatSummary 9 | { 10 | public required int CatId { get; set; } 11 | 12 | public required string Name { get; set; } 13 | 14 | public required string? Description { get; set; } 15 | 16 | public required int TotalLikes { get; set; } 17 | 18 | public ImageAssetRenderDetails? MainImage { get; set; } 19 | } 20 | -------------------------------------------------------------------------------- /src/Cofoundry.Samples.SPASite.Domain/Domain/Cats/Queries/GetCatDetailsByIdQuery.cs: -------------------------------------------------------------------------------- 1 | namespace Cofoundry.Samples.SPASite.Domain; 2 | 3 | public class GetCatDetailsByIdQuery : IQuery 4 | { 5 | public GetCatDetailsByIdQuery() { } 6 | 7 | public GetCatDetailsByIdQuery(int id) 8 | { 9 | CatId = id; 10 | } 11 | 12 | public int CatId { get; set; } 13 | } 14 | -------------------------------------------------------------------------------- /src/Cofoundry.Samples.SPASite.Domain/Domain/Cats/Queries/GetCatDetailsByIdQueryHandler.cs: -------------------------------------------------------------------------------- 1 | using Cofoundry.Samples.SPASite.Data; 2 | 3 | namespace Cofoundry.Samples.SPASite.Domain; 4 | 5 | public class GetCatDetailsByIdQueryHandler 6 | : IQueryHandler 7 | , IIgnorePermissionCheckHandler 8 | { 9 | private readonly IContentRepository _contentRepository; 10 | private readonly SPASiteDbContext _dbContext; 11 | 12 | public GetCatDetailsByIdQueryHandler( 13 | IContentRepository contentRepository, 14 | SPASiteDbContext dbContext 15 | ) 16 | { 17 | _contentRepository = contentRepository; 18 | _dbContext = dbContext; 19 | } 20 | 21 | public async Task ExecuteAsync(GetCatDetailsByIdQuery query, IExecutionContext executionContext) 22 | { 23 | var cat = await _contentRepository 24 | .CustomEntities() 25 | .GetById(query.CatId) 26 | .AsRenderSummary() 27 | .MapWhenNotNull(MapCatAsync) 28 | .ExecuteAsync(); 29 | 30 | return cat; 31 | } 32 | 33 | private async Task MapCatAsync(CustomEntityRenderSummary customEntity) 34 | { 35 | if (customEntity.Model is not CatDataModel model) 36 | { 37 | return null; 38 | } 39 | 40 | var cat = new CatDetails 41 | { 42 | CatId = customEntity.CustomEntityId, 43 | Name = customEntity.Title, 44 | Description = model.Description, 45 | Breed = await GetBreedAsync(model.BreedId), 46 | Features = await GetFeaturesAsync(model.FeatureIds), 47 | Images = await GetImagesAsync(model.ImageAssetIds), 48 | TotalLikes = await GetLikeCount(customEntity.CustomEntityId) 49 | }; 50 | 51 | return cat; 52 | } 53 | 54 | private Task GetLikeCount(int catId) 55 | { 56 | return _dbContext 57 | .CatLikeCounts 58 | .AsNoTracking() 59 | .Where(c => c.CatCustomEntityId == catId) 60 | .Select(c => c.TotalLikes) 61 | .FirstOrDefaultAsync(); 62 | } 63 | 64 | private async Task GetBreedAsync(int? breedId) 65 | { 66 | if (!breedId.HasValue) 67 | { 68 | return null; 69 | } 70 | 71 | var query = new GetBreedByIdQuery(breedId.Value); 72 | 73 | return await _contentRepository.ExecuteQueryAsync(query); 74 | } 75 | 76 | private async Task> GetFeaturesAsync(IReadOnlyCollection featureIds) 77 | { 78 | if (EnumerableHelper.IsNullOrEmpty(featureIds)) 79 | { 80 | return Array.Empty(); 81 | } 82 | 83 | var query = new GetFeaturesByIdRangeQuery(featureIds); 84 | 85 | var features = await _contentRepository.ExecuteQueryAsync(query); 86 | 87 | return features 88 | .Select(f => f.Value) 89 | .OrderBy(f => f.Title) 90 | .ToArray(); 91 | } 92 | 93 | private async Task> GetImagesAsync(IReadOnlyCollection imageAssetIds) 94 | { 95 | if (EnumerableHelper.IsNullOrEmpty(imageAssetIds)) 96 | { 97 | return Array.Empty(); 98 | } 99 | 100 | var images = await _contentRepository 101 | .ImageAssets() 102 | .GetByIdRange(imageAssetIds) 103 | .AsRenderDetails() 104 | .FilterAndOrderByKeys(imageAssetIds) 105 | .ExecuteAsync(); 106 | 107 | return images; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/Cofoundry.Samples.SPASite.Domain/Domain/Cats/Queries/GetCatSummariesByMemberLikedQuery.cs: -------------------------------------------------------------------------------- 1 | namespace Cofoundry.Samples.SPASite.Domain; 2 | 3 | public class GetCatSummariesByMemberLikedQuery : IQuery> 4 | { 5 | public GetCatSummariesByMemberLikedQuery() { } 6 | 7 | public GetCatSummariesByMemberLikedQuery(int id) 8 | { 9 | UserId = id; 10 | } 11 | 12 | public int UserId { get; set; } 13 | } 14 | -------------------------------------------------------------------------------- /src/Cofoundry.Samples.SPASite.Domain/Domain/Cats/Queries/GetCatSummariesByMemberLikedQueryHandler.cs: -------------------------------------------------------------------------------- 1 | using Cofoundry.Samples.SPASite.Data; 2 | 3 | namespace Cofoundry.Samples.SPASite.Domain; 4 | 5 | public class GetCatSummariesByMemberLikedQueryHandler 6 | : IQueryHandler> 7 | , ISignedInPermissionCheckHandler 8 | { 9 | private readonly IContentRepository _contentRepository; 10 | private readonly SPASiteDbContext _dbContext; 11 | 12 | public GetCatSummariesByMemberLikedQueryHandler( 13 | IContentRepository contentRepository, 14 | SPASiteDbContext dbContext 15 | ) 16 | { 17 | _contentRepository = contentRepository; 18 | _dbContext = dbContext; 19 | } 20 | 21 | public async Task> ExecuteAsync(GetCatSummariesByMemberLikedQuery query, IExecutionContext executionContext) 22 | { 23 | var userCatIds = await _dbContext 24 | .CatLikes 25 | .AsNoTracking() 26 | .Where(c => c.UserId == query.UserId) 27 | .OrderByDescending(c => c.CreateDate) 28 | .Select(c => c.CatCustomEntityId) 29 | .ToListAsync(); 30 | 31 | // GetByIdRange queries return a dictionary to make lookups easier, so we 32 | // have an extra step to do if we want to set the ordering to match the 33 | // original id collection. 34 | var catCustomEntities = await _contentRepository 35 | .CustomEntities() 36 | .GetByIdRange(userCatIds) 37 | .AsRenderSummaries() 38 | .FilterAndOrderByKeys(userCatIds) 39 | .ExecuteAsync(); 40 | 41 | var allMainImages = await GetMainImages(catCustomEntities); 42 | var allLikeCounts = await GetLikeCounts(catCustomEntities); 43 | 44 | return MapCats(catCustomEntities, allMainImages, allLikeCounts); 45 | } 46 | 47 | private Task> GetMainImages(IReadOnlyCollection customEntities) 48 | { 49 | var imageAssetIds = customEntities 50 | .Select(i => (CatDataModel)i.Model) 51 | .Where(m => !EnumerableHelper.IsNullOrEmpty(m.ImageAssetIds)) 52 | .Select(m => m.ImageAssetIds.First()) 53 | .Distinct(); 54 | 55 | return _contentRepository 56 | .ImageAssets() 57 | .GetByIdRange(imageAssetIds) 58 | .AsRenderDetails() 59 | .ExecuteAsync(); 60 | } 61 | 62 | private Task> GetLikeCounts(IReadOnlyCollection customEntities) 63 | { 64 | var catIds = customEntities 65 | .Select(i => i.CustomEntityId) 66 | .Distinct() 67 | .ToArray(); 68 | 69 | return _dbContext 70 | .CatLikeCounts 71 | .AsNoTracking() 72 | .Where(c => catIds.Contains(c.CatCustomEntityId)) 73 | .ToDictionaryAsync(c => c.CatCustomEntityId, c => c.TotalLikes); 74 | } 75 | 76 | private static List MapCats( 77 | IReadOnlyCollection customEntities, 78 | IReadOnlyDictionary images, 79 | IReadOnlyDictionary allLikeCounts 80 | ) 81 | { 82 | var cats = new List(customEntities.Count); 83 | 84 | foreach (var customEntity in customEntities) 85 | { 86 | var model = (CatDataModel)customEntity.Model; 87 | 88 | var cat = new CatSummary 89 | { 90 | CatId = customEntity.CustomEntityId, 91 | Name = customEntity.Title, 92 | Description = model.Description, 93 | TotalLikes = allLikeCounts.GetValueOrDefault(customEntity.CustomEntityId) 94 | }; 95 | 96 | if (!EnumerableHelper.IsNullOrEmpty(model.ImageAssetIds)) 97 | { 98 | cat.MainImage = images.GetValueOrDefault(model.ImageAssetIds.FirstOrDefault()); 99 | } 100 | 101 | cats.Add(cat); 102 | } 103 | 104 | return cats; 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/Cofoundry.Samples.SPASite.Domain/Domain/Cats/Queries/SearchCatSummariesQuery.cs: -------------------------------------------------------------------------------- 1 | namespace Cofoundry.Samples.SPASite.Domain; 2 | 3 | public class SearchCatSummariesQuery 4 | : SimplePageableQuery 5 | , IQuery> 6 | { 7 | } 8 | -------------------------------------------------------------------------------- /src/Cofoundry.Samples.SPASite.Domain/Domain/Cats/Queries/SearchCatSummariesQueryHandler.cs: -------------------------------------------------------------------------------- 1 | using Cofoundry.Samples.SPASite.Data; 2 | 3 | namespace Cofoundry.Samples.SPASite.Domain; 4 | 5 | public class SearchCatSummariesQueryHandler 6 | : IQueryHandler> 7 | , IIgnorePermissionCheckHandler 8 | { 9 | private readonly SPASiteDbContext _dbContext; 10 | private readonly IContentRepository _contentRepository; 11 | 12 | public SearchCatSummariesQueryHandler( 13 | SPASiteDbContext dbContext, 14 | IContentRepository contentRepository 15 | ) 16 | { 17 | _dbContext = dbContext; 18 | _contentRepository = contentRepository; 19 | } 20 | 21 | public async Task> ExecuteAsync(SearchCatSummariesQuery query, IExecutionContext executionContext) 22 | { 23 | var customEntityQuery = new SearchCustomEntityRenderSummariesQuery 24 | { 25 | CustomEntityDefinitionCode = CatCustomEntityDefinition.Code, 26 | PageSize = query.PageSize, 27 | PageNumber = query.PageNumber, 28 | PublishStatus = PublishStatusQuery.Published, 29 | SortBy = CustomEntityQuerySortType.PublishDate 30 | }; 31 | 32 | var catCustomEntities = await _contentRepository 33 | .CustomEntities() 34 | .Search() 35 | .AsRenderSummaries(customEntityQuery) 36 | .ExecuteAsync(); 37 | 38 | var allMainImages = await GetMainImages(catCustomEntities); 39 | var allLikeCounts = await GetLikeCounts(catCustomEntities); 40 | 41 | return MapCats(catCustomEntities, allMainImages, allLikeCounts); 42 | } 43 | 44 | private Task> GetMainImages(PagedQueryResult customEntityResult) 45 | { 46 | var imageAssetIds = customEntityResult 47 | .Items 48 | .Select(i => (CatDataModel)i.Model) 49 | .Where(m => !EnumerableHelper.IsNullOrEmpty(m.ImageAssetIds)) 50 | .Select(m => m.ImageAssetIds.First()) 51 | .Distinct(); 52 | 53 | return _contentRepository 54 | .ImageAssets() 55 | .GetByIdRange(imageAssetIds) 56 | .AsRenderDetails() 57 | .ExecuteAsync(); 58 | } 59 | 60 | private Task> GetLikeCounts(PagedQueryResult customEntityResult) 61 | { 62 | var catIds = customEntityResult 63 | .Items 64 | .Select(i => i.CustomEntityId) 65 | .Distinct() 66 | .ToList(); 67 | 68 | return _dbContext 69 | .CatLikeCounts 70 | .AsNoTracking() 71 | .Where(c => catIds.Contains(c.CatCustomEntityId)) 72 | .ToDictionaryAsync(c => c.CatCustomEntityId, c => c.TotalLikes); 73 | } 74 | 75 | private static PagedQueryResult MapCats( 76 | PagedQueryResult customEntityResult, 77 | IReadOnlyDictionary images, 78 | IReadOnlyDictionary allLikeCounts 79 | ) 80 | { 81 | var cats = new List(customEntityResult.Items.Count); 82 | 83 | foreach (var customEntity in customEntityResult.Items) 84 | { 85 | var model = (CatDataModel)customEntity.Model; 86 | 87 | var cat = new CatSummary 88 | { 89 | CatId = customEntity.CustomEntityId, 90 | Name = customEntity.Title, 91 | Description = model.Description, 92 | TotalLikes = allLikeCounts.GetValueOrDefault(customEntity.CustomEntityId) 93 | }; 94 | 95 | if (!EnumerableHelper.IsNullOrEmpty(model.ImageAssetIds)) 96 | { 97 | cat.MainImage = images.GetValueOrDefault(model.ImageAssetIds.FirstOrDefault()); 98 | } 99 | 100 | cats.Add(cat); 101 | } 102 | 103 | return customEntityResult.ChangeType(cats); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/Cofoundry.Samples.SPASite.Domain/Domain/Features/Definition/FeatureCustomEntityDefinition.cs: -------------------------------------------------------------------------------- 1 | namespace Cofoundry.Samples.SPASite.Domain; 2 | 3 | /// 4 | /// Each custom entity requires a definition class which provides settings 5 | /// describing the entity and how it should behave. 6 | /// 7 | public class FeatureCustomEntityDefinition : ICustomEntityDefinition 8 | { 9 | /// 10 | /// This constant is a convention that allows us to reference this definition code 11 | /// in other parts of the application (e.g. querying) 12 | /// 13 | public const string Code = "SPAFET"; 14 | 15 | /// 16 | /// Unique 6 letter code representing the module (the convention is to use uppercase) 17 | /// 18 | public string CustomEntityDefinitionCode => Code; 19 | 20 | /// 21 | /// Singlar name of the entity 22 | /// 23 | public string Name => "Feature"; 24 | 25 | /// 26 | /// Plural name of the entity 27 | /// 28 | public string NamePlural => "Features"; 29 | 30 | /// 31 | /// A short description that shows up as a tooltip for the admin 32 | /// panel. 33 | /// 34 | public string Description => "Physical features or properties that describe a cat"; 35 | 36 | /// 37 | /// Indicates whether the UrlSlug property should be treated 38 | /// as a unique property and be validated as such. 39 | /// 40 | public bool ForceUrlSlugUniqueness => true; 41 | 42 | /// 43 | /// Indicates whether the url slug should be autogenerated. If this 44 | /// is selected then the user will not be shown the UrlSlug property 45 | /// and it will be auto-generated based on the title. 46 | /// 47 | public bool AutoGenerateUrlSlug => true; 48 | 49 | /// 50 | /// Indicates whether this custom entity should always be published when 51 | /// saved, provided the user has permissions to do so. Useful if this isn't 52 | /// the sort of entity that needs a draft state workflow 53 | /// 54 | public bool AutoPublish => true; 55 | 56 | /// 57 | /// Indicates whether the entities are partitioned by locale 58 | /// 59 | public bool HasLocale => false; 60 | } -------------------------------------------------------------------------------- /src/Cofoundry.Samples.SPASite.Domain/Domain/Features/Definition/FeatureDataModel.cs: -------------------------------------------------------------------------------- 1 | namespace Cofoundry.Samples.SPASite.Domain; 2 | 3 | public class FeatureDataModel : ICustomEntityDataModel 4 | { 5 | } -------------------------------------------------------------------------------- /src/Cofoundry.Samples.SPASite.Domain/Domain/Features/Model/Feature.cs: -------------------------------------------------------------------------------- 1 | namespace Cofoundry.Samples.SPASite.Domain; 2 | 3 | public class Feature 4 | { 5 | public required int FeatureId { get; set; } 6 | 7 | public required string Title { get; set; } 8 | } 9 | -------------------------------------------------------------------------------- /src/Cofoundry.Samples.SPASite.Domain/Domain/Features/Queries/GetAllFeaturesQuery.cs: -------------------------------------------------------------------------------- 1 | namespace Cofoundry.Samples.SPASite.Domain; 2 | 3 | public class GetAllFeaturesQuery : IQuery> 4 | { 5 | } 6 | -------------------------------------------------------------------------------- /src/Cofoundry.Samples.SPASite.Domain/Domain/Features/Queries/GetAllFeaturesQueryHandler.cs: -------------------------------------------------------------------------------- 1 | namespace Cofoundry.Samples.SPASite.Domain; 2 | 3 | public class GetAllFeaturesQueryHandler 4 | : IQueryHandler> 5 | , IIgnorePermissionCheckHandler 6 | { 7 | private readonly IContentRepository _contentRepository; 8 | 9 | public GetAllFeaturesQueryHandler( 10 | IContentRepository contentRepository 11 | ) 12 | { 13 | _contentRepository = contentRepository; 14 | } 15 | 16 | public async Task> ExecuteAsync(GetAllFeaturesQuery query, IExecutionContext executionContext) 17 | { 18 | var features = await _contentRepository 19 | .CustomEntities() 20 | .GetByDefinition() 21 | .AsRenderSummaries() 22 | .MapItem(MapFeature) 23 | .ExecuteAsync(); 24 | 25 | return features; 26 | } 27 | 28 | private Feature MapFeature(CustomEntityRenderSummary customEntity) 29 | { 30 | var feature = new Feature 31 | { 32 | FeatureId = customEntity.CustomEntityId, 33 | Title = customEntity.Title 34 | }; 35 | 36 | return feature; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Cofoundry.Samples.SPASite.Domain/Domain/Features/Queries/GetFeaturesByIdRangeQuery.cs: -------------------------------------------------------------------------------- 1 | namespace Cofoundry.Samples.SPASite.Domain; 2 | 3 | public class GetFeaturesByIdRangeQuery : IQuery> 4 | { 5 | public GetFeaturesByIdRangeQuery() 6 | { 7 | FeatureIds = new List(); 8 | } 9 | 10 | public GetFeaturesByIdRangeQuery(IReadOnlyCollection ids) 11 | { 12 | FeatureIds = ids; 13 | } 14 | 15 | public IReadOnlyCollection FeatureIds { get; set; } 16 | } 17 | -------------------------------------------------------------------------------- /src/Cofoundry.Samples.SPASite.Domain/Domain/Features/Queries/GetFeaturesByIdRangeQueryHandler.cs: -------------------------------------------------------------------------------- 1 | namespace Cofoundry.Samples.SPASite.Domain; 2 | 3 | public class GetFeaturesByIdRangeQueryHandler 4 | : IQueryHandler> 5 | , IIgnorePermissionCheckHandler 6 | { 7 | private readonly IContentRepository _contentRepository; 8 | 9 | public GetFeaturesByIdRangeQueryHandler( 10 | IContentRepository contentRepository 11 | ) 12 | { 13 | _contentRepository = contentRepository; 14 | } 15 | 16 | public async Task> ExecuteAsync(GetFeaturesByIdRangeQuery query, IExecutionContext executionContext) 17 | { 18 | var features = await _contentRepository 19 | .CustomEntities() 20 | .GetByIdRange(query.FeatureIds) 21 | .AsRenderSummaries() 22 | .MapItem(MapFeature) 23 | .ExecuteAsync(); 24 | 25 | return features; 26 | } 27 | 28 | private Feature MapFeature(CustomEntityRenderSummary customEntity) 29 | { 30 | var feature = new Feature 31 | { 32 | FeatureId = customEntity.CustomEntityId, 33 | Title = customEntity.Title 34 | }; 35 | 36 | return feature; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Cofoundry.Samples.SPASite.Domain/Domain/Members/Commands/RegisterMemberAndLogInCommand.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | using Newtonsoft.Json; 3 | 4 | namespace Cofoundry.Samples.SPASite; 5 | 6 | public class RegisterMemberAndLogInCommand : ICommand 7 | { 8 | [Required] 9 | [StringLength(150)] 10 | [EmailAddress(ErrorMessage = "Please use a valid email address")] 11 | [DataType(DataType.EmailAddress)] 12 | public string Email { get; set; } = string.Empty; 13 | 14 | [Required] 15 | [StringLength(50)] 16 | public string DisplayName { get; set; } = string.Empty; 17 | 18 | [Required] 19 | [StringLength(300, MinimumLength = 8)] 20 | [DataType(DataType.Password)] 21 | public string Password { get; set; } = string.Empty; 22 | 23 | [OutputValue] 24 | public int OutputMemberId { get; set; } 25 | } 26 | -------------------------------------------------------------------------------- /src/Cofoundry.Samples.SPASite.Domain/Domain/Members/Commands/RegisterMemberAndLogInCommandHandler.cs: -------------------------------------------------------------------------------- 1 | using Cofoundry.Core.Mail; 2 | 3 | namespace Cofoundry.Samples.SPASite.Domain; 4 | 5 | /// 6 | /// This command handler ties together a number of functions built 7 | /// into Cofoundry such as saving a new user, sending a notification 8 | /// and logging them in. 9 | /// 10 | public class RegisterMemberAndLogInCommandHandler 11 | : ICommandHandler 12 | , IIgnorePermissionCheckHandler 13 | { 14 | private readonly IAdvancedContentRepository _contentRepository; 15 | private readonly IMailService _mailService; 16 | 17 | public RegisterMemberAndLogInCommandHandler( 18 | IAdvancedContentRepository contentRepository, 19 | IMailService mailService 20 | ) 21 | { 22 | _contentRepository = contentRepository; 23 | _mailService = mailService; 24 | } 25 | 26 | public async Task ExecuteAsync(RegisterMemberAndLogInCommand command, IExecutionContext executionContext) 27 | { 28 | var addUserCommand = MapAddUserCommand(command); 29 | 30 | // Because the user is not signed in, we'll need to elevate 31 | // permissions to be able add a new user account. 32 | var userId = await _contentRepository 33 | .WithElevatedPermissions() 34 | .Users() 35 | .AddAsync(addUserCommand); 36 | 37 | await SendWelcomeNotification(command); 38 | 39 | await _contentRepository 40 | .Users() 41 | .Authentication() 42 | .SignInAuthenticatedUserAsync(new SignInAuthenticatedUserCommand() 43 | { 44 | UserId = userId, 45 | RememberUser = true 46 | }); 47 | } 48 | 49 | /// 50 | /// We're going to make use of the built in AddUserCommand which will take 51 | /// care of most of the logic for us. Here we map from our domain command to 52 | /// the Cofoundry one. 53 | /// 54 | /// It's important that we don't expose the AddUserCommand directly in our 55 | /// web api, which could allow a 'parameter injection attack' to take place: 56 | /// 57 | /// See https://www.owasp.org/index.php/Web_Parameter_Tampering 58 | /// 59 | private static AddUserCommand MapAddUserCommand(RegisterMemberAndLogInCommand command) 60 | { 61 | var addUserCommand = new AddUserCommand() 62 | { 63 | Email = command.Email, 64 | DisplayName = command.DisplayName, 65 | Password = command.Password, 66 | RoleCode = MemberRole.Code, 67 | UserAreaCode = MemberUserArea.Code, 68 | }; 69 | 70 | return addUserCommand; 71 | } 72 | 73 | /// 74 | /// For more info on sending mail with Cofoundry see https://www.cofoundry.org/docs/framework/mail 75 | /// 76 | private async Task SendWelcomeNotification(RegisterMemberAndLogInCommand command) 77 | { 78 | var welcomeEmailTemplate = new NewUserWelcomeMailTemplate 79 | { 80 | Name = command.DisplayName 81 | }; 82 | await _mailService.SendAsync(command.Email, welcomeEmailTemplate); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/Cofoundry.Samples.SPASite.Domain/Domain/Members/Commands/SignMemberInCommand.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | using Newtonsoft.Json; 3 | 4 | namespace Cofoundry.Samples.SPASite; 5 | 6 | public class SignMemberInCommand : ICommand 7 | { 8 | [Required] 9 | [EmailAddress(ErrorMessage = "Please use a valid email address")] 10 | public string Email { get; set; } = string.Empty; 11 | 12 | [Required] 13 | [DataType(DataType.Password)] 14 | public string Password { get; set; } = string.Empty; 15 | } 16 | -------------------------------------------------------------------------------- /src/Cofoundry.Samples.SPASite.Domain/Domain/Members/Commands/SignMemberInCommandHandler.cs: -------------------------------------------------------------------------------- 1 | namespace Cofoundry.Samples.SPASite.Domain; 2 | 3 | /// 4 | /// Cofoundry has a number of apis to help you validate 5 | /// and sign users in, but here were going to simply wrap 6 | /// the Cofoundry SignInUserWithCredentialsCommand which handles 7 | /// validation, authentication and additional security checks 8 | /// such as rate limiting sign in attempts. 9 | /// 10 | public class SignMemberInCommandHandler 11 | : ICommandHandler 12 | , IIgnorePermissionCheckHandler 13 | { 14 | private readonly IAdvancedContentRepository _contentRepository; 15 | 16 | public SignMemberInCommandHandler( 17 | IAdvancedContentRepository contentRepository 18 | ) 19 | { 20 | _contentRepository = contentRepository; 21 | } 22 | 23 | public Task ExecuteAsync(SignMemberInCommand command, IExecutionContext executionContext) 24 | { 25 | return _contentRepository 26 | .Users() 27 | .Authentication() 28 | .SignInWithCredentialsAsync(new SignInUserWithCredentialsCommand() 29 | { 30 | Username = command.Email, 31 | Password = command.Password, 32 | UserAreaCode = MemberUserArea.Code, 33 | RememberUser = true 34 | }); 35 | } 36 | } -------------------------------------------------------------------------------- /src/Cofoundry.Samples.SPASite.Domain/Domain/Members/Commands/SignMemberOutCommand.cs: -------------------------------------------------------------------------------- 1 | namespace Cofoundry.Samples.SPASite; 2 | 3 | public class SignMemberOutCommand : ICommand 4 | { 5 | } -------------------------------------------------------------------------------- /src/Cofoundry.Samples.SPASite.Domain/Domain/Members/Commands/SignMemberOutCommandHandler.cs: -------------------------------------------------------------------------------- 1 | namespace Cofoundry.Samples.SPASite.Domain; 2 | 3 | /// 4 | /// A simple command handler to wrap member signout logic. Although it's 5 | /// only a one-liner, we've created a handler just to keep it consistent 6 | /// with the reset of the domain logic. 7 | /// 8 | public class SignMemberOutCommandHandler 9 | : ICommandHandler 10 | , IIgnorePermissionCheckHandler 11 | { 12 | private readonly IAdvancedContentRepository _contentRepository; 13 | 14 | public SignMemberOutCommandHandler( 15 | IAdvancedContentRepository contentRepository 16 | ) 17 | { 18 | _contentRepository = contentRepository; 19 | } 20 | 21 | public Task ExecuteAsync(SignMemberOutCommand command, IExecutionContext executionContext) 22 | { 23 | return _contentRepository 24 | .Users() 25 | .Authentication() 26 | .SignOutAsync(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Cofoundry.Samples.SPASite.Domain/Domain/Members/Definition/MemberRole.cs: -------------------------------------------------------------------------------- 1 | namespace Cofoundry.Samples.SPASite.Domain; 2 | 3 | /// 4 | /// Roles can be defined in code as well as in the admin panel. Defining 5 | /// a role in code means that it gets added automatically at startup. 6 | /// Additionally we have a RoleCode that we can use to query 7 | /// the role programatically. 8 | /// 9 | /// See https://www.cofoundry.org/docs/framework/roles-and-permissions 10 | /// 11 | public class MemberRole : IRoleDefinition 12 | { 13 | /// 14 | /// By convention we add a constant for the role code 15 | /// to make it easier to reference. 16 | /// 17 | public const string Code = "MEM"; 18 | 19 | /// 20 | /// The role code is a unique three letter code that can be used to reference 21 | /// the role programatically. The code must be unique and convention is to use 22 | /// upper case, although code matching is case insensitive. 23 | /// 24 | public string RoleCode => Code; 25 | 26 | /// 27 | /// The role title is used to identify the role and select it in the admin 28 | /// UI and therefore must be unique. Max 50 characters. 29 | /// 30 | public string Title => "Member"; 31 | 32 | /// 33 | /// A role must be assigned to a user area, in this case the role is used for members 34 | /// MemberUserArea.Code; 36 | 37 | /// 38 | /// This method determines which permissions the role is granted when it is first created. 39 | /// To help do this you are provided with a builder that contains all permissions in the 40 | /// system which you can use to either include or exclude permissions based on rules. 41 | /// 42 | public void ConfigurePermissions(IPermissionSetBuilder builder) 43 | { 44 | // In this example application we don't require any additional permissions for members 45 | // so we can re-use the permission set on the anonymous role which include read access 46 | // to most entities. 47 | builder.ApplyAnonymousRoleConfiguration(); 48 | } 49 | } -------------------------------------------------------------------------------- /src/Cofoundry.Samples.SPASite.Domain/Domain/Members/Definition/MemberUserArea.cs: -------------------------------------------------------------------------------- 1 | namespace Cofoundry.Samples.SPASite.Domain; 2 | 3 | /// 4 | /// Defining a user area allows us to require users to 5 | /// sign-up or log in to access certain features of the site. 6 | /// 7 | /// For more info see https://www.cofoundry.org/docs/content-management/user-areas 8 | /// 9 | public class MemberUserArea : IUserAreaDefinition 10 | { 11 | /// 12 | /// By convention we add a constant for the user area code 13 | /// to make it easier to reference. 14 | /// 15 | public const string Code = "SPA"; 16 | 17 | /// 18 | /// Indicates if users in this area can login using a password. If this is false 19 | /// the password field will be null and login will typically be via SSO or some 20 | /// other method. 21 | /// 22 | public bool AllowPasswordSignIn => true; 23 | 24 | /// 25 | /// Display name of the area, used in the Cofoundry admin panel 26 | /// as the navigation link to manage your users. This should be singular 27 | /// because "Users" is appended to the link text. 28 | /// 29 | public string Name => "SPA Cat"; 30 | 31 | /// 32 | /// Indicates whether the user should login using thier email address as the username. 33 | /// Some SSO systems might provide only a username and not an email address so in 34 | /// this case the email address is allowed to be null. 35 | /// 36 | public bool UseEmailAsUsername => true; 37 | 38 | /// 39 | /// A unique 3 letter code identifying this user area. The cofoundry 40 | /// user are uses the code "COF" so you can pick anything else! 41 | /// 42 | public string UserAreaCode => Code; 43 | 44 | /// 45 | /// Because the login routing is handled by the front end framework, we don't need to redirect to 46 | /// a specific login route. 47 | /// 48 | public string SignInPath => "/"; 49 | 50 | /// 51 | /// Setting this to true means that this user area will be used as the default login 52 | /// schema which means the HttpContext.User property will be set to this identity. 53 | /// 54 | public bool IsDefaultAuthScheme => true; 55 | 56 | public void ConfigureOptions(UserAreaOptions options) 57 | { 58 | // No additional configuration required 59 | } 60 | } -------------------------------------------------------------------------------- /src/Cofoundry.Samples.SPASite.Domain/Domain/Members/Models/MemberSummary.cs: -------------------------------------------------------------------------------- 1 | namespace Cofoundry.Samples.SPASite.Domain; 2 | 3 | /// 4 | /// A small model projection of a member. 5 | /// 6 | public class MemberSummary 7 | { 8 | public required int UserId { get; set; } 9 | 10 | public required string DisplayName { get; set; } 11 | } 12 | -------------------------------------------------------------------------------- /src/Cofoundry.Samples.SPASite.Domain/Domain/Members/Queries/GetCurrentMemberSummaryQuery.cs: -------------------------------------------------------------------------------- 1 | namespace Cofoundry.Samples.SPASite.Domain; 2 | 3 | public class GetCurrentMemberSummaryQuery : IQuery 4 | { 5 | } 6 | -------------------------------------------------------------------------------- /src/Cofoundry.Samples.SPASite.Domain/Domain/Members/Queries/GetCurrentMemberSummaryQueryHandler.cs: -------------------------------------------------------------------------------- 1 | namespace Cofoundry.Samples.SPASite.Domain; 2 | 3 | /// 4 | /// Query to get some information about the currently logged in user. We can use 5 | /// here because if the user is not 6 | /// logged in then we return null, so there's no need for a permission check. 7 | /// 8 | public class GetCurrentMemberSummaryQueryHandler 9 | : IQueryHandler 10 | , IIgnorePermissionCheckHandler 11 | { 12 | private readonly IContentRepository _contentRepository; 13 | 14 | public GetCurrentMemberSummaryQueryHandler( 15 | IContentRepository contentRepository 16 | ) 17 | { 18 | _contentRepository = contentRepository; 19 | } 20 | 21 | public async Task ExecuteAsync(GetCurrentMemberSummaryQuery query, IExecutionContext executionContext) 22 | { 23 | var userContext = executionContext.UserContext.ToSignedInContext(); 24 | if (userContext == null || userContext.UserArea.UserAreaCode != MemberUserArea.Code) 25 | { 26 | return null; 27 | } 28 | 29 | var user = await _contentRepository 30 | .Users() 31 | .Current() 32 | .Get() 33 | .AsMicroSummary() 34 | .ExecuteAsync(); 35 | 36 | if (user == null) 37 | { 38 | return null; 39 | } 40 | 41 | return new MemberSummary() 42 | { 43 | UserId = user.UserId, 44 | DisplayName = user.DisplayName ?? "Unknown" 45 | }; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Cofoundry.Samples.SPASite.Domain/Domain/ReadMe.md: -------------------------------------------------------------------------------- 1 | # SPASite.Domain 2 | 3 | In this example SPA we've moved all our domain logic out into a separate project and use the Cofoundry CQS framework to structure data access. 4 | 5 | This is how we structure our domain in Cofoundry, but you can of course choose to layer (or not layer) your application any way you prefer. 6 | 7 | For more information see https://www.cofoundry.org/docs/framework/cqs -------------------------------------------------------------------------------- /src/Cofoundry.Samples.SPASite.Domain/Install/Db/Schema/0001.sql: -------------------------------------------------------------------------------- 1 | /**************************************************************************** 2 | We can use the Cofoundry auto-update framework to upgrade our 3 | database using custom sql scripts. Cofoundry objects are namespaced 4 | under the Cofoundry schema, and we create an "app" schema that you can 5 | use for your own database objects but you're free to use your own schema 6 | or the default dbo schema if you prefer. 7 | *****************************************************************************/ 8 | 9 | -- Table to keep track of user likes 10 | 11 | create table app.CatLike ( 12 | CatCustomEntityId int not null, 13 | UserId int not null, 14 | CreateDate datetime2(7) not null, 15 | 16 | constraint PK_CatLike primary key (CatCustomEntityId, UserId), 17 | constraint FK_CatLike_CatCustomEntity foreign key (CatCustomEntityId) references Cofoundry.CustomEntity (CustomEntityId) on delete cascade, 18 | constraint FK_CatLike_User foreign key (UserId) references Cofoundry.[User] (UserId) on delete cascade 19 | ) 20 | 21 | -- Table to cache the total number of likes 22 | create table app.CatLikeCount ( 23 | CatCustomEntityId int not null, 24 | TotalLikes int not null, 25 | 26 | constraint PK_CatLikeCount primary key (CatCustomEntityId), 27 | constraint FK_CatLikeCount_CatCustomEntity foreign key (CatCustomEntityId) references Cofoundry.CustomEntity (CustomEntityId) on delete cascade 28 | ) -------------------------------------------------------------------------------- /src/Cofoundry.Samples.SPASite.Domain/Install/Db/StoredProcedures/app.CatLike_SetLiked.sql: -------------------------------------------------------------------------------- 1 | /**************************************************************************** 2 | The Cofoundry auto-updater can also be used to create stored procedures, 3 | triggers and functions. These scripts are run every time there is a version 4 | update and are automatically dropped if they exist before the create script 5 | is re-run. 6 | *****************************************************************************/ 7 | 8 | create procedure app.CatLike_SetLiked 9 | ( 10 | @CatId int, 11 | @UserId int, 12 | @IsLiked bit, 13 | @CreateDate datetime2 14 | ) 15 | as 16 | begin 17 | 18 | if (@IsLiked = 1) 19 | begin 20 | merge app.CatLike as destination 21 | using (values (@CatId, @UserId, @CreateDate)) src (CatCustomEntityId, UserId, CreateDate) 22 | on destination.UserId = src.UserId AND destination.CatCustomEntityId = src.CatCustomEntityId 23 | when not matched then 24 | insert (CatCustomEntityId, UserId, CreateDate) 25 | values (src.CatCustomEntityId, src.UserId, src.CreateDate); 26 | end 27 | else 28 | begin 29 | delete from app.CatLike where CatCustomEntityId = @CatId and UserId = @UserId 30 | end 31 | 32 | merge app.CatLikeCount as destination 33 | using ( 34 | select @CatId, Count(UserId) 35 | from app.CatLike cl 36 | right outer join Cofoundry.CustomEntity c on cl.CatCustomEntityId = c.CustomEntityId 37 | where cl.CatCustomEntityId = @CatId or c.CustomEntityId = @CatId 38 | group by cl.CatCustomEntityId 39 | ) src (CatCustomEntityId, TotalLikes) 40 | on destination.CatCustomEntityId = src.CatCustomEntityId 41 | when matched then 42 | update set destination.TotalLikes = src.TotalLikes 43 | when not matched then 44 | insert (CatCustomEntityId, TotalLikes) 45 | values (src.CatCustomEntityId, src.TotalLikes); 46 | 47 | end -------------------------------------------------------------------------------- /src/Cofoundry.Samples.SPASite.Domain/Install/UpdatePackageFactory.cs: -------------------------------------------------------------------------------- 1 | using Cofoundry.Core.AutoUpdate; 2 | 3 | namespace Cofoundry.Samples.SPASite.Domain.Install; 4 | 5 | /// 6 | /// Cofoundry has an automatic update system that runs when the app is 7 | /// started. This mainly runs sql scripts, but it can also run .net 8 | /// code too. Your app as well as any plugin can tap into this process 9 | /// to run updates. This example uses a base class to use the default 10 | /// process for updating a database using sql scripts. 11 | /// 12 | /// See https://www.cofoundry.org/docs/framework/auto-update 13 | /// 14 | public class UpdatePackageFactory : BaseDbOnlyUpdatePackageFactory 15 | { 16 | /// 17 | /// The module identifier should be unique to this installation 18 | /// and usually indicates the application or plugin being updated 19 | /// 20 | public override string ModuleIdentifier => "SPASite"; 21 | 22 | /// 23 | /// Here we can any modules that this installation is dependent 24 | /// on. In this case we are dependent on the Cofoundry installation 25 | /// being run before this one 26 | /// 27 | public override IReadOnlyCollection DependentModules { get; } = [CofoundryModuleInfo.ModuleIdentifier]; 28 | } 29 | -------------------------------------------------------------------------------- /src/Cofoundry.Samples.SPASite.Domain/MailTemplates/Bootstrap/AssemblyResourceRegistration.cs: -------------------------------------------------------------------------------- 1 | using Cofoundry.Core.ResourceFiles; 2 | 3 | namespace Cofoundry.Samples.SPASite.Domain; 4 | 5 | /// 6 | /// Registers this assembly so that embedded resources like the cshtml view files 7 | /// can be found by the framework. 8 | /// 9 | public class AssemblyResourceRegistration : IAssemblyResourceRegistration 10 | { 11 | } 12 | -------------------------------------------------------------------------------- /src/Cofoundry.Samples.SPASite.Domain/MailTemplates/NewUserWelcomeMailTemplate.cs: -------------------------------------------------------------------------------- 1 | using Cofoundry.Core.Mail; 2 | 3 | namespace Cofoundry.Samples.SPASite.Domain; 4 | 5 | /// 6 | /// Cofoundry includes a framework for sending mail based around template 7 | /// classes and razor view files. 8 | /// 9 | /// For more information see https://www.cofoundry.org/docs/framework/mail 10 | /// 11 | public class NewUserWelcomeMailTemplate : IMailTemplate 12 | { 13 | public string ViewFile 14 | { 15 | get { return "~/MailTemplates/NewUserWelcomeMail"; } 16 | } 17 | 18 | public string Subject 19 | { 20 | get { return "Welcome to SPA Cats!"; } 21 | } 22 | 23 | public string Name { get; set; } = string.Empty; 24 | } 25 | -------------------------------------------------------------------------------- /src/Cofoundry.Samples.SPASite.Domain/MailTemplates/NewUserWelcomeMail_Html.cshtml: -------------------------------------------------------------------------------- 1 | @model NewUserWelcomeMailTemplate 2 | @inject ICofoundryMailTemplateHelper Cofoundry 3 | @* 4 | Template view files can be placed in your website views directory, but this example 5 | demonstrates how you can keep them organized in your domain layer. 6 | 7 | To use these embedded view files you need to give them a build action of 'Embedded Resource' in 8 | the properties panel and include a class that implements IAssemblyResourceRegistration in this 9 | assembly. 10 | 11 | More info here: https://www.cofoundry.org/docs/framework/mail 12 | 13 | For a simpler (non-embedded resource) example see https://github.com/cofoundry-cms/Cofoundry.Samples.SimpleSite 14 | *@ 15 | 16 | @{ 17 | Layout = null; 18 | } 19 | 20 |

Hi @Model.Name

21 | 22 |

23 | Thanks for registering with SPA Cats! 24 |

25 |

26 | Sign in here 27 |

28 | 29 |

30 | Thanks,
31 | SPA Cats 32 |

-------------------------------------------------------------------------------- /src/Cofoundry.Samples.SPASite.Domain/MailTemplates/NewUserWelcomeMail_Text.cshtml: -------------------------------------------------------------------------------- 1 | @model NewUserWelcomeMailTemplate 2 | @inject ICofoundryMailTemplateHelper Cofoundry 3 | @{ 4 | Layout = null; 5 | } 6 | Hi @Model.Name 7 | 8 | Thanks for registering with SPA Cats! 9 | 10 | You can sign in to the website here: @Cofoundry.Routing.ToAbsolute("/") 11 | 12 | Thanks, 13 | SPA Cats 14 | -------------------------------------------------------------------------------- /src/Cofoundry.Samples.SPASite.Domain/MailTemplates/NewUserWelcomeMail_html.cshtml: -------------------------------------------------------------------------------- 1 | @model NewUserWelcomeMailTemplate 2 | @inject ICofoundryMailTemplateHelper Cofoundry 3 | @* 4 | Template view files can be placed in your website views directory, but this example 5 | demonstrates how you can keep them organized in your domain layer. 6 | 7 | To use these embedded view files you need to give them a build action of 'Embedded Resource' in 8 | the properties panel and include a class that implements IAssemblyResourceRegistration in this 9 | assembly. 10 | 11 | More info here: https://www.cofoundry.org/docs/framework/mail 12 | 13 | For a simpler (non-embedded resource) example see https://github.com/cofoundry-cms/Cofoundry.Samples.SimpleSite 14 | *@ 15 | 16 | @{ 17 | Layout = null; 18 | } 19 | 20 |

Hi @Model.Name

21 | 22 |

23 | Thanks for registering with SPA Cats! 24 |

25 |

26 | Sign in here 27 |

28 | 29 |

30 | Thanks,
31 | SPA Cats 32 |

-------------------------------------------------------------------------------- /src/Cofoundry.Samples.SPASite.Domain/MailTemplates/NewUserWelcomeMail_text.cshtml: -------------------------------------------------------------------------------- 1 | @model NewUserWelcomeMailTemplate 2 | @inject ICofoundryMailTemplateHelper Cofoundry 3 | @{ 4 | Layout = null; 5 | } 6 | Hi @Model.Name 7 | 8 | Thanks for registering with SPA Cats! 9 | 10 | You can sign in to the website here: @Cofoundry.Routing.ToAbsolute("/") 11 | 12 | Thanks, 13 | SPA Cats 14 | -------------------------------------------------------------------------------- /src/Cofoundry.Samples.SPASite.Domain/MailTemplates/_ViewImports.cshtml: -------------------------------------------------------------------------------- 1 | @using Cofoundry.Core 2 | @using Cofoundry.Domain 3 | @using Cofoundry.Domain.MailTemplates 4 | @using Cofoundry.Web 5 | @using Cofoundry.Samples.SPASite.Domain -------------------------------------------------------------------------------- /src/Cofoundry.Samples.SPASite.Domain/Usings.cs: -------------------------------------------------------------------------------- 1 | global using Cofoundry.Core; 2 | global using Cofoundry.Domain; 3 | global using Cofoundry.Domain.CQS; 4 | global using Microsoft.EntityFrameworkCore; 5 | -------------------------------------------------------------------------------- /src/Cofoundry.Samples.SPASite/Api/AuthApiController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Antiforgery; 2 | 3 | namespace Cofoundry.Samples.SPASite; 4 | 5 | [Route("api/auth")] 6 | [AutoValidateAntiforgeryToken] 7 | public class AuthApiController : ControllerBase 8 | { 9 | private readonly IApiResponseHelper _apiResponseHelper; 10 | private readonly IAntiforgery _antiforgery; 11 | private readonly IDomainRepository _domainRepository; 12 | 13 | public AuthApiController( 14 | IApiResponseHelper apiResponseHelper, 15 | IAntiforgery antiforgery, 16 | IDomainRepository domainRepository 17 | ) 18 | { 19 | _apiResponseHelper = apiResponseHelper; 20 | _antiforgery = antiforgery; 21 | _domainRepository = domainRepository; 22 | } 23 | 24 | /// 25 | /// Once we have logged in we need to re-fetch the csrf token because 26 | /// the user identity will have changed and the old token will be invalid 27 | /// 28 | [HttpGet("session")] 29 | public async Task GetAuthSession() 30 | { 31 | return await _apiResponseHelper.RunWithResultAsync(async () => 32 | { 33 | var member = await _domainRepository.ExecuteQueryAsync(new GetCurrentMemberSummaryQuery()); 34 | var token = _antiforgery.GetAndStoreTokens(HttpContext); 35 | 36 | var sessionInfo = new 37 | { 38 | Member = member, 39 | AntiForgeryToken = token.RequestToken 40 | }; 41 | 42 | return sessionInfo; 43 | }); 44 | } 45 | 46 | [HttpPost("register")] 47 | public async Task Register([FromBody] RegisterMemberAndLogInCommand command) 48 | { 49 | return await _apiResponseHelper.RunCommandAsync(command); 50 | } 51 | 52 | [HttpPost("login")] 53 | public async Task Login([FromBody] SignMemberInCommand command) 54 | { 55 | return await _apiResponseHelper.RunCommandAsync(command); 56 | } 57 | 58 | [HttpPost("sign-out")] 59 | public async Task SignOutUser() 60 | { 61 | var command = new SignMemberOutCommand(); 62 | 63 | return await _apiResponseHelper.RunCommandAsync(command); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Cofoundry.Samples.SPASite/Api/BreedsApiController.cs: -------------------------------------------------------------------------------- 1 | namespace Cofoundry.Samples.SPASite; 2 | 3 | [Route("api/breeds")] 4 | public class BreedsApiController : ControllerBase 5 | { 6 | private readonly IApiResponseHelper _apiResponseHelper; 7 | 8 | public BreedsApiController( 9 | IApiResponseHelper apiResponseHelper 10 | ) 11 | { 12 | _apiResponseHelper = apiResponseHelper; 13 | } 14 | 15 | [HttpGet("")] 16 | public async Task Get() 17 | { 18 | var query = new GetAllBreedsQuery(); 19 | 20 | return await _apiResponseHelper.RunQueryAsync(query); 21 | } 22 | 23 | [HttpGet("{breedId:int}")] 24 | public async Task Get(int breedId) 25 | { 26 | var query = new GetBreedByIdQuery(breedId); 27 | 28 | return await _apiResponseHelper.RunQueryAsync(query); 29 | } 30 | } -------------------------------------------------------------------------------- /src/Cofoundry.Samples.SPASite/Api/CatsApiController.cs: -------------------------------------------------------------------------------- 1 | namespace Cofoundry.Samples.SPASite; 2 | 3 | [Route("api/cats")] 4 | [AutoValidateAntiforgeryToken] 5 | public class CatsApiController : ControllerBase 6 | { 7 | private readonly IApiResponseHelper _apiResponseHelper; 8 | 9 | public CatsApiController( 10 | IApiResponseHelper apiResponseHelper 11 | ) 12 | { 13 | _apiResponseHelper = apiResponseHelper; 14 | } 15 | 16 | [HttpGet("")] 17 | public async Task Get([FromQuery] SearchCatSummariesQuery query) 18 | { 19 | query ??= new SearchCatSummariesQuery(); 20 | 21 | return await _apiResponseHelper.RunQueryAsync(query); 22 | } 23 | 24 | [HttpGet("{catId:int}")] 25 | public async Task Get(int catId) 26 | { 27 | var query = new GetCatDetailsByIdQuery(catId); 28 | 29 | return await _apiResponseHelper.RunQueryAsync(query); 30 | } 31 | 32 | /// 33 | /// Note that here we use the standard Authorize attribute to restrict 34 | /// access to this endpoint because you need to be logged in to 'like' a 35 | /// cat 36 | /// 37 | [AuthorizeUserArea(MemberUserArea.Code)] 38 | [HttpPost("{catId:int}/likes")] 39 | public async Task Like(int catId) 40 | { 41 | var command = new SetCatLikedCommand() 42 | { 43 | CatId = catId, 44 | IsLiked = true 45 | }; 46 | 47 | // IApiResponseHelper will validate the command and permissions before executing it 48 | // and return any validation errors in a formatted data object 49 | return await _apiResponseHelper.RunCommandAsync(command); 50 | } 51 | 52 | [AuthorizeUserArea(MemberUserArea.Code)] 53 | [HttpDelete("{catId:int}/likes")] 54 | public async Task UnLike(int catId) 55 | { 56 | var command = new SetCatLikedCommand() 57 | { 58 | CatId = catId 59 | }; 60 | 61 | return await _apiResponseHelper.RunCommandAsync(command); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Cofoundry.Samples.SPASite/Api/CurrentUserApiController.cs: -------------------------------------------------------------------------------- 1 | namespace Cofoundry.Samples.SPASite; 2 | 3 | [AuthorizeUserArea(MemberUserArea.Code)] 4 | [Route("api/members/current")] 5 | public class CurrentMemberApiController : ControllerBase 6 | { 7 | private readonly IContentRepository _contentRepository; 8 | private readonly IApiResponseHelper _apiResponseHelper; 9 | 10 | public CurrentMemberApiController( 11 | IContentRepository contentRepository, 12 | IApiResponseHelper apiResponseHelper 13 | ) 14 | { 15 | _contentRepository = contentRepository; 16 | _apiResponseHelper = apiResponseHelper; 17 | } 18 | 19 | [HttpGet("cats/liked")] 20 | public async Task GetLikedCats() 21 | { 22 | // Here we get the userId of the currently signed in member. We could have 23 | // done this in the query handler, but instead we've chosen to keep the query 24 | // flexible so it can be re-used in a more generic fashion 25 | var userContext = await _contentRepository 26 | .Users() 27 | .Current() 28 | .Get() 29 | .AsUserContext() 30 | .ExecuteAsync(); 31 | 32 | var signedInUser = userContext.ToSignedInContext(); 33 | if (signedInUser == null) 34 | { 35 | return Forbid(); 36 | } 37 | 38 | var query = new GetCatSummariesByMemberLikedQuery(signedInUser.UserId); 39 | return await _apiResponseHelper.RunQueryAsync(query); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Cofoundry.Samples.SPASite/Api/FeaturesApiController.cs: -------------------------------------------------------------------------------- 1 | namespace Cofoundry.Samples.SPASite; 2 | 3 | [Route("api/features")] 4 | public class FeaturesApiController : ControllerBase 5 | { 6 | private readonly IApiResponseHelper _apiResponseHelper; 7 | 8 | public FeaturesApiController( 9 | IApiResponseHelper apiResponseHelper 10 | ) 11 | { 12 | _apiResponseHelper = apiResponseHelper; 13 | } 14 | 15 | [HttpGet("")] 16 | public async Task Get() 17 | { 18 | var query = new GetAllFeaturesQuery(); 19 | 20 | return await _apiResponseHelper.RunQueryAsync(query); 21 | } 22 | } -------------------------------------------------------------------------------- /src/Cofoundry.Samples.SPASite/Api/ReadMe.md: -------------------------------------------------------------------------------- 1 | # WebApi 2 | 3 | Cofoundry doesn't include web api endpoints out the box because we don't want to make assumptions as to what data you want to expose or how you want to structure it. Creating a web api is simple to do though and you can make use of our IApiResponseHelper to help cut down on boilerplate code. 4 | -------------------------------------------------------------------------------- /src/Cofoundry.Samples.SPASite/ClientApp/README.md: -------------------------------------------------------------------------------- 1 | # SPASite Vue CLI App 2 | 3 | Tested with node v14.15.1 4 | 5 | ## Project setup 6 | ``` 7 | npm install 8 | ``` 9 | 10 | ### Compiles and hot-reloads for development 11 | ``` 12 | npm run serve 13 | ``` 14 | 15 | ### Compiles and minifies for production 16 | ``` 17 | npm run build 18 | ``` 19 | 20 | ### Run your tests 21 | ``` 22 | npm run test 23 | ``` 24 | 25 | ### Lints and fixes files 26 | ``` 27 | npm run lint 28 | ``` 29 | 30 | ### Customize configuration 31 | See [Configuration Reference](https://cli.vuejs.org/config/). 32 | -------------------------------------------------------------------------------- /src/Cofoundry.Samples.SPASite/ClientApp/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/app' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /src/Cofoundry.Samples.SPASite/ClientApp/dist/css/CatDetails.e8c33d00.css: -------------------------------------------------------------------------------- 1 | /*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */html[data-v-ea29e130]{line-height:1.15;-webkit-text-size-adjust:100%}body[data-v-ea29e130]{margin:0}main[data-v-ea29e130]{display:block}h1[data-v-ea29e130]{font-size:2em;margin:.67em 0}hr[data-v-ea29e130]{-webkit-box-sizing:content-box;box-sizing:content-box;height:0;overflow:visible}pre[data-v-ea29e130]{font-family:monospace,monospace;font-size:1em}a[data-v-ea29e130]{background-color:transparent}abbr[title][data-v-ea29e130]{border-bottom:none;text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted}b[data-v-ea29e130],strong[data-v-ea29e130]{font-weight:bolder}code[data-v-ea29e130],kbd[data-v-ea29e130],samp[data-v-ea29e130]{font-family:monospace,monospace;font-size:1em}small[data-v-ea29e130]{font-size:80%}sub[data-v-ea29e130],sup[data-v-ea29e130]{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub[data-v-ea29e130]{bottom:-.25em}sup[data-v-ea29e130]{top:-.5em}img[data-v-ea29e130]{border-style:none}button[data-v-ea29e130],input[data-v-ea29e130],optgroup[data-v-ea29e130],select[data-v-ea29e130],textarea[data-v-ea29e130]{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button[data-v-ea29e130],input[data-v-ea29e130]{overflow:visible}button[data-v-ea29e130],select[data-v-ea29e130]{text-transform:none}[type=button][data-v-ea29e130],[type=reset][data-v-ea29e130],[type=submit][data-v-ea29e130],button[data-v-ea29e130]{-webkit-appearance:button}[type=button][data-v-ea29e130]::-moz-focus-inner,[type=reset][data-v-ea29e130]::-moz-focus-inner,[type=submit][data-v-ea29e130]::-moz-focus-inner,button[data-v-ea29e130]::-moz-focus-inner{border-style:none;padding:0}[type=button][data-v-ea29e130]:-moz-focusring,[type=reset][data-v-ea29e130]:-moz-focusring,[type=submit][data-v-ea29e130]:-moz-focusring,button[data-v-ea29e130]:-moz-focusring{outline:1px dotted ButtonText}fieldset[data-v-ea29e130]{padding:.35em .75em .625em}legend[data-v-ea29e130]{-webkit-box-sizing:border-box;box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress[data-v-ea29e130]{vertical-align:baseline}textarea[data-v-ea29e130]{overflow:auto}[type=checkbox][data-v-ea29e130],[type=radio][data-v-ea29e130]{-webkit-box-sizing:border-box;box-sizing:border-box;padding:0}[type=number][data-v-ea29e130]::-webkit-inner-spin-button,[type=number][data-v-ea29e130]::-webkit-outer-spin-button{height:auto}[type=search][data-v-ea29e130]{-webkit-appearance:textfield;outline-offset:-2px}[type=search][data-v-ea29e130]::-webkit-search-decoration{-webkit-appearance:none}[data-v-ea29e130]::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details[data-v-ea29e130]{display:block}summary[data-v-ea29e130]{display:list-item}[hidden][data-v-ea29e130],template[data-v-ea29e130]{display:none}.wrapper[data-v-ea29e130]{margin:2rem 0;-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1}.content[data-v-ea29e130]{background-color:#fff;padding:1rem;margin:1rem;border-radius:3px}@media screen and (min-width:768px){.content[data-v-ea29e130]{max-width:1260px;margin:0 auto}}[data-v-ea29e130]:first-child{margin-top:0} 2 | 3 | /*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */html[data-v-39d0d568]{line-height:1.15;-webkit-text-size-adjust:100%}body[data-v-39d0d568]{margin:0}main[data-v-39d0d568]{display:block}h1[data-v-39d0d568]{font-size:2em;margin:.67em 0}hr[data-v-39d0d568]{-webkit-box-sizing:content-box;box-sizing:content-box;height:0;overflow:visible}pre[data-v-39d0d568]{font-family:monospace,monospace;font-size:1em}a[data-v-39d0d568]{background-color:transparent}abbr[title][data-v-39d0d568]{border-bottom:none;text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted}b[data-v-39d0d568],strong[data-v-39d0d568]{font-weight:bolder}code[data-v-39d0d568],kbd[data-v-39d0d568],samp[data-v-39d0d568]{font-family:monospace,monospace;font-size:1em}small[data-v-39d0d568]{font-size:80%}sub[data-v-39d0d568],sup[data-v-39d0d568]{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub[data-v-39d0d568]{bottom:-.25em}sup[data-v-39d0d568]{top:-.5em}img[data-v-39d0d568]{border-style:none}button[data-v-39d0d568],input[data-v-39d0d568],optgroup[data-v-39d0d568],select[data-v-39d0d568],textarea[data-v-39d0d568]{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button[data-v-39d0d568],input[data-v-39d0d568]{overflow:visible}button[data-v-39d0d568],select[data-v-39d0d568]{text-transform:none}[type=button][data-v-39d0d568],[type=reset][data-v-39d0d568],[type=submit][data-v-39d0d568],button[data-v-39d0d568]{-webkit-appearance:button}[type=button][data-v-39d0d568]::-moz-focus-inner,[type=reset][data-v-39d0d568]::-moz-focus-inner,[type=submit][data-v-39d0d568]::-moz-focus-inner,button[data-v-39d0d568]::-moz-focus-inner{border-style:none;padding:0}[type=button][data-v-39d0d568]:-moz-focusring,[type=reset][data-v-39d0d568]:-moz-focusring,[type=submit][data-v-39d0d568]:-moz-focusring,button[data-v-39d0d568]:-moz-focusring{outline:1px dotted ButtonText}fieldset[data-v-39d0d568]{padding:.35em .75em .625em}legend[data-v-39d0d568]{-webkit-box-sizing:border-box;box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress[data-v-39d0d568]{vertical-align:baseline}textarea[data-v-39d0d568]{overflow:auto}[type=checkbox][data-v-39d0d568],[type=radio][data-v-39d0d568]{-webkit-box-sizing:border-box;box-sizing:border-box;padding:0}[type=number][data-v-39d0d568]::-webkit-inner-spin-button,[type=number][data-v-39d0d568]::-webkit-outer-spin-button{height:auto}[type=search][data-v-39d0d568]{-webkit-appearance:textfield;outline-offset:-2px}[type=search][data-v-39d0d568]::-webkit-search-decoration{-webkit-appearance:none}[data-v-39d0d568]::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details[data-v-39d0d568]{display:block}summary[data-v-39d0d568]{display:list-item}[hidden][data-v-39d0d568],template[data-v-39d0d568]{display:none}.heading[data-v-39d0d568]{display:block}@media screen and (min-width:768px){.heading[data-v-39d0d568]{display:-webkit-box;display:-ms-flexbox;display:flex}}.title[data-v-39d0d568]{margin:0;-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1;font-size:1.6rem}@media screen and (min-width:768px){.title[data-v-39d0d568]{font-size:2rem}}.num-likes[data-v-39d0d568]{font-size:1.6rem}@media screen and (min-width:768px){.num-likes[data-v-39d0d568]{font-size:2rem}}.info dt[data-v-39d0d568]{margin-top:1rem;font-weight:700}.info dd[data-v-39d0d568]{margin:0}.cat-images[data-v-39d0d568]{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}.cat-images img[data-v-39d0d568]{width:100%;height:auto;margin-bottom:1rem}@media screen and (min-width:768px){.cat-images[data-v-39d0d568]{display:grid;grid-template-columns:repeat(auto-fit,minmax(300px,1fr));grid-auto-rows:minmax(150px,auto);grid-gap:2rem}.cat-images img[data-v-39d0d568]{margin-bottom:0}}.actions[data-v-39d0d568]{margin:2rem 0}.btn-love[data-v-39d0d568]{background-color:#5e0042;color:#fff;padding:.6rem 4rem;border:none;border-radius:30px;-webkit-transition:background-color .2s ease-out;transition:background-color .2s ease-out;width:100%;cursor:pointer}.btn-love[data-v-39d0d568]:hover{background-color:#2c2233;color:#fff}@media screen and (min-width:768px){.btn-love[data-v-39d0d568]{width:unset}} -------------------------------------------------------------------------------- /src/Cofoundry.Samples.SPASite/ClientApp/dist/css/Login~Register.76e18863.css: -------------------------------------------------------------------------------- 1 | /*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */html[data-v-3d87f538]{line-height:1.15;-webkit-text-size-adjust:100%}body[data-v-3d87f538]{margin:0}main[data-v-3d87f538]{display:block}h1[data-v-3d87f538]{font-size:2em;margin:.67em 0}hr[data-v-3d87f538]{-webkit-box-sizing:content-box;box-sizing:content-box;height:0;overflow:visible}pre[data-v-3d87f538]{font-family:monospace,monospace;font-size:1em}a[data-v-3d87f538]{background-color:transparent}abbr[title][data-v-3d87f538]{border-bottom:none;text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted}b[data-v-3d87f538],strong[data-v-3d87f538]{font-weight:bolder}code[data-v-3d87f538],kbd[data-v-3d87f538],samp[data-v-3d87f538]{font-family:monospace,monospace;font-size:1em}small[data-v-3d87f538]{font-size:80%}sub[data-v-3d87f538],sup[data-v-3d87f538]{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub[data-v-3d87f538]{bottom:-.25em}sup[data-v-3d87f538]{top:-.5em}img[data-v-3d87f538]{border-style:none}button[data-v-3d87f538],input[data-v-3d87f538],optgroup[data-v-3d87f538],select[data-v-3d87f538],textarea[data-v-3d87f538]{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button[data-v-3d87f538],input[data-v-3d87f538]{overflow:visible}button[data-v-3d87f538],select[data-v-3d87f538]{text-transform:none}[type=button][data-v-3d87f538],[type=reset][data-v-3d87f538],[type=submit][data-v-3d87f538],button[data-v-3d87f538]{-webkit-appearance:button}[type=button][data-v-3d87f538]::-moz-focus-inner,[type=reset][data-v-3d87f538]::-moz-focus-inner,[type=submit][data-v-3d87f538]::-moz-focus-inner,button[data-v-3d87f538]::-moz-focus-inner{border-style:none;padding:0}[type=button][data-v-3d87f538]:-moz-focusring,[type=reset][data-v-3d87f538]:-moz-focusring,[type=submit][data-v-3d87f538]:-moz-focusring,button[data-v-3d87f538]:-moz-focusring{outline:1px dotted ButtonText}fieldset[data-v-3d87f538]{padding:.35em .75em .625em}legend[data-v-3d87f538]{-webkit-box-sizing:border-box;box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress[data-v-3d87f538]{vertical-align:baseline}textarea[data-v-3d87f538]{overflow:auto}[type=checkbox][data-v-3d87f538],[type=radio][data-v-3d87f538]{-webkit-box-sizing:border-box;box-sizing:border-box;padding:0}[type=number][data-v-3d87f538]::-webkit-inner-spin-button,[type=number][data-v-3d87f538]::-webkit-outer-spin-button{height:auto}[type=search][data-v-3d87f538]{-webkit-appearance:textfield;outline-offset:-2px}[type=search][data-v-3d87f538]::-webkit-search-decoration{-webkit-appearance:none}[data-v-3d87f538]::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details[data-v-3d87f538]{display:block}summary[data-v-3d87f538]{display:list-item}[hidden][data-v-3d87f538],template[data-v-3d87f538]{display:none}.container[data-v-3d87f538]{padding:1rem 2rem;color:red;border:1px solid #ff8181;background-color:#fee;margin:2rem 0} 2 | 3 | /*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */html[data-v-5ab48f2a]{line-height:1.15;-webkit-text-size-adjust:100%}body[data-v-5ab48f2a]{margin:0}main[data-v-5ab48f2a]{display:block}h1[data-v-5ab48f2a]{font-size:2em;margin:.67em 0}hr[data-v-5ab48f2a]{-webkit-box-sizing:content-box;box-sizing:content-box;height:0;overflow:visible}pre[data-v-5ab48f2a]{font-family:monospace,monospace;font-size:1em}a[data-v-5ab48f2a]{background-color:transparent}abbr[title][data-v-5ab48f2a]{border-bottom:none;text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted}b[data-v-5ab48f2a],strong[data-v-5ab48f2a]{font-weight:bolder}code[data-v-5ab48f2a],kbd[data-v-5ab48f2a],samp[data-v-5ab48f2a]{font-family:monospace,monospace;font-size:1em}small[data-v-5ab48f2a]{font-size:80%}sub[data-v-5ab48f2a],sup[data-v-5ab48f2a]{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub[data-v-5ab48f2a]{bottom:-.25em}sup[data-v-5ab48f2a]{top:-.5em}img[data-v-5ab48f2a]{border-style:none}button[data-v-5ab48f2a],input[data-v-5ab48f2a],optgroup[data-v-5ab48f2a],select[data-v-5ab48f2a],textarea[data-v-5ab48f2a]{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button[data-v-5ab48f2a],input[data-v-5ab48f2a]{overflow:visible}button[data-v-5ab48f2a],select[data-v-5ab48f2a]{text-transform:none}[type=button][data-v-5ab48f2a],[type=reset][data-v-5ab48f2a],[type=submit][data-v-5ab48f2a],button[data-v-5ab48f2a]{-webkit-appearance:button}[type=button][data-v-5ab48f2a]::-moz-focus-inner,[type=reset][data-v-5ab48f2a]::-moz-focus-inner,[type=submit][data-v-5ab48f2a]::-moz-focus-inner,button[data-v-5ab48f2a]::-moz-focus-inner{border-style:none;padding:0}[type=button][data-v-5ab48f2a]:-moz-focusring,[type=reset][data-v-5ab48f2a]:-moz-focusring,[type=submit][data-v-5ab48f2a]:-moz-focusring,button[data-v-5ab48f2a]:-moz-focusring{outline:1px dotted ButtonText}fieldset[data-v-5ab48f2a]{padding:.35em .75em .625em}legend[data-v-5ab48f2a]{-webkit-box-sizing:border-box;box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress[data-v-5ab48f2a]{vertical-align:baseline}textarea[data-v-5ab48f2a]{overflow:auto}[type=checkbox][data-v-5ab48f2a],[type=radio][data-v-5ab48f2a]{-webkit-box-sizing:border-box;box-sizing:border-box;padding:0}[type=number][data-v-5ab48f2a]::-webkit-inner-spin-button,[type=number][data-v-5ab48f2a]::-webkit-outer-spin-button{height:auto}[type=search][data-v-5ab48f2a]{-webkit-appearance:textfield;outline-offset:-2px}[type=search][data-v-5ab48f2a]::-webkit-search-decoration{-webkit-appearance:none}[data-v-5ab48f2a]::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details[data-v-5ab48f2a]{display:block}summary[data-v-5ab48f2a]{display:list-item}[hidden][data-v-5ab48f2a],template[data-v-5ab48f2a]{display:none}.form-group[data-v-5ab48f2a]{max-width:400px;margin-bottom:1rem}label[data-v-5ab48f2a]{display:block;font-weight:700;margin-bottom:.5rem}input[data-v-5ab48f2a]{border:1px solid #ccc;padding:.3rem .8rem;width:100%} 4 | 5 | /*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */html[data-v-ea29e130]{line-height:1.15;-webkit-text-size-adjust:100%}body[data-v-ea29e130]{margin:0}main[data-v-ea29e130]{display:block}h1[data-v-ea29e130]{font-size:2em;margin:.67em 0}hr[data-v-ea29e130]{-webkit-box-sizing:content-box;box-sizing:content-box;height:0;overflow:visible}pre[data-v-ea29e130]{font-family:monospace,monospace;font-size:1em}a[data-v-ea29e130]{background-color:transparent}abbr[title][data-v-ea29e130]{border-bottom:none;text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted}b[data-v-ea29e130],strong[data-v-ea29e130]{font-weight:bolder}code[data-v-ea29e130],kbd[data-v-ea29e130],samp[data-v-ea29e130]{font-family:monospace,monospace;font-size:1em}small[data-v-ea29e130]{font-size:80%}sub[data-v-ea29e130],sup[data-v-ea29e130]{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub[data-v-ea29e130]{bottom:-.25em}sup[data-v-ea29e130]{top:-.5em}img[data-v-ea29e130]{border-style:none}button[data-v-ea29e130],input[data-v-ea29e130],optgroup[data-v-ea29e130],select[data-v-ea29e130],textarea[data-v-ea29e130]{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button[data-v-ea29e130],input[data-v-ea29e130]{overflow:visible}button[data-v-ea29e130],select[data-v-ea29e130]{text-transform:none}[type=button][data-v-ea29e130],[type=reset][data-v-ea29e130],[type=submit][data-v-ea29e130],button[data-v-ea29e130]{-webkit-appearance:button}[type=button][data-v-ea29e130]::-moz-focus-inner,[type=reset][data-v-ea29e130]::-moz-focus-inner,[type=submit][data-v-ea29e130]::-moz-focus-inner,button[data-v-ea29e130]::-moz-focus-inner{border-style:none;padding:0}[type=button][data-v-ea29e130]:-moz-focusring,[type=reset][data-v-ea29e130]:-moz-focusring,[type=submit][data-v-ea29e130]:-moz-focusring,button[data-v-ea29e130]:-moz-focusring{outline:1px dotted ButtonText}fieldset[data-v-ea29e130]{padding:.35em .75em .625em}legend[data-v-ea29e130]{-webkit-box-sizing:border-box;box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress[data-v-ea29e130]{vertical-align:baseline}textarea[data-v-ea29e130]{overflow:auto}[type=checkbox][data-v-ea29e130],[type=radio][data-v-ea29e130]{-webkit-box-sizing:border-box;box-sizing:border-box;padding:0}[type=number][data-v-ea29e130]::-webkit-inner-spin-button,[type=number][data-v-ea29e130]::-webkit-outer-spin-button{height:auto}[type=search][data-v-ea29e130]{-webkit-appearance:textfield;outline-offset:-2px}[type=search][data-v-ea29e130]::-webkit-search-decoration{-webkit-appearance:none}[data-v-ea29e130]::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details[data-v-ea29e130]{display:block}summary[data-v-ea29e130]{display:list-item}[hidden][data-v-ea29e130],template[data-v-ea29e130]{display:none}.wrapper[data-v-ea29e130]{margin:2rem 0;-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1}.content[data-v-ea29e130]{background-color:#fff;padding:1rem;margin:1rem;border-radius:3px}@media screen and (min-width:768px){.content[data-v-ea29e130]{max-width:1260px;margin:0 auto}}[data-v-ea29e130]:first-child{margin-top:0} 6 | 7 | /*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */html[data-v-5e06ae76]{line-height:1.15;-webkit-text-size-adjust:100%}body[data-v-5e06ae76]{margin:0}main[data-v-5e06ae76]{display:block}h1[data-v-5e06ae76]{font-size:2em;margin:.67em 0}hr[data-v-5e06ae76]{-webkit-box-sizing:content-box;box-sizing:content-box;height:0;overflow:visible}pre[data-v-5e06ae76]{font-family:monospace,monospace;font-size:1em}a[data-v-5e06ae76]{background-color:transparent}abbr[title][data-v-5e06ae76]{border-bottom:none;text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted}b[data-v-5e06ae76],strong[data-v-5e06ae76]{font-weight:bolder}code[data-v-5e06ae76],kbd[data-v-5e06ae76],samp[data-v-5e06ae76]{font-family:monospace,monospace;font-size:1em}small[data-v-5e06ae76]{font-size:80%}sub[data-v-5e06ae76],sup[data-v-5e06ae76]{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub[data-v-5e06ae76]{bottom:-.25em}sup[data-v-5e06ae76]{top:-.5em}img[data-v-5e06ae76]{border-style:none}button[data-v-5e06ae76],input[data-v-5e06ae76],optgroup[data-v-5e06ae76],select[data-v-5e06ae76],textarea[data-v-5e06ae76]{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button[data-v-5e06ae76],input[data-v-5e06ae76]{overflow:visible}button[data-v-5e06ae76],select[data-v-5e06ae76]{text-transform:none}[type=button][data-v-5e06ae76],[type=reset][data-v-5e06ae76],[type=submit][data-v-5e06ae76],button[data-v-5e06ae76]{-webkit-appearance:button}[type=button][data-v-5e06ae76]::-moz-focus-inner,[type=reset][data-v-5e06ae76]::-moz-focus-inner,[type=submit][data-v-5e06ae76]::-moz-focus-inner,button[data-v-5e06ae76]::-moz-focus-inner{border-style:none;padding:0}[type=button][data-v-5e06ae76]:-moz-focusring,[type=reset][data-v-5e06ae76]:-moz-focusring,[type=submit][data-v-5e06ae76]:-moz-focusring,button[data-v-5e06ae76]:-moz-focusring{outline:1px dotted ButtonText}fieldset[data-v-5e06ae76]{padding:.35em .75em .625em}legend[data-v-5e06ae76]{-webkit-box-sizing:border-box;box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress[data-v-5e06ae76]{vertical-align:baseline}textarea[data-v-5e06ae76]{overflow:auto}[type=checkbox][data-v-5e06ae76],[type=radio][data-v-5e06ae76]{-webkit-box-sizing:border-box;box-sizing:border-box;padding:0}[type=number][data-v-5e06ae76]::-webkit-inner-spin-button,[type=number][data-v-5e06ae76]::-webkit-outer-spin-button{height:auto}[type=search][data-v-5e06ae76]{-webkit-appearance:textfield;outline-offset:-2px}[type=search][data-v-5e06ae76]::-webkit-search-decoration{-webkit-appearance:none}[data-v-5e06ae76]::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details[data-v-5e06ae76]{display:block}summary[data-v-5e06ae76]{display:list-item}[hidden][data-v-5e06ae76],template[data-v-5e06ae76]{display:none}.actions[data-v-5e06ae76]{margin:2rem 0 1rem 0} 8 | 9 | /*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */html[data-v-167d0e48]{line-height:1.15;-webkit-text-size-adjust:100%}body[data-v-167d0e48]{margin:0}main[data-v-167d0e48]{display:block}h1[data-v-167d0e48]{font-size:2em;margin:.67em 0}hr[data-v-167d0e48]{-webkit-box-sizing:content-box;box-sizing:content-box;height:0;overflow:visible}pre[data-v-167d0e48]{font-family:monospace,monospace;font-size:1em}a[data-v-167d0e48]{background-color:transparent}abbr[title][data-v-167d0e48]{border-bottom:none;text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted}b[data-v-167d0e48],strong[data-v-167d0e48]{font-weight:bolder}code[data-v-167d0e48],kbd[data-v-167d0e48],samp[data-v-167d0e48]{font-family:monospace,monospace;font-size:1em}small[data-v-167d0e48]{font-size:80%}sub[data-v-167d0e48],sup[data-v-167d0e48]{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub[data-v-167d0e48]{bottom:-.25em}sup[data-v-167d0e48]{top:-.5em}img[data-v-167d0e48]{border-style:none}button[data-v-167d0e48],input[data-v-167d0e48],optgroup[data-v-167d0e48],select[data-v-167d0e48],textarea[data-v-167d0e48]{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button[data-v-167d0e48],input[data-v-167d0e48]{overflow:visible}button[data-v-167d0e48],select[data-v-167d0e48]{text-transform:none}[type=button][data-v-167d0e48],[type=reset][data-v-167d0e48],[type=submit][data-v-167d0e48],button[data-v-167d0e48]{-webkit-appearance:button}[type=button][data-v-167d0e48]::-moz-focus-inner,[type=reset][data-v-167d0e48]::-moz-focus-inner,[type=submit][data-v-167d0e48]::-moz-focus-inner,button[data-v-167d0e48]::-moz-focus-inner{border-style:none;padding:0}[type=button][data-v-167d0e48]:-moz-focusring,[type=reset][data-v-167d0e48]:-moz-focusring,[type=submit][data-v-167d0e48]:-moz-focusring,button[data-v-167d0e48]:-moz-focusring{outline:1px dotted ButtonText}fieldset[data-v-167d0e48]{padding:.35em .75em .625em}legend[data-v-167d0e48]{-webkit-box-sizing:border-box;box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress[data-v-167d0e48]{vertical-align:baseline}textarea[data-v-167d0e48]{overflow:auto}[type=checkbox][data-v-167d0e48],[type=radio][data-v-167d0e48]{-webkit-box-sizing:border-box;box-sizing:border-box;padding:0}[type=number][data-v-167d0e48]::-webkit-inner-spin-button,[type=number][data-v-167d0e48]::-webkit-outer-spin-button{height:auto}[type=search][data-v-167d0e48]{-webkit-appearance:textfield;outline-offset:-2px}[type=search][data-v-167d0e48]::-webkit-search-decoration{-webkit-appearance:none}[data-v-167d0e48]::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details[data-v-167d0e48]{display:block}summary[data-v-167d0e48]{display:list-item}[hidden][data-v-167d0e48],template[data-v-167d0e48]{display:none}.btn[data-v-167d0e48]{background-color:#5e0042;color:#fff;padding:.6rem 4rem;border:none;border-radius:30px;-webkit-transition:background-color .2s ease-out;transition:background-color .2s ease-out;width:100%;cursor:pointer}.btn[data-v-167d0e48]:hover{background-color:#2c2233;color:#fff}@media screen and (min-width:768px){.btn[data-v-167d0e48]{width:unset}} -------------------------------------------------------------------------------- /src/Cofoundry.Samples.SPASite/ClientApp/dist/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cofoundry-cms/Cofoundry.Samples.SPASite/ba0f8aaf565dc9b42a172ceac5b14f8aa06dd371/src/Cofoundry.Samples.SPASite/ClientApp/dist/favicon.png -------------------------------------------------------------------------------- /src/Cofoundry.Samples.SPASite/ClientApp/dist/img/cofoundry-logo.6a7dc3a6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cofoundry-cms/Cofoundry.Samples.SPASite/ba0f8aaf565dc9b42a172ceac5b14f8aa06dd371/src/Cofoundry.Samples.SPASite/ClientApp/dist/img/cofoundry-logo.6a7dc3a6.png -------------------------------------------------------------------------------- /src/Cofoundry.Samples.SPASite/ClientApp/dist/img/spacats-logo.602f6e89.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cofoundry-cms/Cofoundry.Samples.SPASite/ba0f8aaf565dc9b42a172ceac5b14f8aa06dd371/src/Cofoundry.Samples.SPASite/ClientApp/dist/img/spacats-logo.602f6e89.png -------------------------------------------------------------------------------- /src/Cofoundry.Samples.SPASite/ClientApp/dist/index.html: -------------------------------------------------------------------------------- 1 | Cofoundry SPA Sample
-------------------------------------------------------------------------------- /src/Cofoundry.Samples.SPASite/ClientApp/dist/js/404.5e643d04.js: -------------------------------------------------------------------------------- 1 | (window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["404"],{9703:function(n,e,t){"use strict";t.r(e);var a=function(){var n=this,e=n.$createElement;n._self._c;return n._m(0)},o=[function(){var n=this,e=n.$createElement,t=n._self._c||e;return t("main",{staticClass:"main"},[t("p",[n._v("\n Page not found\n ")])])}],u=(t("cadf"),t("551c"),t("097d"),{name:"NotFound"}),c=u,s=t("2877"),i=Object(s["a"])(c,a,o,!1,null,null,null);i.options.__file="NotFound.vue";e["default"]=i.exports}}]); 2 | //# sourceMappingURL=404.5e643d04.js.map -------------------------------------------------------------------------------- /src/Cofoundry.Samples.SPASite/ClientApp/dist/js/CatDetails.98ad2d1f.js: -------------------------------------------------------------------------------- 1 | (window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["CatDetails"],{"014b":function(t,e,n){"use strict";var r=n("e53d"),i=n("07e3"),a=n("8e60"),o=n("63b6"),c=n("9138"),s=n("ebfd").KEY,f=n("294c"),u=n("dbdb"),l=n("45f2"),b=n("62a0"),d=n("5168"),p=n("ccb9"),h=n("6718"),v=n("47ee"),y=n("9003"),m=n("e4ae"),g=n("f772"),O=n("36c3"),w=n("1bc3"),_=n("aebd"),k=n("a159"),S=n("0395"),j=n("bf0b"),C=n("d9f6"),P=n("c3a1"),x=j.f,E=C.f,L=S.f,D=r.Symbol,I=r.JSON,N=I&&I.stringify,F="prototype",A=d("_hidden"),J=d("toPrimitive"),$={}.propertyIsEnumerable,K=u("symbol-registry"),T=u("symbols"),W=u("op-symbols"),B=Object[F],M="function"==typeof D,Y=r.QObject,z=!Y||!Y[F]||!Y[F].findChild,G=a&&f(function(){return 7!=k(E({},"a",{get:function(){return E(this,"a",{value:7}).a}})).a})?function(t,e,n){var r=x(B,e);r&&delete B[e],E(t,e,n),r&&t!==B&&E(B,e,r)}:E,Q=function(t){var e=T[t]=k(D[F]);return e._k=t,e},U=M&&"symbol"==typeof D.iterator?function(t){return"symbol"==typeof t}:function(t){return t instanceof D},q=function(t,e,n){return t===B&&q(W,e,n),m(t),e=w(e,!0),m(n),i(T,e)?(n.enumerable?(i(t,A)&&t[A][e]&&(t[A][e]=!1),n=k(n,{enumerable:_(0,!1)})):(i(t,A)||E(t,A,_(1,{})),t[A][e]=!0),G(t,e,n)):E(t,e,n)},H=function(t,e){m(t);var n,r=v(e=O(e)),i=0,a=r.length;while(a>i)q(t,n=r[i++],e[n]);return t},R=function(t,e){return void 0===e?k(t):H(k(t),e)},V=function(t){var e=$.call(this,t=w(t,!0));return!(this===B&&i(T,t)&&!i(W,t))&&(!(e||!i(this,t)||!i(T,t)||i(this,A)&&this[A][t])||e)},X=function(t,e){if(t=O(t),e=w(e,!0),t!==B||!i(T,e)||i(W,e)){var n=x(t,e);return!n||!i(T,e)||i(t,A)&&t[A][e]||(n.enumerable=!0),n}},Z=function(t){var e,n=L(O(t)),r=[],a=0;while(n.length>a)i(T,e=n[a++])||e==A||e==s||r.push(e);return r},tt=function(t){var e,n=t===B,r=L(n?W:O(t)),a=[],o=0;while(r.length>o)!i(T,e=r[o++])||n&&!i(B,e)||a.push(T[e]);return a};M||(D=function(){if(this instanceof D)throw TypeError("Symbol is not a constructor!");var t=b(arguments.length>0?arguments[0]:void 0),e=function(n){this===B&&e.call(W,n),i(this,A)&&i(this[A],t)&&(this[A][t]=!1),G(this,t,_(1,n))};return a&&z&&G(B,t,{configurable:!0,set:e}),Q(t)},c(D[F],"toString",function(){return this._k}),j.f=X,C.f=q,n("6abf").f=S.f=Z,n("355d").f=V,n("9aa9").f=tt,a&&!n("b8e3")&&c(B,"propertyIsEnumerable",V,!0),p.f=function(t){return Q(d(t))}),o(o.G+o.W+o.F*!M,{Symbol:D});for(var et="hasInstance,isConcatSpreadable,iterator,match,replace,search,species,split,toPrimitive,toStringTag,unscopables".split(","),nt=0;et.length>nt;)d(et[nt++]);for(var rt=P(d.store),it=0;rt.length>it;)h(rt[it++]);o(o.S+o.F*!M,"Symbol",{for:function(t){return i(K,t+="")?K[t]:K[t]=D(t)},keyFor:function(t){if(!U(t))throw TypeError(t+" is not a symbol!");for(var e in K)if(K[e]===t)return e},useSetter:function(){z=!0},useSimple:function(){z=!1}}),o(o.S+o.F*!M,"Object",{create:R,defineProperty:q,defineProperties:H,getOwnPropertyDescriptor:X,getOwnPropertyNames:Z,getOwnPropertySymbols:tt}),I&&o(o.S+o.F*(!M||f(function(){var t=D();return"[null]"!=N([t])||"{}"!=N({a:t})||"{}"!=N(Object(t))})),"JSON",{stringify:function(t){var e,n,r=[t],i=1;while(arguments.length>i)r.push(arguments[i++]);if(n=e=r[1],(g(e)||void 0!==t)&&!U(t))return y(e)||(e=function(t,e){if("function"==typeof n&&(e=n.call(this,t,e)),!U(e))return e}),r[1]=e,N.apply(I,r)}}),D[F][J]||n("35e8")(D[F],J,D[F].valueOf),l(D,"Symbol"),l(Math,"Math",!0),l(r.JSON,"JSON",!0)},"0395":function(t,e,n){var r=n("36c3"),i=n("6abf").f,a={}.toString,o="object"==typeof window&&window&&Object.getOwnPropertyNames?Object.getOwnPropertyNames(window):[],c=function(t){try{return i(t)}catch(e){return o.slice()}};t.exports.f=function(t){return o&&"[object Window]"==a.call(t)?c(t):i(r(t))}},"195c":function(t,e,n){"use strict";var r=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("div",{staticClass:"wrapper"},[n("div",{staticClass:"content"},[t._t("default")],2)])},i=[],a={name:"ContentPanel"},o=a,c=(n("32ec"),n("2877")),s=Object(c["a"])(o,r,i,!1,null,"ea29e130",null);s.options.__file="ContentPanel.vue";e["a"]=s.exports},"268f":function(t,e,n){t.exports=n("fde4")},"32a6":function(t,e,n){var r=n("241e"),i=n("c3a1");n("ce7e")("keys",function(){return function(t){return i(r(t))}})},"32ec":function(t,e,n){"use strict";var r=n("58e1"),i=n.n(r);i.a},"355d":function(t,e){e.f={}.propertyIsEnumerable},"454f":function(t,e,n){n("46a7");var r=n("584a").Object;t.exports=function(t,e,n){return r.defineProperty(t,e,n)}},"46a7":function(t,e,n){var r=n("63b6");r(r.S+r.F*!n("8e60"),"Object",{defineProperty:n("d9f6").f})},"47ee":function(t,e,n){var r=n("c3a1"),i=n("9aa9"),a=n("355d");t.exports=function(t){var e=r(t),n=i.f;if(n){var o,c=n(t),s=a.f,f=0;while(c.length>f)s.call(t,o=c[f++])&&e.push(o)}return e}},4958:function(t,e,n){},"4b61":function(t,e,n){"use strict";var r=n("4958"),i=n.n(r);i.a},"58e1":function(t,e,n){},6718:function(t,e,n){var r=n("e53d"),i=n("584a"),a=n("b8e3"),o=n("ccb9"),c=n("d9f6").f;t.exports=function(t){var e=i.Symbol||(i.Symbol=a?{}:r.Symbol||{});"_"==t.charAt(0)||t in e||c(e,t,{value:o.f(t)})}},"6abf":function(t,e,n){var r=n("e6f3"),i=n("1691").concat("length","prototype");e.f=Object.getOwnPropertyNames||function(t){return r(t,i)}},"85f2":function(t,e,n){t.exports=n("454f")},"8aae":function(t,e,n){n("32a6"),t.exports=n("584a").Object.keys},9003:function(t,e,n){var r=n("6b4c");t.exports=Array.isArray||function(t){return"Array"==r(t)}},9783:function(t,e,n){"use strict";n.r(e);var r=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("content-panel",[n("loader",{attrs:{"is-loading":t.loading}}),t.cat?n("div",[n("div",{staticClass:"heading"},[n("h1",{staticClass:"title"},[t._v(t._s(t.cat.name))]),n("likes-counter",{staticClass:"num-likes",attrs:{"num-likes":t.cat.totalLikes}})],1),n("dl",{staticClass:"info"},[t.cat.breed?n("dt",[t._v("Breed:")]):t._e(),t.cat.breed?n("dd",[t._v(t._s(t.cat.breed.title))]):t._e(),n("dt",[t._v("Characteristics:")]),n("dd",[t._v(t._s(t.formattedCharacteristics))]),n("dt",[t._v("Description:")]),n("dd",[t._v(t._s(t.cat.description))])]),n("div",{staticClass:"actions"},[t.member&&!t.isLiked?n("button",{staticClass:"btn-love",on:{click:t.handleLike}},[t._v("Like")]):t._e(),t.member&&t.isLiked?n("button",{staticClass:"btn-love",on:{click:t.handleLike}},[t._v("Un-like")]):t._e()]),n("div",{staticClass:"cat-images"},t._l(t.cat.images,function(t){return n("image-asset",{key:t.imageAssetId,attrs:{image:t,width:640,height:480}})}),1)]):t._e()],1)},i=[],a=n("268f"),o=n.n(a),c=n("e265"),s=n.n(c),f=n("a4bb"),u=n.n(f),l=n("85f2"),b=n.n(l);function d(t,e,n){return e in t?b()(t,e,{value:n,enumerable:!0,configurable:!0,writable:!0}):t[e]=n,t}function p(t){for(var e=1;e1?n("ul",t._l(t.errors,function(e){return n("li",{key:e.message},[t._v(t._s(e.message))])}),0):t._e()]):t._e()},s=[],a={name:"ValidationSummary",props:{errors:Array}},i=a,c=(n("d854"),n("2877")),o=Object(c["a"])(i,r,s,!1,null,"3d87f538",null);o.options.__file="ValidationSummary.vue";e["a"]=o.exports},c377:function(t,e,n){"use strict";var r=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("button",{staticClass:"btn",attrs:{type:"submit"}},[t._t("default",[t._v(t._s(t.title))])],2)},s=[],a={name:"SubmitButton",props:{title:String}},i=a,c=(n("dc10"),n("2877")),o=Object(c["a"])(i,r,s,!1,null,"167d0e48",null);o.options.__file="SubmitButton.vue";e["a"]=o.exports},c7cc:function(t,e,n){},d854:function(t,e,n){"use strict";var r=n("bd88"),s=n.n(r);s.a},dc10:function(t,e,n){"use strict";var r=n("c7cc"),s=n.n(r);s.a},ec11:function(t,e,n){"use strict";var r=n("a678"),s=n.n(r);s.a}}]); 2 | //# sourceMappingURL=Login~Register.c7c8d3ab.js.map -------------------------------------------------------------------------------- /src/Cofoundry.Samples.SPASite/ClientApp/dist/js/Register.99e7157e.js: -------------------------------------------------------------------------------- 1 | (window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["Register"],{"73cf":function(t,e,a){"use strict";a.r(e);var o=function(){var t=this,e=t.$createElement,a=t._self._c||e;return a("content-panel",[a("h1",[t._v("Register")]),t.registrationComplete?t._e():a("form",{on:{submit:function(e){return e.preventDefault(),t.submitRegistration(e)}}},[a("form-group",{attrs:{title:"Name",id:"inputName"}},[a("input",{directives:[{name:"model",rawName:"v-model",value:t.command.displayName,expression:"command.displayName"}],staticClass:"form-control",attrs:{type:"text",name:"displayName",id:"inputName",placeholder:"Name"},domProps:{value:t.command.displayName},on:{input:function(e){e.target.composing||t.$set(t.command,"displayName",e.target.value)}}})]),a("form-group",{attrs:{title:"Email address",id:"inputEmail"}},[a("input",{directives:[{name:"model",rawName:"v-model",value:t.command.email,expression:"command.email"}],staticClass:"form-control",attrs:{type:"email",name:"email",id:"inputEmail",placeholder:"Email"},domProps:{value:t.command.email},on:{input:function(e){e.target.composing||t.$set(t.command,"email",e.target.value)}}})]),a("form-group",{attrs:{title:"Password",id:"inputPassword"}},[a("input",{directives:[{name:"model",rawName:"v-model",value:t.command.password,expression:"command.password"}],staticClass:"form-control",attrs:{type:"password",name:"password",id:"inputPassword",placeholder:"Password"},domProps:{value:t.command.password},on:{input:function(e){e.target.composing||t.$set(t.command,"password",e.target.value)}}})]),a("validation-summary",{attrs:{errors:t.errors}}),a("form-actions",[a("submit-button",{attrs:{title:"Register"}})],1)],1),t.registrationComplete?a("div",[a("p",[t._v("Thank you for registering with SPA Cats! You now have access to extra features, such as favouriting the cats you love the most.")]),a("p",[t._v("Enjoy!")]),a("p",[a("router-link",{attrs:{to:"/"}},[t._v("View the cats")])],1)]):t._e()])},r=[],s=a("c06c"),i=a("405a"),n=a("195c"),m=a("56b7"),l=a("c377"),u={name:"registration",components:{ValidationSummary:s["a"],FormGroup:i["a"],ContentPanel:n["a"],FormActions:m["a"],SubmitButton:l["a"]},data:function(){return{registrationComplete:!1,command:{},errors:[]}},methods:{submitRegistration:function(){var t=this;function e(){t.registrationComplete=!0,t.errors=[]}function a(e){t.errors=e}this.$store.dispatch("auth/register",this.command).then(e).catch(a)}}},c=u,d=a("2877"),p=Object(d["a"])(c,o,r,!1,null,null,null);p.options.__file="Register.vue";e["default"]=p.exports}}]); 2 | //# sourceMappingURL=Register.99e7157e.js.map -------------------------------------------------------------------------------- /src/Cofoundry.Samples.SPASite/ClientApp/dist/js/app.4e13b28a.js: -------------------------------------------------------------------------------- 1 | (function(t){function e(e){for(var a,i,o=e[0],c=e[1],u=e[2],l=0,d=[];l0||this.height>0){var e=[];this.width>0&&e.push("width="+this.width),this.height>0&&e.push("height="+this.height),t+="?"+e.join("&")}return t}}}),s=r,o=n("2877"),c=Object(o["a"])(s,a,i,!1,null,null,null);c.options.__file="ImageAsset.vue";e["a"]=c.exports},"8eab":function(t,e,n){},"96b4":function(t,e,n){"use strict";var a=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("span",{staticClass:"likes"},[t._v(t._s(t.numLikes))])},i=[],r=(n("c5f6"),{name:"LikesCounter",props:{numLikes:Number}}),s=r,o=(n("cfaa"),n("2877")),c=Object(o["a"])(s,a,i,!1,null,"4b8de005",null);c.options.__file="LikesCounter.vue";e["a"]=c.exports},"984e":function(t,e,n){"use strict";var a=n("4409"),i=n.n(a);i.a},a31f:function(t,e,n){"use strict";var a=n("f85c"),i=n.n(a);i.a},a6d0:function(t,e,n){},bd58:function(t,e,n){"use strict";var a=n("8eab"),i=n.n(a);i.a},cf58:function(t,e,n){"use strict";var a=n("a6d0"),i=n.n(a);i.a},cfaa:function(t,e,n){"use strict";var a=n("0e47"),i=n.n(a);i.a},cfd4:function(t,e,n){"use strict";var a=n("e0ee"),i=n.n(a);i.a},e0ee:function(t,e,n){},f6cc:function(t,e,n){"use strict";var a=n("5c22"),i=n.n(a);i.a},f85c:function(t,e,n){}}); 2 | //# sourceMappingURL=app.4e13b28a.js.map -------------------------------------------------------------------------------- /src/Cofoundry.Samples.SPASite/ClientApp/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "spa-site", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "lint": "vue-cli-service lint" 9 | }, 10 | "dependencies": { 11 | "axios": "^0.18.0", 12 | "vue": "^2.5.21", 13 | "vue-router": "^3.0.1", 14 | "vuex": "^3.0.1" 15 | }, 16 | "devDependencies": { 17 | "@vue/cli-plugin-babel": "^3.3.0", 18 | "@vue/cli-plugin-eslint": "^3.3.0", 19 | "@vue/cli-service": "^3.3.0", 20 | "babel-eslint": "^10.0.1", 21 | "eslint": "^5.8.0", 22 | "eslint-plugin-vue": "^5.0.0", 23 | "node-sass": "^4.14.1", 24 | "sass-loader": "^7.0.1", 25 | "vue-template-compiler": "^2.5.21" 26 | }, 27 | "eslintConfig": { 28 | "root": true, 29 | "env": { 30 | "node": true 31 | }, 32 | "extends": [ 33 | "plugin:vue/essential", 34 | "eslint:recommended" 35 | ], 36 | "rules": {}, 37 | "parserOptions": { 38 | "parser": "babel-eslint" 39 | } 40 | }, 41 | "postcss": { 42 | "plugins": { 43 | "autoprefixer": {} 44 | } 45 | }, 46 | "browserslist": [ 47 | "> 1%", 48 | "last 2 versions", 49 | "not ie <= 8" 50 | ] 51 | } 52 | -------------------------------------------------------------------------------- /src/Cofoundry.Samples.SPASite/ClientApp/public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cofoundry-cms/Cofoundry.Samples.SPASite/ba0f8aaf565dc9b42a172ceac5b14f8aa06dd371/src/Cofoundry.Samples.SPASite/ClientApp/public/favicon.png -------------------------------------------------------------------------------- /src/Cofoundry.Samples.SPASite/ClientApp/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Cofoundry SPA Sample 9 | 10 | 11 | 12 | 15 | 16 |
17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/Cofoundry.Samples.SPASite/ClientApp/src/App.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 29 | 30 | 58 | -------------------------------------------------------------------------------- /src/Cofoundry.Samples.SPASite/ClientApp/src/api/auth.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import axiosHelper from '@/api/axiosHelper' 3 | 4 | const BASE_URI = '/api/auth/'; 5 | const XSRF_VERBS = ['post', 'put', 'patch', 'delete']; 6 | 7 | function mapSessionInfoResponse(response) { 8 | const sessionInfo = response.data.data; 9 | 10 | for (const verb of XSRF_VERBS) { 11 | axios.defaults.headers[verb]['RequestVerificationToken'] = sessionInfo.antiForgeryToken; 12 | } 13 | 14 | return sessionInfo.member; 15 | } 16 | 17 | export default { 18 | 19 | getSession() { 20 | return axios 21 | .get(BASE_URI + 'session') 22 | .then(mapSessionInfoResponse); 23 | }, 24 | 25 | login(command) { 26 | return axios 27 | .post(BASE_URI + 'login', command) 28 | .then(this.getSession) 29 | .catch(axiosHelper.handleCommandError); 30 | }, 31 | 32 | register(command) { 33 | return axios 34 | .post(BASE_URI + 'register', command) 35 | .then(this.getSession) 36 | .catch(axiosHelper.handleCommandError); 37 | }, 38 | 39 | signOut() { 40 | return axios 41 | .post(BASE_URI + 'sign-out') 42 | .then(this.getSession) 43 | .catch(axiosHelper.handleCommandError); 44 | } 45 | } -------------------------------------------------------------------------------- /src/Cofoundry.Samples.SPASite/ClientApp/src/api/axiosHelper.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 3 | handleCommandError(error) { 4 | const response = error.response; 5 | 6 | if (response.request.method !== 'get' && response.status === 400) { 7 | return Promise.reject(response.data.errors); 8 | } 9 | else { 10 | alert('An unhandled error has occured'); 11 | return Promise.reject(error); 12 | } 13 | }, 14 | 15 | handleQueryResponse(response) { 16 | return response.data.data; 17 | } 18 | } -------------------------------------------------------------------------------- /src/Cofoundry.Samples.SPASite/ClientApp/src/api/cats.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import axiosHelper from '@/api/axiosHelper' 3 | 4 | const BASE_URI = '/api/cats/'; 5 | 6 | export default { 7 | searchCats() { 8 | return axios 9 | .get(BASE_URI) 10 | .then(axiosHelper.handleQueryResponse); 11 | }, 12 | 13 | getCatById(id) { 14 | return axios 15 | .get(BASE_URI + id) 16 | .then(axiosHelper.handleQueryResponse); 17 | }, 18 | 19 | like(id) { 20 | return axios 21 | .post(BASE_URI + id + '/likes') 22 | .catch(axiosHelper.handleCommandError); 23 | }, 24 | 25 | unlike(id) { 26 | return axios 27 | .delete(BASE_URI + id + '/likes') 28 | .catch(axiosHelper.handleCommandError); 29 | } 30 | } -------------------------------------------------------------------------------- /src/Cofoundry.Samples.SPASite/ClientApp/src/api/currentMember.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import axiosHelper from '@/api/axiosHelper' 3 | 4 | const BASE_URI = '/api/members/current/'; 5 | 6 | export default { 7 | 8 | getLikedCats() { 9 | return axios 10 | .get(BASE_URI + 'cats/liked') 11 | .then(axiosHelper.handleQueryResponse); 12 | } 13 | } -------------------------------------------------------------------------------- /src/Cofoundry.Samples.SPASite/ClientApp/src/assets/cc-images.txt: -------------------------------------------------------------------------------- 1 | All images used under terms of creative commons. 2 | 3 | Original Images 4 | Jans Canon - https://www.flickr.com/photos/43158397@N02/4083241988 5 | Paul Sullivan - https://www.flickr.com/photos/pfsullivan_1056/8729452606 6 | Ollie Harridge - https://www.flickr.com/photos/olliethebastard/5439049117 7 | Ollie Harridge - https://www.flickr.com/photos/olliethebastard/5439051427 8 | ReflectedSerendipity - https://www.flickr.com/photos/sjdunphy/9294248537 9 | ReflectedSerendipity - https://www.flickr.com/photos/sjdunphy/9297024076 10 | http://www.pauldingcountyareafoundation.net/scottish-fold-cat-ddvudi5k0z5f.html -------------------------------------------------------------------------------- /src/Cofoundry.Samples.SPASite/ClientApp/src/assets/cofoundry-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cofoundry-cms/Cofoundry.Samples.SPASite/ba0f8aaf565dc9b42a172ceac5b14f8aa06dd371/src/Cofoundry.Samples.SPASite/ClientApp/src/assets/cofoundry-logo.png -------------------------------------------------------------------------------- /src/Cofoundry.Samples.SPASite/ClientApp/src/assets/heart-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cofoundry-cms/Cofoundry.Samples.SPASite/ba0f8aaf565dc9b42a172ceac5b14f8aa06dd371/src/Cofoundry.Samples.SPASite/ClientApp/src/assets/heart-icon.png -------------------------------------------------------------------------------- /src/Cofoundry.Samples.SPASite/ClientApp/src/assets/spacats-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cofoundry-cms/Cofoundry.Samples.SPASite/ba0f8aaf565dc9b42a172ceac5b14f8aa06dd371/src/Cofoundry.Samples.SPASite/ClientApp/src/assets/spacats-logo.png -------------------------------------------------------------------------------- /src/Cofoundry.Samples.SPASite/ClientApp/src/components/CatGrid.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 20 | 21 | 31 | -------------------------------------------------------------------------------- /src/Cofoundry.Samples.SPASite/ClientApp/src/components/CatItem.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 28 | 29 | 91 | -------------------------------------------------------------------------------- /src/Cofoundry.Samples.SPASite/ClientApp/src/components/ContentPanel.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 16 | 17 | 42 | -------------------------------------------------------------------------------- /src/Cofoundry.Samples.SPASite/ClientApp/src/components/FormActions.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 14 | 15 | 20 | -------------------------------------------------------------------------------- /src/Cofoundry.Samples.SPASite/ClientApp/src/components/FormGroup.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 18 | 19 | 38 | -------------------------------------------------------------------------------- /src/Cofoundry.Samples.SPASite/ClientApp/src/components/ImageAsset.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 31 | -------------------------------------------------------------------------------- /src/Cofoundry.Samples.SPASite/ClientApp/src/components/LikesCounter.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 13 | 14 | 32 | -------------------------------------------------------------------------------- /src/Cofoundry.Samples.SPASite/ClientApp/src/components/Loader.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 20 | 21 | 60 | -------------------------------------------------------------------------------- /src/Cofoundry.Samples.SPASite/ClientApp/src/components/SiteFooter.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 23 | 24 | 54 | -------------------------------------------------------------------------------- /src/Cofoundry.Samples.SPASite/ClientApp/src/components/SiteNav.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 33 | 34 | 104 | -------------------------------------------------------------------------------- /src/Cofoundry.Samples.SPASite/ClientApp/src/components/SubmitButton.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 14 | 15 | 37 | -------------------------------------------------------------------------------- /src/Cofoundry.Samples.SPASite/ClientApp/src/components/ValidationSummary.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 18 | 19 | 20 | 29 | -------------------------------------------------------------------------------- /src/Cofoundry.Samples.SPASite/ClientApp/src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import App from './App.vue'; 3 | import router from './router'; 4 | import store from './store'; 5 | 6 | new Vue({ 7 | router, 8 | store, 9 | render: h => h(App) 10 | }).$mount('#app'); 11 | -------------------------------------------------------------------------------- /src/Cofoundry.Samples.SPASite/ClientApp/src/router.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Router from 'vue-router' 3 | import Home from './views/Home.vue' 4 | 5 | Vue.use(Router) 6 | 7 | export default new Router({ 8 | mode: 'history', 9 | base: process.env.BASE_URL, 10 | routes: [{ 11 | path: '/', 12 | name: 'home', 13 | component: Home 14 | }, { 15 | path: '/cat/:id', 16 | name: 'catDetails', 17 | component: () => import(/* webpackChunkName: "CatDetails" */ './views/CatDetails.vue') 18 | }, { 19 | path: '/login', 20 | name: 'login', 21 | component: () => import(/* webpackChunkName: "Login" */ './views/Login.vue') 22 | }, { 23 | path: '/register', 24 | name: 'register', 25 | component: () => import(/* webpackChunkName: "Register" */ './views/Register.vue') 26 | }, { 27 | path: '*', 28 | name: 'NotFound', 29 | component: () => import(/* webpackChunkName: "404" */ './views/NotFound.vue') 30 | } ] 31 | }) 32 | -------------------------------------------------------------------------------- /src/Cofoundry.Samples.SPASite/ClientApp/src/scss/mixins.scss: -------------------------------------------------------------------------------- 1 | @mixin hide-text { 2 | text-indent: -100%; 3 | overflow:hidden; 4 | } 5 | 6 | // Adapted from http://jakearchibald.github.io/sass-ie/ 7 | $mq-support: true !default; 8 | $mq-max-width: 1400px; 9 | @mixin respond-min($width) { 10 | 11 | // Check for units on the width 12 | @if unitless($width){ 13 | @warn "Assuming #{$width} to be in pixels, change to #{$width}px or another unit of measurement"; 14 | $width: $width * 1px; 15 | } 16 | 17 | @if $mq-support { 18 | @media screen and (min-width: $width) { 19 | @content; 20 | } 21 | } 22 | @else { 23 | // Check media query applies 24 | @if (em($width) <= em($mq-max-width)) { 25 | @content; 26 | } 27 | } 28 | } 29 | 30 | @mixin respond-max($width) { 31 | 32 | // Check for units on the width 33 | @if unitless($width){ 34 | @warn "Assuming #{$width} to be in pixels, change to #{$width}px or another unit of measurement"; 35 | $width: $width * 1px; 36 | } 37 | 38 | // if this should apply like an element query, apply appropriate polyfill styles 39 | // @if $element-query { 40 | // // we use a wrapper with data attribute set with JS. 41 | // .eq-wrapper[data-min-width="#{$width}"] & { 42 | // @content; 43 | // } 44 | // } 45 | 46 | @if $mq-support { 47 | @media screen and (max-width: $width) { 48 | @content; 49 | } 50 | } 51 | @else { 52 | // Check media query applies 53 | @if (em($width) >= em($mq-max-width)) { 54 | @content; 55 | } 56 | } 57 | } -------------------------------------------------------------------------------- /src/Cofoundry.Samples.SPASite/ClientApp/src/scss/normalize.scss: -------------------------------------------------------------------------------- 1 | /*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */ 2 | 3 | /* Document 4 | ========================================================================== */ 5 | 6 | /** 7 | * 1. Correct the line height in all browsers. 8 | * 2. Prevent adjustments of font size after orientation changes in iOS. 9 | */ 10 | 11 | html { 12 | line-height: 1.15; /* 1 */ 13 | -webkit-text-size-adjust: 100%; /* 2 */ 14 | } 15 | 16 | /* Sections 17 | ========================================================================== */ 18 | 19 | /** 20 | * Remove the margin in all browsers. 21 | */ 22 | 23 | body { 24 | margin: 0; 25 | } 26 | 27 | /** 28 | * Render the `main` element consistently in IE. 29 | */ 30 | 31 | main { 32 | display: block; 33 | } 34 | 35 | /** 36 | * Correct the font size and margin on `h1` elements within `section` and 37 | * `article` contexts in Chrome, Firefox, and Safari. 38 | */ 39 | 40 | h1 { 41 | font-size: 2em; 42 | margin: 0.67em 0; 43 | } 44 | 45 | /* Grouping content 46 | ========================================================================== */ 47 | 48 | /** 49 | * 1. Add the correct box sizing in Firefox. 50 | * 2. Show the overflow in Edge and IE. 51 | */ 52 | 53 | hr { 54 | box-sizing: content-box; /* 1 */ 55 | height: 0; /* 1 */ 56 | overflow: visible; /* 2 */ 57 | } 58 | 59 | /** 60 | * 1. Correct the inheritance and scaling of font size in all browsers. 61 | * 2. Correct the odd `em` font sizing in all browsers. 62 | */ 63 | 64 | pre { 65 | font-family: monospace, monospace; /* 1 */ 66 | font-size: 1em; /* 2 */ 67 | } 68 | 69 | /* Text-level semantics 70 | ========================================================================== */ 71 | 72 | /** 73 | * Remove the gray background on active links in IE 10. 74 | */ 75 | 76 | a { 77 | background-color: transparent; 78 | } 79 | 80 | /** 81 | * 1. Remove the bottom border in Chrome 57- 82 | * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. 83 | */ 84 | 85 | abbr[title] { 86 | border-bottom: none; /* 1 */ 87 | text-decoration: underline; /* 2 */ 88 | text-decoration: underline dotted; /* 2 */ 89 | } 90 | 91 | /** 92 | * Add the correct font weight in Chrome, Edge, and Safari. 93 | */ 94 | 95 | b, 96 | strong { 97 | font-weight: bolder; 98 | } 99 | 100 | /** 101 | * 1. Correct the inheritance and scaling of font size in all browsers. 102 | * 2. Correct the odd `em` font sizing in all browsers. 103 | */ 104 | 105 | code, 106 | kbd, 107 | samp { 108 | font-family: monospace, monospace; /* 1 */ 109 | font-size: 1em; /* 2 */ 110 | } 111 | 112 | /** 113 | * Add the correct font size in all browsers. 114 | */ 115 | 116 | small { 117 | font-size: 80%; 118 | } 119 | 120 | /** 121 | * Prevent `sub` and `sup` elements from affecting the line height in 122 | * all browsers. 123 | */ 124 | 125 | sub, 126 | sup { 127 | font-size: 75%; 128 | line-height: 0; 129 | position: relative; 130 | vertical-align: baseline; 131 | } 132 | 133 | sub { 134 | bottom: -0.25em; 135 | } 136 | 137 | sup { 138 | top: -0.5em; 139 | } 140 | 141 | /* Embedded content 142 | ========================================================================== */ 143 | 144 | /** 145 | * Remove the border on images inside links in IE 10. 146 | */ 147 | 148 | img { 149 | border-style: none; 150 | } 151 | 152 | /* Forms 153 | ========================================================================== */ 154 | 155 | /** 156 | * 1. Change the font styles in all browsers. 157 | * 2. Remove the margin in Firefox and Safari. 158 | */ 159 | 160 | button, 161 | input, 162 | optgroup, 163 | select, 164 | textarea { 165 | font-family: inherit; /* 1 */ 166 | font-size: 100%; /* 1 */ 167 | line-height: 1.15; /* 1 */ 168 | margin: 0; /* 2 */ 169 | } 170 | 171 | /** 172 | * Show the overflow in IE. 173 | * 1. Show the overflow in Edge. 174 | */ 175 | 176 | button, 177 | input { /* 1 */ 178 | overflow: visible; 179 | } 180 | 181 | /** 182 | * Remove the inheritance of text transform in Edge, Firefox, and IE. 183 | * 1. Remove the inheritance of text transform in Firefox. 184 | */ 185 | 186 | button, 187 | select { /* 1 */ 188 | text-transform: none; 189 | } 190 | 191 | /** 192 | * Correct the inability to style clickable types in iOS and Safari. 193 | */ 194 | 195 | button, 196 | [type="button"], 197 | [type="reset"], 198 | [type="submit"] { 199 | -webkit-appearance: button; 200 | } 201 | 202 | /** 203 | * Remove the inner border and padding in Firefox. 204 | */ 205 | 206 | button::-moz-focus-inner, 207 | [type="button"]::-moz-focus-inner, 208 | [type="reset"]::-moz-focus-inner, 209 | [type="submit"]::-moz-focus-inner { 210 | border-style: none; 211 | padding: 0; 212 | } 213 | 214 | /** 215 | * Restore the focus styles unset by the previous rule. 216 | */ 217 | 218 | button:-moz-focusring, 219 | [type="button"]:-moz-focusring, 220 | [type="reset"]:-moz-focusring, 221 | [type="submit"]:-moz-focusring { 222 | outline: 1px dotted ButtonText; 223 | } 224 | 225 | /** 226 | * Correct the padding in Firefox. 227 | */ 228 | 229 | fieldset { 230 | padding: 0.35em 0.75em 0.625em; 231 | } 232 | 233 | /** 234 | * 1. Correct the text wrapping in Edge and IE. 235 | * 2. Correct the color inheritance from `fieldset` elements in IE. 236 | * 3. Remove the padding so developers are not caught out when they zero out 237 | * `fieldset` elements in all browsers. 238 | */ 239 | 240 | legend { 241 | box-sizing: border-box; /* 1 */ 242 | color: inherit; /* 2 */ 243 | display: table; /* 1 */ 244 | max-width: 100%; /* 1 */ 245 | padding: 0; /* 3 */ 246 | white-space: normal; /* 1 */ 247 | } 248 | 249 | /** 250 | * Add the correct vertical alignment in Chrome, Firefox, and Opera. 251 | */ 252 | 253 | progress { 254 | vertical-align: baseline; 255 | } 256 | 257 | /** 258 | * Remove the default vertical scrollbar in IE 10+. 259 | */ 260 | 261 | textarea { 262 | overflow: auto; 263 | } 264 | 265 | /** 266 | * 1. Add the correct box sizing in IE 10. 267 | * 2. Remove the padding in IE 10. 268 | */ 269 | 270 | [type="checkbox"], 271 | [type="radio"] { 272 | box-sizing: border-box; /* 1 */ 273 | padding: 0; /* 2 */ 274 | } 275 | 276 | /** 277 | * Correct the cursor style of increment and decrement buttons in Chrome. 278 | */ 279 | 280 | [type="number"]::-webkit-inner-spin-button, 281 | [type="number"]::-webkit-outer-spin-button { 282 | height: auto; 283 | } 284 | 285 | /** 286 | * 1. Correct the odd appearance in Chrome and Safari. 287 | * 2. Correct the outline style in Safari. 288 | */ 289 | 290 | [type="search"] { 291 | -webkit-appearance: textfield; /* 1 */ 292 | outline-offset: -2px; /* 2 */ 293 | } 294 | 295 | /** 296 | * Remove the inner padding in Chrome and Safari on macOS. 297 | */ 298 | 299 | [type="search"]::-webkit-search-decoration { 300 | -webkit-appearance: none; 301 | } 302 | 303 | /** 304 | * 1. Correct the inability to style clickable types in iOS and Safari. 305 | * 2. Change font properties to `inherit` in Safari. 306 | */ 307 | 308 | ::-webkit-file-upload-button { 309 | -webkit-appearance: button; /* 1 */ 310 | font: inherit; /* 2 */ 311 | } 312 | 313 | /* Interactive 314 | ========================================================================== */ 315 | 316 | /* 317 | * Add the correct display in Edge, IE 10+, and Firefox. 318 | */ 319 | 320 | details { 321 | display: block; 322 | } 323 | 324 | /* 325 | * Add the correct display in all browsers. 326 | */ 327 | 328 | summary { 329 | display: list-item; 330 | } 331 | 332 | /* Misc 333 | ========================================================================== */ 334 | 335 | /** 336 | * Add the correct display in IE 10+. 337 | */ 338 | 339 | template { 340 | display: none; 341 | } 342 | 343 | /** 344 | * Add the correct display in IE 10. 345 | */ 346 | 347 | [hidden] { 348 | display: none; 349 | } -------------------------------------------------------------------------------- /src/Cofoundry.Samples.SPASite/ClientApp/src/scss/variables.scss: -------------------------------------------------------------------------------- 1 | // colors 2 | $color-primary: #2C2233; 3 | $color-secondary: #5E0042; 4 | $color-tertiary: #005869; 5 | 6 | $color-highlight: #00856A; 7 | $color-lowlight: #8DB500; 8 | 9 | $color-text: #000; 10 | 11 | // dimensions 12 | $tablet: 768px; 13 | 14 | $layout-max-width: 1260px; -------------------------------------------------------------------------------- /src/Cofoundry.Samples.SPASite/ClientApp/src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Vuex from 'vuex'; 3 | import cats from './modules/cats'; 4 | import auth from './modules/auth'; 5 | 6 | Vue.use(Vuex); 7 | 8 | export default new Vuex.Store({ 9 | modules: { 10 | cats, 11 | auth 12 | } 13 | }); 14 | -------------------------------------------------------------------------------- /src/Cofoundry.Samples.SPASite/ClientApp/src/store/modules/auth.js: -------------------------------------------------------------------------------- 1 | import authApi from '@/api/auth'; 2 | 3 | function loadAdditionalSessionData(context) { 4 | return context.dispatch('cats/loadSession', null, { root: true }); 5 | } 6 | 7 | export default { 8 | namespaced: true, 9 | 10 | state: { 11 | member: null 12 | }, 13 | 14 | mutations: { 15 | setMember(state, member) { 16 | state.member = member; 17 | } 18 | }, 19 | 20 | actions: { 21 | loadSession(context) { 22 | loadAdditionalSessionData(context); 23 | 24 | return authApi 25 | .getSession() 26 | .then(member => { 27 | context.commit('setMember', member); 28 | }); 29 | }, 30 | 31 | register(context, command) { 32 | return authApi 33 | .register(command) 34 | .then(member => { 35 | loadAdditionalSessionData(context) 36 | .then(() => { 37 | context.commit('setMember', member); 38 | }); 39 | }) 40 | }, 41 | 42 | login(context, command) { 43 | return authApi 44 | .login(command) 45 | .then(member => { 46 | loadAdditionalSessionData(context) 47 | .then(() => { 48 | context.commit('setMember', member); 49 | }); 50 | }) 51 | }, 52 | 53 | signOut(context) { 54 | return authApi 55 | .signOut() 56 | .then(member => { 57 | context.dispatch('cats/clearSession', null, { root: true }) 58 | .then(() => { 59 | context.commit('setMember', member); 60 | }); 61 | }) 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Cofoundry.Samples.SPASite/ClientApp/src/store/modules/cats.js: -------------------------------------------------------------------------------- 1 | import currentMemberApi from '@/api/currentMember'; 2 | import catsApi from '@/api/cats'; 3 | 4 | export default { 5 | namespaced: true, 6 | state: { 7 | likedCatIds: [] 8 | }, 9 | mutations: { 10 | setLikedCatIds(state, catIds) { 11 | state.likedCatIds = catIds; 12 | }, 13 | setCatLiked(state, catId) { 14 | state.likedCatIds.push(catId); 15 | }, 16 | setCatUnliked(state, catId) { 17 | state.likedCatIds = state.likedCatIds.filter(id => id !== catId); 18 | } 19 | }, 20 | actions: { 21 | loadSession(context) { 22 | return currentMemberApi.getLikedCats() 23 | .then(cats => { 24 | const catIds = cats ? cats.map(i => i.catId) : []; 25 | context.commit('setLikedCatIds', catIds); 26 | }); 27 | }, 28 | 29 | clearSession(context) { 30 | context.commit('setLikedCatIds', []); 31 | 32 | return Promise.resolve(); 33 | }, 34 | 35 | like(context, catId) { 36 | if (context.state.likedCatIds.indexOf(catId) !== -1) return Promise.resolve(); 37 | 38 | return catsApi.like(catId).then(() => { 39 | context.commit('setCatLiked', catId); 40 | }); 41 | }, 42 | 43 | unlike(context, catId) { 44 | if (context.state.likedCatIds.indexOf(catId) === -1) return Promise.resolve(); 45 | 46 | return catsApi.unlike(catId).then(() => { 47 | context.commit('setCatUnliked', catId); 48 | }); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Cofoundry.Samples.SPASite/ClientApp/src/views/CatDetails.vue: -------------------------------------------------------------------------------- 1 | 41 | 42 | 100 | 101 | -------------------------------------------------------------------------------- /src/Cofoundry.Samples.SPASite/ClientApp/src/views/Home.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 56 | 57 | 98 | -------------------------------------------------------------------------------- /src/Cofoundry.Samples.SPASite/ClientApp/src/views/Login.vue: -------------------------------------------------------------------------------- 1 | 44 | 45 | 89 | 90 | 93 | -------------------------------------------------------------------------------- /src/Cofoundry.Samples.SPASite/ClientApp/src/views/NotFound.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 15 | -------------------------------------------------------------------------------- /src/Cofoundry.Samples.SPASite/ClientApp/src/views/Register.vue: -------------------------------------------------------------------------------- 1 | 56 | 57 | 101 | -------------------------------------------------------------------------------- /src/Cofoundry.Samples.SPASite/ClientApp/vue.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | css: { 3 | loaderOptions: { 4 | sass: { 5 | data: ` 6 | @import "@/scss/mixins.scss"; 7 | @import "@/scss/variables.scss"; 8 | @import "@/scss/normalize.scss"; 9 | ` 10 | } 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Cofoundry.Samples.SPASite/Cofoundry.Samples.SPASite.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | false 8 | false 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/Cofoundry.Samples.SPASite/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.SpaServices; 2 | using VueCliMiddleware; 3 | 4 | var builder = WebApplication.CreateBuilder(args); 5 | 6 | builder.Services.AddSpaStaticFiles(configuration => 7 | { 8 | configuration.RootPath = "ClientApp/dist"; 9 | }); 10 | 11 | builder.Services 12 | .AddControllersWithViews() 13 | .AddCofoundry(builder.Configuration); 14 | 15 | var app = builder.Build(); 16 | 17 | app.UseHttpsRedirection(); 18 | app.UseSpaStaticFiles(); 19 | app.UseCofoundry(); 20 | 21 | // Un-comment this to run the vue cli automatically when debugging 22 | // You'll need to install the vue cli, see https://cli.vuejs.org/guide/installation.html 23 | app.MapToVueCliProxy( 24 | "{*path}", 25 | new SpaOptions { SourcePath = "ClientApp" }, 26 | npmScript: null, //(System.Diagnostics.Debugger.IsAttached) ? "serve" : null, 27 | regex: "Compiled successfully", 28 | forceKill: true 29 | ); 30 | 31 | app.Run(); 32 | -------------------------------------------------------------------------------- /src/Cofoundry.Samples.SPASite/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:58139/", 7 | "sslPort": 0 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": true, 14 | "environmentVariables": { 15 | "ASPNETCORE_ENVIRONMENT": "Development" 16 | } 17 | }, 18 | "Cofoundry.Samples.SPASite": { 19 | "commandName": "Project", 20 | "launchBrowser": true, 21 | "environmentVariables": { 22 | "ASPNETCORE_ENVIRONMENT": "Development" 23 | }, 24 | "applicationUrl": "http://localhost:58140" 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Cofoundry.Samples.SPASite/Usings.cs: -------------------------------------------------------------------------------- 1 | global using Cofoundry.Domain; 2 | global using Cofoundry.Samples.SPASite.Domain; 3 | global using Cofoundry.Web; 4 | global using Microsoft.AspNetCore.Mvc; 5 | -------------------------------------------------------------------------------- /src/Cofoundry.Samples.SPASite/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "DetailedErrors": true, 3 | "Logging": { 4 | "LogLevel": { 5 | "Default": "Information", 6 | "Microsoft.AspNetCore": "Warning" 7 | } 8 | } 9 | } -------------------------------------------------------------------------------- /src/Cofoundry.Samples.SPASite/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | }, 8 | "AllowedHosts": "*", 9 | 10 | "Cofoundry": { 11 | 12 | "Database": { 13 | "ConnectionString": "Server=.\\sqlexpress;Database=Cofoundry.Samples.SPASite;Integrated Security=True;Encrypt=False;MultipleActiveResultSets=True" 14 | }, 15 | 16 | "Mail": { 17 | "DefaultFromAddress": "auto@example.com" 18 | }, 19 | 20 | "DocumentAssets:Disabled": true, 21 | "Pages:Disabled": true 22 | } 23 | } 24 | --------------------------------------------------------------------------------