├── .dockerignore ├── .gitattributes ├── .github └── stale.yml ├── .gitignore ├── LICENSE ├── PreviewImage_JavaScript.png ├── PreviewImage_TypeScript.png ├── RCB.JavaScript ├── .babelrc ├── .dockerignore ├── AppSettings.cs ├── ClientApp │ ├── boot-client.jsx │ ├── boot-server.jsx │ ├── components │ │ ├── person │ │ │ └── PersonEditor.jsx │ │ └── shared │ │ │ ├── AppRoute.jsx │ │ │ ├── Footer.jsx │ │ │ ├── FormValidator.jsx │ │ │ ├── Paginator.jsx │ │ │ └── TopMenu.jsx │ ├── core │ │ ├── Result.js │ │ ├── ServiceBase.js │ │ ├── responseContext.js │ │ └── session │ │ │ └── index.js │ ├── images │ │ └── logo.png │ ├── layouts │ │ ├── AuthorizedLayout.jsx │ │ └── GuestLayout.jsx │ ├── pages │ │ ├── ExamplesPage.jsx │ │ ├── HomePage.jsx │ │ ├── LoginPage.jsx │ │ └── NotFoundPage.jsx │ ├── routes.jsx │ ├── services │ │ ├── AccountService.js │ │ └── PersonService.js │ ├── store │ │ ├── configureStore.js │ │ ├── index.js │ │ ├── loginStore.js │ │ └── personStore.js │ ├── styles │ │ ├── authorizedLayout.scss │ │ ├── guestLayout.scss │ │ ├── loaders │ │ │ ├── applicationLoader.css │ │ │ └── queryLoader.scss │ │ └── main.scss │ └── utils.js ├── Constants.cs ├── Controllers │ ├── AccountController.cs │ ├── ControllerBase.cs │ ├── MainController.cs │ └── PersonController.cs ├── Dockerfile ├── Extensions │ └── NodeServicesExtensions.cs ├── Infrastructure │ ├── ExceptionMiddleware.cs │ ├── Result.cs │ ├── SerilogMvcLoggingAttribute.cs │ ├── ServiceBase.cs │ ├── ServiceUser.cs │ └── WebSessionModels.cs ├── Models │ ├── LoginModel.cs │ └── PersonModel.cs ├── Program.cs ├── Properties │ └── launchSettings.json ├── RCB.JavaScript.csproj ├── README.FIRSTRUN.txt ├── Services │ ├── AccountService.cs │ └── PersonService.cs ├── Startup.cs ├── Views │ ├── Main │ │ └── Index.cshtml │ ├── Shared │ │ └── Error.cshtml │ ├── _ViewImports.cshtml │ └── _ViewStart.cshtml ├── appsettings.Development.json ├── appsettings.Production.json ├── appsettings.json ├── build.before.js ├── download.js ├── hosting.Production.json ├── jsconfig.json ├── package-lock.json ├── package.json ├── webpack.config.js ├── webpack.config.vendor.js └── wwwroot │ └── favicon.ico ├── RCB.TypeScript ├── .dockerignore ├── AppSettings.cs ├── ClientApp │ ├── boot-client.tsx │ ├── boot-server.tsx │ ├── components │ │ ├── person │ │ │ └── PersonEditor.tsx │ │ └── shared │ │ │ ├── AppRoute.tsx │ │ │ ├── Footer.tsx │ │ │ ├── FormValidator.tsx │ │ │ ├── Paginator.tsx │ │ │ └── TopMenu.tsx │ ├── core │ │ ├── Result.ts │ │ ├── ServiceBase.ts │ │ ├── responseContext.ts │ │ └── session │ │ │ ├── index.ts │ │ │ └── models.ts │ ├── global.d.ts │ ├── images │ │ └── logo.png │ ├── layouts │ │ ├── AuthorizedLayout.tsx │ │ └── GuestLayout.tsx │ ├── models │ │ ├── ILoginModel.ts │ │ └── IPersonModel.ts │ ├── pages │ │ ├── ExamplesPage.tsx │ │ ├── HomePage.tsx │ │ ├── LoginPage.tsx │ │ └── NotFoundPage.tsx │ ├── routes.tsx │ ├── services │ │ ├── AccountService.ts │ │ └── PersonService.ts │ ├── store │ │ ├── configureStore.ts │ │ ├── index.ts │ │ ├── loginStore.ts │ │ └── personStore.ts │ ├── styles │ │ ├── authorizedLayout.scss │ │ ├── guestLayout.scss │ │ ├── loaders │ │ │ ├── applicationLoader.css │ │ │ └── queryLoader.scss │ │ └── main.scss │ └── utils.ts ├── Constants.cs ├── Controllers │ ├── AccountController.cs │ ├── ControllerBase.cs │ ├── MainController.cs │ └── PersonController.cs ├── Dockerfile ├── Extensions │ └── NodeServicesExtensions.cs ├── Infrastructure │ ├── ExceptionMiddleware.cs │ ├── Result.cs │ ├── SerilogMvcLoggingAttribute.cs │ ├── ServiceBase.cs │ ├── ServiceUser.cs │ └── WebSessionModels.cs ├── Models │ ├── LoginModel.cs │ └── PersonModel.cs ├── Program.cs ├── Properties │ └── launchSettings.json ├── RCB.TypeScript.csproj ├── README.FIRSTRUN.txt ├── Services │ ├── AccountService.cs │ └── PersonService.cs ├── Startup.cs ├── Views │ ├── Main │ │ └── Index.cshtml │ ├── Shared │ │ └── Error.cshtml │ ├── _ViewImports.cshtml │ └── _ViewStart.cshtml ├── appsettings.Development.json ├── appsettings.Production.json ├── appsettings.json ├── build.before.js ├── download.js ├── hosting.Production.json ├── package-lock.json ├── package.json ├── tsconfig.json ├── webpack.config.js ├── webpack.config.vendor.js └── wwwroot │ └── favicon.ico ├── RCB.sln ├── README.md ├── TemplateIcon.png └── azure-pipelines.yml /.dockerignore: -------------------------------------------------------------------------------- 1 | **/.classpath 2 | **/.dockerignore 3 | **/.env 4 | **/.git 5 | **/.gitignore 6 | **/.project 7 | **/.settings 8 | **/.toolstarget 9 | **/.vs 10 | **/.vscode 11 | **/*.*proj.user 12 | **/*.dbmdl 13 | **/*.jfm 14 | **/azds.yaml 15 | **/bin 16 | **/charts 17 | **/docker-compose* 18 | **/Dockerfile* 19 | **/node_modules 20 | **/npm-debug.log 21 | **/obj 22 | **/secrets.dev.yaml 23 | **/values.dev.yaml 24 | LICENSE 25 | README.md -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 60 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 7 5 | # Issues with these labels will never be considered stale 6 | exemptLabels: 7 | - bug 8 | - feature request 9 | - pinned 10 | - security 11 | - help wanted 12 | # Label to use when marking an issue as stale 13 | staleLabel: wontfix 14 | # Comment to post when marking an issue as stale. Set to `false` to disable 15 | markComment: > 16 | This issue has been automatically marked as stale because it has not had 17 | recent activity. It will be closed if no further activity occurs. Thank you 18 | for your contributions. 19 | # Comment to post when closing a stale issue. Set to `false` to disable 20 | closeComment: true -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /Properties/launchSettings.json 2 | 3 | ## Ignore Visual Studio temporary files, build results, and 4 | ## files generated by popular Visual Studio add-ons. 5 | 6 | # User-specific files 7 | *.suo 8 | *.user 9 | *.userosscache 10 | *.sln.docstates 11 | 12 | # User-specific files (MonoDevelop/Xamarin Studio) 13 | *.userprefs 14 | 15 | # Build results 16 | [Dd]ebug/ 17 | [Dd]ebugPublic/ 18 | [Rr]elease/ 19 | [Rr]eleases/ 20 | x64/ 21 | x86/ 22 | build/ 23 | bld/ 24 | bin/ 25 | Bin/ 26 | obj/ 27 | Obj/ 28 | logs/ 29 | Logs/ 30 | dist/ 31 | 32 | # Visual Studio 2015 cache/options directory 33 | .vs/ 34 | /wwwroot/dist/ 35 | /ClientApp/dist/ 36 | 37 | /yarn.lock 38 | 39 | # MSTest test Results 40 | [Tt]est[Rr]esult*/ 41 | [Bb]uild[Ll]og.* 42 | 43 | # NUNIT 44 | *.VisualState.xml 45 | TestResult.xml 46 | 47 | # Build Results of an ATL Project 48 | [Dd]ebugPS/ 49 | [Rr]eleasePS/ 50 | dlldata.c 51 | 52 | *_i.c 53 | *_p.c 54 | *_i.h 55 | *.ilk 56 | *.meta 57 | *.obj 58 | *.pch 59 | *.pdb 60 | *.pgc 61 | *.pgd 62 | *.rsp 63 | *.sbr 64 | *.tlb 65 | *.tli 66 | *.tlh 67 | *.tmp 68 | *.tmp_proj 69 | *.log 70 | *.vspscc 71 | *.vssscc 72 | .builds 73 | *.pidb 74 | *.svclog 75 | *.scc 76 | 77 | # Chutzpah Test files 78 | _Chutzpah* 79 | 80 | # Visual C++ cache files 81 | ipch/ 82 | *.aps 83 | *.ncb 84 | *.opendb 85 | *.opensdf 86 | *.sdf 87 | *.cachefile 88 | 89 | # Visual Studio profiler 90 | *.psess 91 | *.vsp 92 | *.vspx 93 | *.sap 94 | 95 | # TFS 2012 Local Workspace 96 | $tf/ 97 | 98 | # Guidance Automation Toolkit 99 | *.gpState 100 | 101 | # ReSharper is a .NET coding add-in 102 | _ReSharper*/ 103 | *.[Rr]e[Ss]harper 104 | *.DotSettings.user 105 | 106 | # JustCode is a .NET coding add-in 107 | .JustCode 108 | 109 | # TeamCity is a build add-in 110 | _TeamCity* 111 | 112 | # DotCover is a Code Coverage Tool 113 | *.dotCover 114 | 115 | # NCrunch 116 | _NCrunch_* 117 | .*crunch*.local.xml 118 | nCrunchTemp_* 119 | 120 | # MightyMoose 121 | *.mm.* 122 | AutoTest.Net/ 123 | 124 | # Web workbench (sass) 125 | .sass-cache/ 126 | 127 | # Installshield output folder 128 | [Ee]xpress/ 129 | 130 | # DocProject is a documentation generator add-in 131 | DocProject/buildhelp/ 132 | DocProject/Help/*.HxT 133 | DocProject/Help/*.HxC 134 | DocProject/Help/*.hhc 135 | DocProject/Help/*.hhk 136 | DocProject/Help/*.hhp 137 | DocProject/Help/Html2 138 | DocProject/Help/html 139 | 140 | # Click-Once directory 141 | publish/ 142 | 143 | # Publish Web Output 144 | *.[Pp]ublish.xml 145 | *.azurePubxml 146 | # TODO: Comment the next line if you want to checkin your web deploy settings 147 | # but database connection strings (with potential passwords) will be unencrypted 148 | *.pubxml 149 | *.publishproj 150 | 151 | # NuGet Packages 152 | *.nupkg 153 | # The packages folder can be ignored because of Package Restore 154 | **/packages/* 155 | # except build/, which is used as an MSBuild target. 156 | !**/packages/build/ 157 | # Uncomment if necessary however generally it will be regenerated when needed 158 | #!**/packages/repositories.config 159 | 160 | # Microsoft Azure Build Output 161 | csx/ 162 | *.build.csdef 163 | 164 | # Microsoft Azure Emulator 165 | ecf/ 166 | rcf/ 167 | 168 | # Microsoft Azure ApplicationInsights config file 169 | ApplicationInsights.config 170 | 171 | # Windows Store app package directory 172 | AppPackages/ 173 | BundleArtifacts/ 174 | 175 | # Visual Studio cache files 176 | # files ending in .cache can be ignored 177 | *.[Cc]ache 178 | # but keep track of directories ending in .cache 179 | !*.[Cc]ache/ 180 | 181 | # Others 182 | ClientBin/ 183 | ~$* 184 | *~ 185 | *.dbmdl 186 | *.dbproj.schemaview 187 | *.pfx 188 | *.publishsettings 189 | orleans.codegen.cs 190 | 191 | /node_modules 192 | 193 | # RIA/Silverlight projects 194 | Generated_Code/ 195 | 196 | # Backup & report files from converting an old project file 197 | # to a newer Visual Studio version. Backup files are not needed, 198 | # because we have git ;-) 199 | _UpgradeReport_Files/ 200 | Backup*/ 201 | UpgradeLog*.XML 202 | UpgradeLog*.htm 203 | 204 | # SQL Server files 205 | *.mdf 206 | *.ldf 207 | 208 | # Business Intelligence projects 209 | *.rdl.data 210 | *.bim.layout 211 | *.bim_*.settings 212 | 213 | # Microsoft Fakes 214 | FakesAssemblies/ 215 | 216 | # GhostDoc plugin setting file 217 | *.GhostDoc.xml 218 | 219 | # Node.js Tools for Visual Studio 220 | .ntvs_analysis.dat 221 | 222 | # Visual Studio 6 build log 223 | *.plg 224 | 225 | # Visual Studio 6 workspace options file 226 | *.opt 227 | 228 | # Visual Studio LightSwitch build output 229 | **/*.HTMLClient/GeneratedArtifacts 230 | **/*.DesktopClient/GeneratedArtifacts 231 | **/*.DesktopClient/ModelManifest.xml 232 | **/*.Server/GeneratedArtifacts 233 | **/*.Server/ModelManifest.xml 234 | _Pvt_Extensions 235 | 236 | # Paket dependency manager 237 | .paket/paket.exe 238 | 239 | # FAKE - F# Make 240 | .fake/ 241 | 242 | node_modules/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Nikolay Maev 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 | -------------------------------------------------------------------------------- /PreviewImage_JavaScript.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NickMaev/react-core-boilerplate/6f6e2eedd82dc8454bde543364557eb00413273e/PreviewImage_JavaScript.png -------------------------------------------------------------------------------- /PreviewImage_TypeScript.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NickMaev/react-core-boilerplate/6f6e2eedd82dc8454bde543364557eb00413273e/PreviewImage_TypeScript.png -------------------------------------------------------------------------------- /RCB.JavaScript/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "compact": true, 3 | "presets": [ "@babel/preset-env", "@babel/preset-react" ], 4 | "plugins": [ 5 | [ 6 | "@babel/plugin-transform-runtime", 7 | { 8 | "absoluteRuntime": false, 9 | "corejs": false, 10 | "helpers": true, 11 | "regenerator": true, 12 | "useESModules": false 13 | } 14 | ], 15 | [ 16 | "@babel/plugin-proposal-decorators", 17 | { "legacy": true } 18 | ], 19 | [ 20 | "@babel/plugin-proposal-class-properties", 21 | { "loose": true } 22 | ], 23 | [ "@babel/plugin-proposal-optional-catch-binding" ] 24 | ] 25 | } -------------------------------------------------------------------------------- /RCB.JavaScript/.dockerignore: -------------------------------------------------------------------------------- 1 | [B|b]in 2 | [O|o]bj -------------------------------------------------------------------------------- /RCB.JavaScript/AppSettings.cs: -------------------------------------------------------------------------------- 1 | namespace RCB.JavaScript 2 | { 3 | public class AppSettings 4 | { 5 | public static AppSettings Default { get; } 6 | 7 | protected AppSettings() 8 | { 9 | } 10 | 11 | static AppSettings() 12 | { 13 | Default = new AppSettings(); 14 | } 15 | 16 | public bool IsDevelopment => Program.EnvironmentName == "Development"; 17 | } 18 | } -------------------------------------------------------------------------------- /RCB.JavaScript/ClientApp/boot-client.jsx: -------------------------------------------------------------------------------- 1 | // Import polyfills. 2 | import "core-js/stable"; 3 | import "custom-event-polyfill"; 4 | import "event-source-polyfill"; 5 | 6 | // Import global styles. 7 | import "bootstrap/dist/css/bootstrap.min.css"; 8 | import "@Styles/main.scss"; 9 | import "@Styles/loaders/queryLoader.scss"; 10 | import "react-toastify/dist/ReactToastify.css"; 11 | 12 | // Other imports. 13 | import * as React from "react"; 14 | import * as ReactDOM from "react-dom"; 15 | import configureStore from "@Store/configureStore"; 16 | import SessionManager from "@Core/session"; 17 | import { AppContainer } from "react-hot-loader"; 18 | import { Provider } from "react-redux"; 19 | import { ConnectedRouter } from "connected-react-router"; 20 | import { createBrowserHistory } from "history"; 21 | import { isNode, showApplicationLoader, hideApplicationLoader } from "@Utils"; 22 | import * as RoutesModule from "./routes"; 23 | let routes = RoutesModule.routes; 24 | 25 | function setupSession() { 26 | if (!isNode()) { 27 | SessionManager.resetSession(); 28 | SessionManager.initSession({ 29 | isomorphic: window["session"], 30 | ssr: {} 31 | }); 32 | } 33 | }; 34 | 35 | function setupGlobalPlugins() { 36 | // Use this function to configure plugins on the client side. 37 | }; 38 | 39 | function setupEvents() { 40 | 41 | showApplicationLoader(); 42 | 43 | document.addEventListener("DOMContentLoaded", () => { 44 | hideApplicationLoader(); 45 | }); 46 | }; 47 | 48 | function getBaseUrl() { 49 | var elements = document.getElementsByTagName('base'); 50 | if (elements.length === 0) { 51 | return null; 52 | } 53 | return elements[0].getAttribute('href'); 54 | } 55 | 56 | // Create browser history to use in the Redux store. 57 | const baseUrl = getBaseUrl(); 58 | const history = createBrowserHistory({ basename: baseUrl }); 59 | 60 | // Get the application-wide store instance, prepopulating with state from the server where available. 61 | const initialState = window.initialReduxState; 62 | const store = configureStore(history, initialState); 63 | 64 | function renderApp() { 65 | // This code starts up the React app when it runs in a browser. 66 | // It sets up the routing configuration and injects the app into a DOM element. 67 | ReactDOM.hydrate( 68 | 69 | 70 | 71 | 72 | , 73 | document.getElementById("react-app") 74 | ); 75 | } 76 | 77 | // Setup the application and render it. 78 | setupSession(); 79 | setupGlobalPlugins(); 80 | setupEvents(); 81 | renderApp(); 82 | 83 | // Allow Hot Module Replacement. 84 | if (module.hot) { 85 | module.hot.accept("./routes", () => { 86 | routes = require("./routes").routes; 87 | renderApp(); 88 | }); 89 | } -------------------------------------------------------------------------------- /RCB.JavaScript/ClientApp/boot-server.jsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import SessionManager from "@Core/session"; 3 | import configureStore from "@Store/configureStore"; 4 | import { createServerRenderer } from "aspnet-prerendering"; 5 | import { replace } from "connected-react-router"; 6 | import { addDomainWait, getCompletedTasks } from "domain-wait"; 7 | import { createMemoryHistory } from "history"; 8 | import { renderToString } from "react-dom/server"; 9 | import { Helmet } from "react-helmet"; 10 | import { Provider } from "react-redux"; 11 | import { StaticRouter } from "react-router-dom"; 12 | import serializeJavascript from "serialize-javascript"; 13 | import { routes } from "./routes"; 14 | import responseContext from "@Core/responseContext"; 15 | 16 | var renderHelmet = () => { 17 | var helmetData = Helmet.renderStatic(); 18 | var helmetStrings = ""; 19 | for (var key in helmetData) { 20 | if (helmetData.hasOwnProperty(key)) { 21 | helmetStrings += helmetData[key].toString(); 22 | } 23 | } 24 | return helmetStrings; 25 | }; 26 | 27 | var createGlobals = (session, initialReduxState, helmetStrings) => { 28 | return { 29 | completedTasks: getCompletedTasks(), 30 | session, 31 | 32 | // Serialize Redux State with "serialize-javascript" library 33 | // prevents XSS atack in the path of React Router via browser. 34 | initialReduxState: serializeJavascript(initialReduxState, { isJSON: true }), 35 | helmetStrings 36 | }; 37 | }; 38 | 39 | export default createServerRenderer((params) => { 40 | 41 | SessionManager.resetSession(); 42 | SessionManager.initSession(params.data); 43 | 44 | return new Promise((resolve, reject) => { 45 | 46 | // Prepare Redux store with in-memory history, and dispatch a navigation event. 47 | // corresponding to the incoming URL. 48 | const basename = params.baseUrl.substring(0, params.baseUrl.length - 1); // Remove trailing slash. 49 | const urlAfterBasename = params.url.substring(basename.length); 50 | const store = configureStore(createMemoryHistory()); 51 | store.dispatch(replace(urlAfterBasename)); 52 | 53 | // Prepare an instance of the application and perform an inital render that will 54 | // cause any async tasks (e.g., data access) to begin. 55 | const routerContext = {}; 56 | const app = ( 57 | 58 | 59 | 60 | ); 61 | 62 | const renderApp = () => { 63 | return renderToString(app); 64 | }; 65 | 66 | addDomainWait(params); 67 | 68 | renderApp(); 69 | 70 | // If there's a redirection, just send this information back to the host application. 71 | if (routerContext.url) { 72 | resolve({ 73 | redirectUrl: routerContext.url, 74 | globals: createGlobals(params.data, store.getState(), renderHelmet()), 75 | statusCode: responseContext.statusCode 76 | }); 77 | return; 78 | } 79 | 80 | // Once any async tasks are done, we can perform the final render. 81 | // We also send the redux store state, so the client can continue execution where the server left off. 82 | params.domainTasks.then(() => { 83 | 84 | resolve({ 85 | html: renderApp(), 86 | globals: createGlobals(params.data, store.getState(), renderHelmet()), 87 | statusCode: responseContext.statusCode 88 | }); 89 | 90 | }, reject); // Also propagate any errors back into the host application. 91 | }); 92 | }); -------------------------------------------------------------------------------- /RCB.JavaScript/ClientApp/components/person/PersonEditor.jsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Formik, Field } from 'formik'; 3 | import FormValidator from "@Components/shared/FormValidator"; 4 | 5 | const PersonEditor = (props) => { 6 | 7 | const formValidator = React.useRef(null); 8 | 9 | const onSubmitForm = (values) => { 10 | if (!formValidator.current.isValid()) { 11 | // Form is not valid. 12 | return; 13 | } 14 | props.onSubmit(values); 15 | } 16 | 17 | // This function will be passed to children components as a parameter. 18 | // It's necessary to build custom markup with controls outside this component. 19 | const renderEditor = (values) => { 20 | 21 | return formValidator.current = x}> 22 |
23 | 24 | {({ field }) => ( 25 | <> 26 | 27 | 38 | 39 | )} 40 | 41 |
42 |
43 | 44 | {({ field }) => ( 45 | <> 46 | 47 | 58 | 59 | )} 60 | 61 |
62 |
; 63 | } 64 | 65 | return { 69 | onSubmitForm(values); 70 | }} 71 | > 72 | {({ values, handleSubmit }) => { 73 | // Let's say that the children element is a parametrizable function. 74 | // So we will pass other elements to this functional component as children 75 | // elements of this one: 76 | // 77 | // {(renderEditor, handleSubmit) => <> 78 | // {renderEditor()} 79 | // 80 | // } 81 | // . 82 | return props.children(() => renderEditor(values), handleSubmit); 83 | }} 84 | ; 85 | } 86 | 87 | export default PersonEditor; -------------------------------------------------------------------------------- /RCB.JavaScript/ClientApp/components/shared/AppRoute.jsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Route, Redirect } from "react-router"; 3 | import SessionManager from "@Core/session"; 4 | import responseContext from "@Core/responseContext"; 5 | 6 | const AppRoute = 7 | ({ component: Component, layout: Layout, statusCode: statusCode, path: Path, ...rest }) => { 8 | 9 | var isLoginPath = Path === "/login"; 10 | 11 | if (!SessionManager.isAuthenticated && !isLoginPath) { 12 | return ; 13 | } 14 | 15 | if (SessionManager.isAuthenticated && isLoginPath) { 16 | return ; 17 | } 18 | 19 | if (statusCode == null) { 20 | responseContext.statusCode = 200; 21 | } else { 22 | responseContext.statusCode = statusCode; 23 | } 24 | 25 | return ( 26 | 27 | 28 | 29 | )} />; 30 | }; 31 | 32 | export default AppRoute; -------------------------------------------------------------------------------- /RCB.JavaScript/ClientApp/components/shared/Footer.jsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | const Footer = () => { 4 | return ; 9 | } 10 | 11 | export default Footer; -------------------------------------------------------------------------------- /RCB.JavaScript/ClientApp/components/shared/FormValidator.jsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { NValTippy } from "nval-tippy"; 3 | 4 | export default class FormValidator extends React.Component { 5 | constructor(props) { 6 | super(props); 7 | } 8 | 9 | validator; 10 | elForm; 11 | 12 | isValid = () => { 13 | return this.validator.isValid(); 14 | } 15 | 16 | componentDidMount() { 17 | this.validator = new NValTippy(this.elForm); 18 | } 19 | 20 | render() { 21 | return
this.elForm = x}>{this.props.children}
; 22 | } 23 | } -------------------------------------------------------------------------------- /RCB.JavaScript/ClientApp/components/shared/Paginator.jsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import Paginating from "react-paginating"; 3 | import { Pagination } from "react-bootstrap"; 4 | 5 | export default class Paginator extends React.Component { 6 | 7 | constructor(props) { 8 | super(props); 9 | } 10 | 11 | firstPageBtn; 12 | lastPageBtn; 13 | 14 | setFirstPage = () => { 15 | var link = this.firstPageBtn.firstChild; 16 | link.click(); 17 | } 18 | 19 | setLastPage = () => { 20 | var link = this.lastPageBtn.firstChild; 21 | link.click(); 22 | } 23 | 24 | render() { 25 | return 30 | {({ 31 | pages, 32 | currentPage, 33 | hasNextPage, 34 | hasPreviousPage, 35 | previousPage, 36 | nextPage, 37 | totalPages, 38 | getPageItemProps 39 | }) => ( 40 | 41 | 42 | this.firstPageBtn = x} 44 | key={`first`} 45 | {...getPageItemProps({ 46 | total: totalPages, 47 | pageValue: 1, 48 | onPageChange: (num, e) => this.props.onChangePage(num) 49 | })} 50 | > 51 | first 52 | 53 | 54 | {hasPreviousPage && ( 55 | this.props.onChangePage(num) 61 | })} 62 | > 63 | {`<`} 64 | 65 | )} 66 | 67 | {pages.map(page => { 68 | return this.props.onChangePage(num) 75 | })} 76 | > 77 | {page} 78 | ; 79 | })} 80 | 81 | {hasNextPage && ( 82 | this.props.onChangePage(num) 88 | })} 89 | > 90 | {`>`} 91 | 92 | )} 93 | 94 | this.lastPageBtn = x} 96 | key={`last`} 97 | {...getPageItemProps({ 98 | total: totalPages, 99 | pageValue: totalPages, 100 | onPageChange: (num, e) => this.props.onChangePage(num) 101 | })} 102 | > 103 | last 104 | 105 | 106 | 107 | )} 108 | 109 | } 110 | } -------------------------------------------------------------------------------- /RCB.JavaScript/ClientApp/components/shared/TopMenu.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { withRouter } from "react-router"; 3 | import { Redirect, NavLink } from "react-router-dom"; 4 | import AccountService from "@Services/AccountService"; 5 | import { Nav, Navbar, Dropdown } from "react-bootstrap"; 6 | import { LinkContainer } from 'react-router-bootstrap' 7 | import SessionManager from "@Core/session"; 8 | 9 | const TopMenu = () => { 10 | 11 | const [isLogout, setLogout] = useState(false); 12 | 13 | const onClickSignOut = async () => { 14 | var accountService = new AccountService(); 15 | await accountService.logout(); 16 | setLogout(true); 17 | } 18 | 19 | if (isLogout) { 20 | return ; 21 | } 22 | 23 | return 24 | 25 | RCB.JavaScript 26 | 27 | 28 | 29 | 37 | 38 | 49 | 50 | 51 | ; 52 | } 53 | 54 | // Attach the React Router to the component to have an opportunity 55 | // to interract with it: use some navigation components, 56 | // have an access to React Router fields in the component's props, etc. 57 | export default withRouter(TopMenu); -------------------------------------------------------------------------------- /RCB.JavaScript/ClientApp/core/Result.js: -------------------------------------------------------------------------------- 1 | export default class Result { 2 | 3 | value; 4 | errors; 5 | 6 | get hasErrors() { 7 | return this.errors != null && Array.isArray(this.errors) && this.errors.length > 0; 8 | } 9 | 10 | constructor(value, ...errors) { 11 | this.value = value; 12 | this.errors = errors[0] == undefined || errors[0] == null ? [] : errors; 13 | } 14 | } -------------------------------------------------------------------------------- /RCB.JavaScript/ClientApp/core/ServiceBase.js: -------------------------------------------------------------------------------- 1 | import Result from "./Result"; 2 | import Axios, { AxiosRequestConfig } from "axios"; 3 | import { transformUrl } from "domain-wait"; 4 | import queryString from "query-string"; 5 | import { isNode, showErrors, getNodeProcess } from "@Utils"; 6 | import SessionManager from "./session"; 7 | 8 | /** 9 | * Represents base class of the isomorphic service. 10 | */ 11 | export class ServiceBase { 12 | 13 | /** 14 | * Make request with JSON data. 15 | * @param opts 16 | */ 17 | async requestJson(opts) { 18 | 19 | var axiosResult = null; 20 | var result = null; 21 | 22 | opts.url = transformUrl(opts.url); // Allow requests also for the Node. 23 | 24 | var processQuery = (url, data) => { 25 | if (data) { 26 | return `${url}?${queryString.stringify(data)}`; 27 | } 28 | return url; 29 | }; 30 | 31 | let axiosRequestConfig; 32 | 33 | if (isNode()) { 34 | 35 | const ssrSessionData = SessionManager.getSessionContext().ssr; 36 | const { cookie } = ssrSessionData; 37 | 38 | // Make SSR requests 'authorized' from the NodeServices to the web server. 39 | axiosRequestConfig = { 40 | headers: { 41 | Cookie: cookie 42 | } 43 | } 44 | } 45 | 46 | try { 47 | switch (opts.method) { 48 | case "GET": 49 | axiosResult = await Axios.get(processQuery(opts.url, opts.data), axiosRequestConfig); 50 | break; 51 | case "POST": 52 | axiosResult = await Axios.post(opts.url, opts.data, axiosRequestConfig); 53 | break; 54 | case "PUT": 55 | axiosResult = await Axios.put(opts.url, opts.data, axiosRequestConfig); 56 | break; 57 | case "PATCH": 58 | axiosResult = await Axios.patch(opts.url, opts.data, axiosRequestConfig); 59 | break; 60 | case "DELETE": 61 | axiosResult = await Axios.delete(processQuery(opts.url, opts.data), axiosRequestConfig); 62 | break; 63 | } 64 | result = new Result(axiosResult.data.value, ...axiosResult.data.errors); 65 | } catch (error) { 66 | result = new Result(null, error.message); 67 | } 68 | 69 | if (result.hasErrors) { 70 | showErrors(...result.errors); 71 | } 72 | 73 | return result; 74 | } 75 | 76 | /** 77 | * Allows you to send files to the server. 78 | * @param opts 79 | */ 80 | async sendFormData(opts) { 81 | let axiosResult = null; 82 | let result = null; 83 | 84 | opts.url = transformUrl(opts.url); // Allow requests also for Node. 85 | 86 | var axiosOpts = { 87 | headers: { 88 | 'Content-Type': 'multipart/form-data' 89 | } 90 | }; 91 | 92 | try { 93 | switch (opts.method) { 94 | case "POST": 95 | axiosResult = await Axios.post(opts.url, opts.data, axiosOpts); 96 | break; 97 | case "PUT": 98 | axiosResult = await Axios.put(opts.url, opts.data, axiosOpts); 99 | break; 100 | case "PATCH": 101 | axiosResult = await Axios.patch(opts.url, opts.data, axiosOpts); 102 | break; 103 | } 104 | result = new Result(axiosResult.data.value, ...axiosResult.data.errors); 105 | } catch (error) { 106 | result = new Result(null, error.message); 107 | } 108 | 109 | if (result.hasErrors) { 110 | showErrors(...result.errors); 111 | } 112 | 113 | return result; 114 | } 115 | } -------------------------------------------------------------------------------- /RCB.JavaScript/ClientApp/core/responseContext.js: -------------------------------------------------------------------------------- 1 | const responseContext = { 2 | statusCode: 200 3 | }; 4 | 5 | export default responseContext; -------------------------------------------------------------------------------- /RCB.JavaScript/ClientApp/core/session/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * User's session context manager. 3 | */ 4 | export default class SessionManager { 5 | 6 | static _isInitialized = false; 7 | 8 | static _context = {}; 9 | 10 | static resetSession() { 11 | this._isInitialized = false; 12 | this._context = {}; 13 | } 14 | 15 | static initSession(sessionContext) { 16 | if (this._isInitialized) { 17 | throw Error("SessionManager: already initialized."); 18 | } 19 | 20 | this._context = (sessionContext || { 21 | isomorphic: {}, 22 | ssr: {} 23 | }); 24 | 25 | this._isInitialized = true; 26 | } 27 | 28 | static throwIfNotInitialized() { 29 | if (!this._isInitialized) { 30 | throw Error("SessionManager: you have to call 'SessionManager.initSession' for initialization."); 31 | } 32 | } 33 | 34 | static getSessionContext() { 35 | this.throwIfNotInitialized(); 36 | return this._context; 37 | } 38 | 39 | static getServiceUser() { 40 | let context = this.getSessionContext(); 41 | if (context) { 42 | const isomorphicData = context.isomorphic; 43 | if (isomorphicData) { 44 | return isomorphicData.serviceUser; 45 | } else { 46 | throw Error("SessionManager: isomorphic session was not initialized.") 47 | } 48 | } 49 | throw Error("SessionManager: current session was not initialized.") 50 | } 51 | 52 | static setServiceUser(serviceUser) { 53 | let context = this.getSessionContext(); 54 | context.isomorphic.serviceUser = serviceUser; 55 | } 56 | 57 | static get isAuthenticated() { 58 | return this.getServiceUser() != null; 59 | } 60 | } -------------------------------------------------------------------------------- /RCB.JavaScript/ClientApp/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NickMaev/react-core-boilerplate/6f6e2eedd82dc8454bde543364557eb00413273e/RCB.JavaScript/ClientApp/images/logo.png -------------------------------------------------------------------------------- /RCB.JavaScript/ClientApp/layouts/AuthorizedLayout.jsx: -------------------------------------------------------------------------------- 1 | import TopMenu from "@Components/shared/TopMenu"; 2 | import * as React from "react"; 3 | import "@Styles/authorizedLayout.scss"; 4 | import { ToastContainer } from "react-toastify"; 5 | import Footer from "@Components/shared/Footer"; 6 | 7 | export default class AuthorizedLayout extends React.Component { 8 | render() { 9 | 10 | return
11 | 12 | {this.props.children} 13 | 14 |
15 |
; 16 | } 17 | } -------------------------------------------------------------------------------- /RCB.JavaScript/ClientApp/layouts/GuestLayout.jsx: -------------------------------------------------------------------------------- 1 | import "@Styles/guestLayout.scss"; 2 | import * as React from "react"; 3 | import { ToastContainer } from "react-toastify"; 4 | 5 | export default class GuestLayout extends React.Component { 6 | render() { 7 | 8 | return
9 |
10 | {this.props.children} 11 |
12 | 13 |
; 14 | } 15 | } -------------------------------------------------------------------------------- /RCB.JavaScript/ClientApp/pages/HomePage.jsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Helmet } from "react-helmet"; 3 | import logo from "@Images/logo.png"; 4 | 5 | const HomePage = () => { 6 | return
7 | 8 | Home page - RCB.JavaScript 9 | 10 | 11 |
12 | 13 | 14 | 15 |

Happy coding!

16 |
; 17 | } 18 | 19 | export default HomePage; -------------------------------------------------------------------------------- /RCB.JavaScript/ClientApp/pages/LoginPage.jsx: -------------------------------------------------------------------------------- 1 | import * as loginStore from "@Store/loginStore"; 2 | import { connect } from "react-redux"; 3 | import React, { useRef } from "react"; 4 | import { Helmet } from "react-helmet"; 5 | import { Redirect, withRouter } from "react-router"; 6 | import FormValidator from "@Components/shared/FormValidator"; 7 | import Button from "react-bootstrap/Button"; 8 | import { Formik, Field } from "formik"; 9 | import { FormGroup } from "react-bootstrap"; 10 | import SessionManager from "@Core/session"; 11 | 12 | const LoginPage = (props) => { 13 | 14 | const formValidator = useRef(null); 15 | 16 | const onSubmit = async (data) => { 17 | if (formValidator.current.isValid()) { 18 | await props.login(data); 19 | } 20 | }; 21 | 22 | if (SessionManager.isAuthenticated && props.isLoginSuccess) { 23 | return ; 24 | } 25 | 26 | return
27 | 28 | 29 | Login page - RCB.JavaScript 30 | 31 | 32 |
33 | 34 |

Type any login and password to enter.

35 | 36 | { 40 | await onSubmit(values); 41 | }} 42 | > 43 | {({ values, handleSubmit }) => { 44 | 45 | return formValidator.current = x}> 46 | 47 | 48 | 49 | {({ field }) => ( 50 | <> 51 | 52 | 62 | 63 | )} 64 | 65 | 66 | 67 | 68 | 69 | {({ field }) => ( 70 | <> 71 | 72 | 82 | 83 | )} 84 | 85 | 86 | 87 |
88 | 89 |
90 | 91 |
92 | }} 93 |
94 | 95 |
96 |
; 97 | } 98 | 99 | // Connect component with Redux store. 100 | var connectedComponent = connect( 101 | state => state.login, // Selects which state properties are merged into the component's props. 102 | loginStore.actionCreators, // Selects which action creators are merged into the component's props. 103 | )(LoginPage); 104 | 105 | // Attach the React Router to the component to have an opportunity 106 | // to interract with it: use some navigation components, 107 | // have an access to React Router fields in the component's props, etc. 108 | export default withRouter(connectedComponent); 109 | -------------------------------------------------------------------------------- /RCB.JavaScript/ClientApp/pages/NotFoundPage.jsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Helmet } from "react-helmet"; 3 | 4 | const NotFoundPage = () => { 5 | return
6 | 7 | Page not found - RCB.TypeScript 8 | 9 | 10 |
11 | 12 |

13 | 404 - Page not found 14 |

15 |
; 16 | } 17 | 18 | export default NotFoundPage; -------------------------------------------------------------------------------- /RCB.JavaScript/ClientApp/routes.jsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import GuestLayout from "@Layouts/GuestLayout"; 3 | import AuthorizedLayout from '@Layouts/AuthorizedLayout'; 4 | import LoginPage from '@Pages/LoginPage'; 5 | import AppRoute from "@Components/shared/AppRoute"; 6 | import HomePage from '@Pages/HomePage'; 7 | import ExamplesPage from '@Pages/ExamplesPage'; 8 | import { Switch } from 'react-router-dom'; 9 | import NotFoundPage from '@Pages/NotFoundPage'; 10 | 11 | export const routes = 12 | 13 | 14 | 15 | 16 | ; -------------------------------------------------------------------------------- /RCB.JavaScript/ClientApp/services/AccountService.js: -------------------------------------------------------------------------------- 1 | import { ServiceBase } from "@Core/ServiceBase"; 2 | import SessionManager from "@Core/session"; 3 | 4 | export default class AccountService extends ServiceBase { 5 | 6 | async login(loginModel) { 7 | var result = await this.requestJson({ 8 | url: "api/Account/Login", 9 | method: "POST", 10 | data: loginModel 11 | }); 12 | 13 | if (!result.hasErrors) { 14 | SessionManager.setServiceUser(result.value); 15 | } 16 | 17 | return result; 18 | } 19 | 20 | async logout() { 21 | var result = await this.requestJson({ 22 | url: "api/Account/Logout", 23 | method: "POST" 24 | }); 25 | 26 | if (!result.hasErrors) { 27 | SessionManager.setServiceUser(null); 28 | } 29 | 30 | return result; 31 | } 32 | } -------------------------------------------------------------------------------- /RCB.JavaScript/ClientApp/services/PersonService.js: -------------------------------------------------------------------------------- 1 | import { ServiceBase } from "@Core/ServiceBase"; 2 | 3 | export default class PersonService extends ServiceBase { 4 | 5 | async search(term = null) { 6 | if (term == null) { 7 | term = ""; 8 | } 9 | var result = await this.requestJson({ 10 | url: `/api/Person/Search?term=${term}`, 11 | method: "GET" 12 | }); 13 | return result; 14 | } 15 | 16 | async update(model) { 17 | var result = await this.requestJson({ 18 | url: `/api/Person/${model.id}`, 19 | method: "PATCH", 20 | data: model 21 | }); 22 | return result; 23 | } 24 | 25 | async delete(id) { 26 | var result = await this.requestJson({ 27 | url: `/api/Person/${id}`, 28 | method: "DELETE" 29 | }); 30 | return result; 31 | } 32 | 33 | async add(model) { 34 | var result = await this.requestJson({ 35 | url: "/api/Person/Add", 36 | method: "POST", 37 | data: model 38 | }); 39 | return result; 40 | } 41 | } -------------------------------------------------------------------------------- /RCB.JavaScript/ClientApp/store/configureStore.js: -------------------------------------------------------------------------------- 1 | import thunk from "redux-thunk"; 2 | import { createStore, applyMiddleware, compose, combineReducers } from 'redux'; 3 | import { routerMiddleware, LOCATION_CHANGE } from "connected-react-router"; 4 | import { reducers } from "@Store/index"; 5 | 6 | export default function configureStore(history, initialState = {}) { 7 | 8 | // Build middleware. These are functions that can process the actions before they reach the store. 9 | const windowIfDefined = typeof window === "undefined" ? null : window; 10 | 11 | // If devTools is installed, connect to it. 12 | const devToolsExtension = windowIfDefined && windowIfDefined.__REDUX_DEVTOOLS_EXTENSION__; 13 | const createStoreWithMiddleware = compose( 14 | applyMiddleware(thunk, routerMiddleware(history)), 15 | devToolsExtension ? devToolsExtension() : next => next 16 | )(createStore); 17 | 18 | // Combine all reducers and instantiate the app-wide store instance. 19 | const allReducers = buildRootReducer(reducers, history); 20 | const store = createStoreWithMiddleware(allReducers, initialState); 21 | 22 | // Enable Webpack hot module replacement for reducers. 23 | if (module.hot) { 24 | module.hot.accept("@Store/index", () => { 25 | const nextRootReducer = require("@Store/index"); 26 | store.replaceReducer(buildRootReducer(nextRootReducer.reducers, history)); 27 | }); 28 | } 29 | 30 | return store; 31 | } 32 | 33 | const routerReducer = (history) => { 34 | const initialState = { 35 | location: history.location, 36 | action: history.action, 37 | }; 38 | return (state = initialState, arg = {}) => { 39 | if (arg.type === LOCATION_CHANGE) { 40 | return { ...state, ...arg.payload }; 41 | } 42 | return state; 43 | } 44 | }; 45 | 46 | function buildRootReducer(allReducers, history) { 47 | return combineReducers({...allReducers, ...{ router: routerReducer(history) }}); 48 | } -------------------------------------------------------------------------------- /RCB.JavaScript/ClientApp/store/index.js: -------------------------------------------------------------------------------- 1 | import * as loginStore from "@Store/loginStore"; 2 | import * as personStore from "@Store/personStore"; 3 | 4 | // Whenever an action is dispatched, Redux will update each top-level application state property using 5 | // the reducer with the matching name. It's important that the names match exactly, and that the reducer 6 | // acts on the corresponding ApplicationState property type. 7 | export const reducers = { 8 | login: loginStore.reducer, 9 | person: personStore.reducer 10 | }; 11 | -------------------------------------------------------------------------------- /RCB.JavaScript/ClientApp/store/loginStore.js: -------------------------------------------------------------------------------- 1 | import { createSlice } from '@reduxjs/toolkit'; 2 | import AccountService from '@Services/AccountService'; 3 | 4 | // Create the slice. 5 | const slice = createSlice({ 6 | name: "login", 7 | initialState: { 8 | isFetching: false, 9 | isLoginSuccess: false 10 | }, 11 | reducers: { 12 | setFetching: (state, action) => { 13 | state.isFetching = action.payload; 14 | }, 15 | setSuccess: (state, action) => { 16 | state.isLoginSuccess = action.payload; 17 | } 18 | } 19 | }); 20 | 21 | // Export reducer from the slice. 22 | export const { reducer } = slice; 23 | 24 | // Define action creators. 25 | export const actionCreators = { 26 | login: (model) => async (dispatch) => { 27 | dispatch(slice.actions.setFetching(true)); 28 | 29 | const service = new AccountService(); 30 | 31 | const result = await service.login(model); 32 | 33 | if (!result.hasErrors) { 34 | dispatch(slice.actions.setSuccess(true)); 35 | } 36 | 37 | dispatch(slice.actions.setFetching(false)); 38 | } 39 | }; 40 | -------------------------------------------------------------------------------- /RCB.JavaScript/ClientApp/store/personStore.js: -------------------------------------------------------------------------------- 1 | import { createSlice } from '@reduxjs/toolkit'; 2 | import PersonService from '@Services/PersonService'; 3 | 4 | // Create the slice. 5 | const slice = createSlice({ 6 | name: "person", 7 | initialState: { 8 | isFetching: false, 9 | collection: [] 10 | }, 11 | reducers: { 12 | setFetching: (state, action) => { 13 | state.isFetching = action.payload; 14 | }, 15 | setData: (state, action) => { 16 | state.collection = action.payload; 17 | }, 18 | addData: (state, action) => { 19 | state.collection = [...state.collection, action.payload]; 20 | }, 21 | updateData: (state, action) => { 22 | // We need to clone collection (Redux-way). 23 | var collection = [...state.collection]; 24 | var entry = collection.find(x => x.id === action.payload.id); 25 | entry.firstName = action.payload.firstName; 26 | entry.lastName = action.payload.lastName; 27 | state.collection = [...state.collection]; 28 | }, 29 | deleteData: (state, action) => { 30 | state.collection = state.collection.filter(x => x.id !== action.payload.id); 31 | } 32 | } 33 | }); 34 | 35 | // Export reducer from the slice. 36 | export const { reducer } = slice; 37 | 38 | // Define action creators. 39 | export const actionCreators = { 40 | search: (term = null) => async (dispatch) => { 41 | dispatch(slice.actions.setFetching(true)); 42 | 43 | const service = new PersonService(); 44 | 45 | const result = await service.search(term); 46 | 47 | if (!result.hasErrors) { 48 | dispatch(slice.actions.setData(result.value)); 49 | } 50 | 51 | dispatch(slice.actions.setFetching(false)); 52 | 53 | return result; 54 | }, 55 | add: (model) => async (dispatch) => { 56 | dispatch(slice.actions.setFetching(true)); 57 | 58 | const service = new PersonService(); 59 | 60 | const result = await service.add(model); 61 | 62 | if (!result.hasErrors) { 63 | model.id = result.value; 64 | dispatch(slice.actions.addData(model)); 65 | } 66 | 67 | dispatch(slice.actions.setFetching(false)); 68 | 69 | return result; 70 | }, 71 | update: (model) => async (dispatch) => { 72 | dispatch(slice.actions.setFetching(true)); 73 | 74 | const service = new PersonService(); 75 | 76 | const result = await service.update(model); 77 | 78 | if (!result.hasErrors) { 79 | dispatch(slice.actions.updateData(model)); 80 | } 81 | 82 | dispatch(slice.actions.setFetching(false)); 83 | 84 | return result; 85 | }, 86 | delete: (id) => async (dispatch) => { 87 | dispatch(slice.actions.setFetching(true)); 88 | 89 | const service = new PersonService(); 90 | 91 | const result = await service.delete(id); 92 | 93 | if (!result.hasErrors) { 94 | dispatch(slice.actions.deleteData({ id })); 95 | } 96 | 97 | dispatch(slice.actions.setFetching(false)); 98 | 99 | return result; 100 | } 101 | }; 102 | -------------------------------------------------------------------------------- /RCB.JavaScript/ClientApp/styles/authorizedLayout.scss: -------------------------------------------------------------------------------- 1 | #authorizedLayout { 2 | background: white !important; 3 | } 4 | -------------------------------------------------------------------------------- /RCB.JavaScript/ClientApp/styles/guestLayout.scss: -------------------------------------------------------------------------------- 1 | #guestLayout { 2 | background: #f8f8f8; 3 | 4 | #loginContainer { 5 | width: 400px; 6 | position: absolute; 7 | left: 0; 8 | right: 0; 9 | top: 20%; 10 | margin-left: auto; 11 | margin-right: auto; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /RCB.JavaScript/ClientApp/styles/loaders/applicationLoader.css: -------------------------------------------------------------------------------- 1 | #applicationLoader { 2 | background: white; 3 | width: 100%; 4 | height: 100%; 5 | position: absolute; 6 | left: 0; 7 | right: 0; 8 | margin: 0 auto; 9 | text-align: center; 10 | z-index: 9999; 11 | display: table; 12 | } 13 | 14 | #applicationLoader > div { 15 | display: table-cell; 16 | vertical-align: middle; 17 | pointer-events: none; 18 | } 19 | 20 | #applicationLoader.hidden { 21 | display: none; 22 | } 23 | 24 | #applicationLoader .spinner { 25 | margin-left: -1px; 26 | width: 19px; 27 | text-align: center; 28 | display: inline-block; 29 | } 30 | 31 | #applicationLoader .spinner > div { 32 | width: 3px; 33 | height: 3px; 34 | background-color: #5c5c5c; 35 | border-radius: 100%; 36 | display: inline-block; 37 | -webkit-animation: sk-bouncedelay 1.4s infinite ease-in-out both; 38 | animation: sk-bouncedelay 1.4s infinite ease-in-out both; 39 | } 40 | 41 | #applicationLoader .spinner .bounce1 { 42 | -webkit-animation-delay: -0.32s; 43 | animation-delay: -0.32s; 44 | } 45 | 46 | #applicationLoader .spinner .bounce2 { 47 | -webkit-animation-delay: -0.16s; 48 | animation-delay: -0.16s; 49 | } 50 | 51 | @-webkit-keyframes sk-bouncedelay { 52 | 0%, 80%, 100% { 53 | -webkit-transform: scale(0) 54 | } 55 | 56 | 40% { 57 | -webkit-transform: scale(1.0) 58 | } 59 | } 60 | 61 | @keyframes sk-bouncedelay { 62 | 0%, 80%, 100% { 63 | -webkit-transform: scale(0); 64 | transform: scale(0); 65 | } 66 | 67 | 40% { 68 | -webkit-transform: scale(1.0); 69 | transform: scale(1.0); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /RCB.JavaScript/ClientApp/styles/loaders/queryLoader.scss: -------------------------------------------------------------------------------- 1 | #queryLoader { 2 | background: rgba(255, 255, 255, 0.7); 3 | width: 100%; 4 | height: 100%; 5 | position: absolute; 6 | left: 0; 7 | right: 0; 8 | margin: 0 auto; 9 | text-align: center; 10 | z-index: 9999; 11 | display: table; 12 | 13 | > div { 14 | display: table-cell; 15 | vertical-align: middle; 16 | pointer-events: none; 17 | } 18 | 19 | &.hidden { 20 | display: none; 21 | background: rgba(255, 255, 255, 0); 22 | } 23 | 24 | .spinner { 25 | width: 56px; 26 | height: 56px; 27 | border: 8px solid rgba(196, 196, 196, 0.25); 28 | border-top-color: rgb(31, 172, 255); 29 | border-radius: 50%; 30 | position: relative; 31 | animation: loader-rotate 1s linear infinite; 32 | top: 50%; 33 | margin: -28px auto 0; 34 | } 35 | } 36 | 37 | @keyframes loader-rotate { 38 | 0% { 39 | transform: rotate(0); 40 | } 41 | 42 | 100% { 43 | transform: rotate(360deg); 44 | } 45 | } -------------------------------------------------------------------------------- /RCB.JavaScript/ClientApp/styles/main.scss: -------------------------------------------------------------------------------- 1 | .footer { 2 | width: 100%; 3 | height: 80px; 4 | padding: 10px; 5 | border-top: 1px solid #e7e7e7; 6 | 7 | p { 8 | margin: 0; 9 | padding: 0; 10 | } 11 | } 12 | 13 | .layout{ 14 | height:100%; 15 | } 16 | 17 | ul.pagination{ 18 | &> li > span { 19 | cursor: pointer; 20 | } 21 | } 22 | 23 | .tippy-content { 24 | font-size: 14px; 25 | } 26 | 27 | label { 28 | margin: 5px !important; 29 | 30 | &.required { 31 | &:after { 32 | content: '*'; 33 | color: red; 34 | padding-left: 4px; 35 | font-size: 18px; 36 | position: absolute; 37 | } 38 | } 39 | } 40 | 41 | .navbar-dark .navbar-nav { 42 | .active > .nav-link, 43 | .nav-link.active, 44 | .nav-link.show, 45 | .show > .nav-link { 46 | color: #fff; 47 | } 48 | } -------------------------------------------------------------------------------- /RCB.JavaScript/ClientApp/utils.js: -------------------------------------------------------------------------------- 1 | import { toast } from "react-toastify"; 2 | 3 | /** 4 | * Is server prerendering by NodeJs. 5 | * There can't be any DOM elements such as: window, document, etc. 6 | */ 7 | export function isNode() { 8 | return typeof process === 'object' && process.versions && !!process.versions.node; 9 | } 10 | 11 | /** 12 | * Get NodeJs process. 13 | * */ 14 | export function getNodeProcess() { 15 | if (isNode()) { 16 | return process; 17 | } 18 | return null; 19 | } 20 | 21 | /** 22 | * Show error messages on page. 23 | * @param messages 24 | */ 25 | export function showErrors(...messages) { 26 | 27 | messages.forEach(x => { 28 | if (!Array.isArray(x)) { 29 | toast.error(x); 30 | } 31 | else { 32 | x.forEach((y) => toast.error(y)); 33 | } 34 | }); 35 | } 36 | 37 | /** 38 | * Show information message on page. 39 | * @param message 40 | */ 41 | export function showInfo(message) { 42 | toast.info(message); 43 | } 44 | 45 | const getApplicationLoader = () => { 46 | if (isNode()) { 47 | return null; 48 | } 49 | return document.getElementById("applicationLoader"); 50 | }; 51 | 52 | const getQueryLoader = () => { 53 | if (isNode()) { 54 | return null; 55 | } 56 | return document.getElementById("queryLoader"); 57 | }; 58 | 59 | /** 60 | * Show main application loader. 61 | * */ 62 | export function showApplicationLoader() { 63 | let loader = getApplicationLoader(); 64 | if (loader) { 65 | loader.className = ""; 66 | } 67 | } 68 | 69 | /** 70 | * Hide main application loader. 71 | * */ 72 | export function hideApplicationLoader() { 73 | let loader = getApplicationLoader(); 74 | if (loader) { 75 | loader.className = "hidden"; 76 | } 77 | } 78 | 79 | /** 80 | * Show query loader. 81 | * */ 82 | export function showQueryLoader() { 83 | let loader = getQueryLoader(); 84 | if (loader) { 85 | loader.className = ""; 86 | } 87 | } 88 | 89 | /** 90 | * Hide query loader. 91 | * */ 92 | export function hideQueryLoader() { 93 | let loader = getQueryLoader(); 94 | if (loader) { 95 | loader.className = "hidden"; 96 | } 97 | } 98 | 99 | /** 100 | * Clone object. 101 | * @param object input object. 102 | */ 103 | export function clone(object) { 104 | return JSON.parse(JSON.stringify(object)); 105 | } 106 | 107 | export function isObjectEmpty(obj) { 108 | for (var key in obj) { 109 | if (obj.hasOwnProperty(key)) 110 | return false; 111 | } 112 | return true; 113 | } 114 | 115 | /** 116 | * Paginate an array for the client side. 117 | * @param array input array. 118 | * @param pageNumber page number. 119 | * @param limitPerPage entries per page. 120 | */ 121 | export function paginate(array, pageNumber, limitPerPage) { 122 | let rowOffset = Math.ceil((pageNumber - 1) * limitPerPage); 123 | return array.slice(rowOffset, rowOffset + limitPerPage); 124 | } -------------------------------------------------------------------------------- /RCB.JavaScript/Constants.cs: -------------------------------------------------------------------------------- 1 | namespace RCB.JavaScript 2 | { 3 | public static class Constants 4 | { 5 | public static string AuthorizationCookieKey => "Auth"; 6 | public static string HttpContextServiceUserItemKey => "ServiceUser"; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /RCB.JavaScript/Controllers/AccountController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | using RCB.JavaScript.Models; 3 | using RCB.JavaScript.Services; 4 | 5 | namespace RCB.JavaScript.Controllers 6 | { 7 | [ApiController] 8 | [Route("api/[controller]")] 9 | public class AccountController : ControllerBase 10 | { 11 | private AccountService AccountService { get; set; } 12 | 13 | public AccountController(AccountService accountService) 14 | { 15 | AccountService = accountService; 16 | } 17 | 18 | [HttpPost("[action]")] 19 | public IActionResult Login([FromBody]LoginModel model) 20 | { 21 | var result = AccountService.Login(HttpContext, model.Login, model.Password); 22 | return Json(result); 23 | } 24 | 25 | [HttpPost("[action]")] 26 | public IActionResult Logout() 27 | { 28 | var result = AccountService.Logout(HttpContext); 29 | return Json(result); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /RCB.JavaScript/Controllers/ControllerBase.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | using Microsoft.AspNetCore.Mvc.Filters; 3 | using RCB.JavaScript.Infrastructure; 4 | 5 | namespace RCB.JavaScript.Controllers 6 | { 7 | public class ControllerBase : Controller 8 | { 9 | protected ServiceUser ServiceUser { get; set; } 10 | 11 | public override void OnActionExecuting(ActionExecutingContext context) 12 | { 13 | ControllerContext 14 | .HttpContext 15 | .Items 16 | .TryGetValue( 17 | Constants.HttpContextServiceUserItemKey, 18 | out object serviceUser); 19 | ServiceUser = serviceUser as ServiceUser; 20 | base.OnActionExecuting(context); 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /RCB.JavaScript/Controllers/MainController.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using System.Linq; 3 | using Microsoft.AspNetCore.Mvc; 4 | using RCB.JavaScript.Infrastructure; 5 | 6 | namespace RCB.JavaScript.Controllers 7 | { 8 | public class MainController : ControllerBase 9 | { 10 | public IActionResult Index() 11 | { 12 | var webSessionContext = new WebSessionContext 13 | { 14 | Ssr = new SsrSessionData 15 | { 16 | Cookie = string.Join(", ", Request.Cookies.Select(x => $"{x.Key}={x.Value};")) 17 | }, 18 | Isomorphic = new IsomorphicSessionData 19 | { 20 | ServiceUser = ServiceUser 21 | } 22 | }; 23 | 24 | return View(webSessionContext); 25 | } 26 | 27 | public IActionResult Error() 28 | { 29 | ViewData["RequestId"] = Activity.Current?.Id ?? HttpContext.TraceIdentifier; 30 | return View(); 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /RCB.JavaScript/Controllers/PersonController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | using RCB.JavaScript.Models; 3 | using RCB.JavaScript.Services; 4 | 5 | namespace RCB.JavaScript.Controllers 6 | { 7 | [ApiController] 8 | [Route("api/[controller]")] 9 | public class PersonController : ControllerBase 10 | { 11 | private PersonService PersonService { get; } 12 | 13 | public PersonController(PersonService personService) 14 | { 15 | PersonService = personService; 16 | } 17 | 18 | [HttpGet("[action]")] 19 | public IActionResult Search([FromQuery]string term = null) 20 | { 21 | return Json(PersonService.Search(term)); 22 | } 23 | 24 | [HttpPost("[action]")] 25 | public IActionResult Add(PersonModel model) 26 | { 27 | if (model == null) 28 | return BadRequest($"{nameof(model)} is null."); 29 | var result = PersonService.Add(model); 30 | return Json(result); 31 | } 32 | 33 | [HttpPatch("{id:int}")] 34 | public IActionResult Update(PersonModel model) 35 | { 36 | if (model == null) 37 | return BadRequest($"{nameof(model)} is null."); 38 | var result = PersonService.Update(model); 39 | return Json(result); 40 | } 41 | 42 | [HttpDelete("{id:int}")] 43 | public IActionResult Delete(int id) 44 | { 45 | if (id <= 0) 46 | return BadRequest($"{nameof(id)} <= 0."); 47 | var result = PersonService.Delete(id); 48 | return Json(result); 49 | } 50 | } 51 | } -------------------------------------------------------------------------------- /RCB.JavaScript/Dockerfile: -------------------------------------------------------------------------------- 1 | #See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging. 2 | 3 | FROM mcr.microsoft.com/dotnet/core/aspnet:3.1-buster-slim AS base 4 | RUN apt-get update -yq \ 5 | && apt-get install curl gnupg -yq \ 6 | && curl -sL https://deb.nodesource.com/setup_12.x | bash \ 7 | && apt-get install nodejs -yq 8 | WORKDIR /app 9 | EXPOSE 80 10 | EXPOSE 443 11 | 12 | FROM mcr.microsoft.com/dotnet/core/sdk:3.1-buster AS build 13 | RUN apt-get update -yq \ 14 | && apt-get install curl gnupg -yq \ 15 | && curl -sL https://deb.nodesource.com/setup_12.x | bash \ 16 | && apt-get install nodejs -yq 17 | WORKDIR /src 18 | COPY RCB.JavaScript.csproj RCB.JavaScript/ 19 | RUN dotnet restore "RCB.JavaScript/RCB.JavaScript.csproj" 20 | COPY . RCB.JavaScript/ 21 | WORKDIR "/src/RCB.JavaScript" 22 | RUN dotnet build "RCB.JavaScript.csproj" -c Release -o /app/build 23 | 24 | FROM build AS publish 25 | RUN dotnet publish "RCB.JavaScript.csproj" -c Release -o /app/publish 26 | 27 | FROM base AS final 28 | WORKDIR /app 29 | COPY --from=publish /app/publish . 30 | ENTRYPOINT ["dotnet", "RCB.JavaScript.dll"] -------------------------------------------------------------------------------- /RCB.JavaScript/Infrastructure/ExceptionMiddleware.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Microsoft.AspNetCore.Http; 4 | using Serilog; 5 | 6 | namespace RCB.JavaScript.Infrastructure 7 | { 8 | public class ExceptionMiddleware 9 | { 10 | private readonly RequestDelegate _next; 11 | 12 | public ExceptionMiddleware(RequestDelegate next) 13 | { 14 | _next = next; 15 | } 16 | 17 | public async Task InvokeAsync(HttpContext httpContext) 18 | { 19 | try 20 | { 21 | await _next(httpContext); 22 | } 23 | catch (Exception ex) 24 | { 25 | Log.Error(ex, "Exception was thrown during the request."); 26 | throw; 27 | } 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /RCB.JavaScript/Infrastructure/Result.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using Newtonsoft.Json; 4 | 5 | namespace RCB.JavaScript.Infrastructure 6 | { 7 | public class Result 8 | { 9 | public List Errors { get; set; } = new List(); 10 | 11 | [JsonIgnore] 12 | public bool HasErrors => Errors != null && Errors.Any(); 13 | 14 | public Result() 15 | { 16 | 17 | } 18 | 19 | public Result(params string[] errors) 20 | { 21 | this.Errors = errors.ToList(); 22 | } 23 | 24 | public void AddError(string error) 25 | { 26 | this.Errors.Add(error); 27 | } 28 | 29 | public void AddErrors(string[] errors) 30 | { 31 | this.Errors.AddRange(errors); 32 | } 33 | } 34 | 35 | public class Result : Result 36 | { 37 | public T Value { get; set; } 38 | 39 | public Result(T value) 40 | { 41 | this.Value = value; 42 | } 43 | 44 | public Result(params string[] errors) : base(errors) 45 | { 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /RCB.JavaScript/Infrastructure/SerilogMvcLoggingAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.AspNetCore.Mvc.Filters; 3 | using Microsoft.Extensions.DependencyInjection; 4 | using Serilog; 5 | 6 | namespace RCB.JavaScript.Infrastructure 7 | { 8 | [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] 9 | public class SerilogMvcLoggingAttribute : ActionFilterAttribute 10 | { 11 | public override void OnActionExecuting(ActionExecutingContext context) 12 | { 13 | var diagnosticContext = context.HttpContext.RequestServices.GetService(); 14 | diagnosticContext.Set("ActionName", context.ActionDescriptor.DisplayName); 15 | diagnosticContext.Set("ActionId", context.ActionDescriptor.Id); 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /RCB.JavaScript/Infrastructure/ServiceBase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.ExceptionServices; 3 | 4 | namespace RCB.JavaScript.Infrastructure 5 | { 6 | public abstract class ServiceBase 7 | { 8 | protected static Result Ok() 9 | { 10 | return new Result(); 11 | } 12 | 13 | protected static Result Ok(T value) 14 | { 15 | return new Result(value); 16 | } 17 | 18 | protected static Result Error(params string[] errors) 19 | { 20 | return new Result(errors); 21 | } 22 | 23 | protected static Result Error(params string[] errors) 24 | { 25 | return new Result(errors); 26 | } 27 | 28 | protected static Result FatalError(params string[] errors) 29 | { 30 | return new Result(errors); 31 | } 32 | 33 | protected static Result FatalError(string errorMessage, Exception e) 34 | { 35 | #if DEBUG 36 | ExceptionDispatchInfo.Capture(e).Throw(); 37 | #endif 38 | return new Result(errorMessage); 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /RCB.JavaScript/Infrastructure/ServiceUser.cs: -------------------------------------------------------------------------------- 1 | namespace RCB.JavaScript.Infrastructure 2 | { 3 | public class ServiceUser 4 | { 5 | public string Login { get; set; } 6 | } 7 | } -------------------------------------------------------------------------------- /RCB.JavaScript/Infrastructure/WebSessionModels.cs: -------------------------------------------------------------------------------- 1 | namespace RCB.JavaScript.Infrastructure 2 | { 3 | /// 4 | /// Represents public session of the web application 5 | /// that can be shared in browser's window object. 6 | /// 7 | public class IsomorphicSessionData 8 | { 9 | public ServiceUser ServiceUser { get; set; } 10 | } 11 | 12 | /// 13 | /// Represents session for the server side rendering. 14 | /// 15 | public class SsrSessionData 16 | { 17 | public string Cookie { get; set; } 18 | } 19 | 20 | /// 21 | /// Represents the isomorphic session for web application. 22 | /// 23 | public class WebSessionContext 24 | { 25 | /// 26 | /// Contains public session that you can share in the browser's window object. 27 | /// 28 | public IsomorphicSessionData Isomorphic { get; set; } 29 | /// 30 | /// Contains private session that can be used only by NodeServices. 31 | /// 32 | public SsrSessionData Ssr { get; set; } 33 | } 34 | } -------------------------------------------------------------------------------- /RCB.JavaScript/Models/LoginModel.cs: -------------------------------------------------------------------------------- 1 | namespace RCB.JavaScript.Models 2 | { 3 | public class LoginModel 4 | { 5 | public string Login { get; set; } 6 | public string Password { get; set; } 7 | public bool RememberMe { get; set; } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /RCB.JavaScript/Models/PersonModel.cs: -------------------------------------------------------------------------------- 1 | namespace RCB.JavaScript.Models 2 | { 3 | public class PersonModel 4 | { 5 | public int Id { get; set; } 6 | public string FirstName { get; set; } 7 | public string LastName { get; set; } 8 | 9 | public PersonModel(int id, string firstName, string lastName) 10 | { 11 | Id = id; 12 | FirstName = firstName; 13 | LastName = lastName; 14 | } 15 | 16 | public PersonModel() 17 | { 18 | 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /RCB.JavaScript/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Hosting; 2 | using Microsoft.Extensions.Configuration; 3 | using Microsoft.Extensions.Hosting; 4 | using Serilog; 5 | using System; 6 | using System.IO; 7 | 8 | namespace RCB.JavaScript 9 | { 10 | public class Program 11 | { 12 | public static string EnvironmentName => 13 | Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Production"; 14 | 15 | public static Action BuildConfiguration = 16 | builder => builder 17 | .SetBasePath(Directory.GetCurrentDirectory()) 18 | .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) 19 | .AddJsonFile($"appsettings.{EnvironmentName}.json", optional: true) 20 | .AddEnvironmentVariables(); 21 | 22 | public static int Main(string[] args) 23 | { 24 | Console.WriteLine("Starting RCB.JavaScript..."); 25 | 26 | var builder = new ConfigurationBuilder(); 27 | BuildConfiguration(builder); 28 | 29 | Log.Logger = 30 | new LoggerConfiguration() 31 | .ReadFrom.Configuration(builder.Build()) 32 | .CreateLogger(); 33 | 34 | try 35 | { 36 | var hostBuilder = CreateHostBuilder(args, builder); 37 | 38 | var host = hostBuilder.Build(); 39 | 40 | host.Run(); 41 | 42 | return 0; 43 | } 44 | catch (Exception ex) 45 | { 46 | Log.Fatal(ex, "Host terminated unexpectedly."); 47 | return 1; 48 | } 49 | finally 50 | { 51 | Log.CloseAndFlush(); 52 | } 53 | } 54 | 55 | public static IHostBuilder CreateHostBuilder(string[] args, IConfigurationBuilder configurationBuilder) 56 | { 57 | return Host 58 | .CreateDefaultBuilder(args) 59 | .UseSerilog() 60 | .ConfigureWebHostDefaults(webBuilder => 61 | { 62 | webBuilder 63 | .UseIISIntegration() 64 | .ConfigureKestrel(serverOptions => 65 | { 66 | // Set properties and call methods on options. 67 | }) 68 | .UseConfiguration( 69 | configurationBuilder 70 | .AddJsonFile("hosting.json", optional: true, reloadOnChange: true) 71 | .AddJsonFile($"hosting.{EnvironmentName}.json", optional: true) 72 | .Build() 73 | ) 74 | .UseStartup(); 75 | }); 76 | } 77 | } 78 | } -------------------------------------------------------------------------------- /RCB.JavaScript/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:4250", 7 | "sslPort": 44354 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": true, 14 | "environmentVariables": { 15 | "ASPNETCORE_ENVIRONMENT": "Development" 16 | } 17 | }, 18 | "RCB.JavaScript": { 19 | "commandName": "Project", 20 | "launchBrowser": true, 21 | "environmentVariables": { 22 | "ASPNETCORE_ENVIRONMENT": "Development" 23 | }, 24 | "applicationUrl": "http://localhost:7000;https://localhost:7001" 25 | }, 26 | "Docker": { 27 | "commandName": "Docker", 28 | "launchBrowser": true, 29 | "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}", 30 | "publishAllPorts": true, 31 | "useSSL": true 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /RCB.JavaScript/RCB.JavaScript.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp3.1 5 | true 6 | false 7 | 8 | 9 | 10 | true 11 | 8ab1d22e-d48e-47e0-bfdb-216b1e87429d 12 | Linux 13 | . 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | PreserveNewest 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | %(DistFiles.Identity) 80 | PreserveNewest 81 | 82 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /RCB.JavaScript/README.FIRSTRUN.txt: -------------------------------------------------------------------------------- 1 | The project contains a fake authorization system, so you can change it to Identity or another. 2 | 3 | # Installation 4 | 0. Install of the latest stable Node.js: https://nodejs.org/en/ 5 | 1. At the first run you must close the project if it runs in Visual Studio or another IDE. 6 | Open project's folder in console and run command `npm install`. 7 | 2. Type `npm run build:dev` for development, it will compile the main and vendor bundles. 8 | 3. Build and run the project. 9 | 10 | # Modify WebPack vendor config 11 | If you modify the WebPack vendor config, you must manually recompile the vendor bundle. 12 | Type `npm run build:dev` to do this. 13 | 14 | # Known issues 15 | * WebPack Hot Module Replacement [HMR] doesn't work with IIS 16 | Will be fixed. Use Kestrel for development instead. 17 | * HTTP Error 500 18 | Probably you don't have the latest version of Node.js. 19 | * HTTP Error 502.5 20 | You must install the latest ".NET Core SDK" and ".NET Core Runtime" 21 | using this link: https://dotnet.microsoft.com/download 22 | * HTTP error 500 when hosted in Azure 23 | Set the "WEBSITE_NODE_DEFAULT_VERSION" to 6.11.2 in the "app settings" in Azure. 24 | 25 | # Other issues 26 | If you will have any issue with project starting, you can see errors in logs ("/logs" directory). 27 | Also feel free to use the issue tracker: https://github.com/NickMaev/react-core-boilerplate/issues 28 | Don't forget to mention the version of the React Core Boilerplate in your issue (e.g. TypeScript, JavaScript). -------------------------------------------------------------------------------- /RCB.JavaScript/Services/AccountService.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Http; 2 | using RCB.JavaScript.Infrastructure; 3 | 4 | namespace RCB.JavaScript.Services 5 | { 6 | public class AccountService : ServiceBase 7 | { 8 | public Result Login(HttpContext context, string login, string password) 9 | { 10 | context.Response.Cookies.Append(Constants.AuthorizationCookieKey, login); 11 | 12 | return Ok(new ServiceUser 13 | { 14 | Login = login 15 | }); 16 | } 17 | 18 | public Result Verify(HttpContext context) 19 | { 20 | var cookieValue = context.Request.Cookies[Constants.AuthorizationCookieKey]; 21 | if (string.IsNullOrEmpty(cookieValue)) 22 | return Error(); 23 | return Ok(new ServiceUser 24 | { 25 | Login = cookieValue 26 | }); 27 | } 28 | 29 | public Result Logout(HttpContext context) 30 | { 31 | context.Response.Cookies.Delete(Constants.AuthorizationCookieKey); 32 | return Ok(); 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /RCB.JavaScript/Services/PersonService.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using RCB.JavaScript.Infrastructure; 4 | using RCB.JavaScript.Models; 5 | 6 | namespace RCB.JavaScript.Services 7 | { 8 | public class PersonService : ServiceBase 9 | { 10 | protected static List PeopleList { get; } 11 | 12 | static PersonService() 13 | { 14 | PeopleList = new List 15 | { 16 | new PersonModel(1, "Bill", "Gates"), 17 | new PersonModel(2, "Jeffrey", "Richter"), 18 | new PersonModel(3, "Dennis", "Ritchie"), 19 | new PersonModel(4, "Ken", "Thompson"), 20 | new PersonModel(5, "Steve", "Jobs"), 21 | new PersonModel(6, "Steve", "Ballmer"), 22 | new PersonModel(7, "Alan", "Turing") 23 | }; 24 | } 25 | 26 | public virtual Result> Search(string term = null) 27 | { 28 | if (!string.IsNullOrEmpty(term)) 29 | { 30 | term = term.ToLower(); 31 | term = term.Trim(); 32 | 33 | var result = 34 | PeopleList 35 | .Where(x => 36 | x.FirstName.ToLower().Contains(term) || 37 | x.LastName.ToLower().Contains(term) 38 | ) 39 | .ToList(); 40 | 41 | return Ok(result); 42 | } 43 | 44 | return Ok(PeopleList); 45 | } 46 | 47 | public virtual Result Add(PersonModel model) 48 | { 49 | if (model == null) 50 | return Error(); 51 | if (string.IsNullOrEmpty(model.FirstName)) 52 | return Error("First name not defined."); 53 | if (string.IsNullOrEmpty(model.LastName)) 54 | return Error("Last name not defined."); 55 | 56 | TrimStrings(model); 57 | 58 | var personExists = 59 | PeopleList 60 | .Any(x => 61 | x.FirstName == model.FirstName && 62 | x.LastName == model.LastName 63 | ); 64 | if (personExists) 65 | { 66 | return Error("Person with the same first name and last name already exists."); 67 | } 68 | 69 | var newId = PeopleList.Max(x => x?.Id ?? 0) + 1; 70 | model.Id = newId; 71 | 72 | PeopleList.Add(model); 73 | 74 | return Ok(model.Id); 75 | } 76 | 77 | public virtual Result Update(PersonModel model) 78 | { 79 | if (model == null) 80 | return Error(); 81 | if (model.Id <= 0) 82 | return Error($"{model.Id} <= 0."); 83 | var person = PeopleList.Where(x => x.Id == model.Id).FirstOrDefault(); 84 | if (person == null) 85 | return Error($"Person with id = {model.Id} not found."); 86 | 87 | TrimStrings(model); 88 | 89 | var personExists = 90 | PeopleList 91 | .Any(x => 92 | x.Id != model.Id && 93 | x.FirstName == model.FirstName && 94 | x.LastName == model.LastName 95 | ); 96 | if (personExists) 97 | { 98 | return Error("Person with the same first name and last name already exists."); 99 | } 100 | 101 | person.FirstName = model.FirstName; 102 | person.LastName = model.LastName; 103 | 104 | return Ok(); 105 | } 106 | 107 | public virtual Result Delete(int id) 108 | { 109 | var unit = PeopleList.Where(x => x.Id == id).FirstOrDefault(); 110 | if (unit == null) 111 | return Error($"Can't find person with Id = {id}."); 112 | PeopleList.Remove(unit); 113 | return Ok(); 114 | } 115 | 116 | private static void TrimStrings(PersonModel model) 117 | { 118 | model.FirstName = model.FirstName.Trim(); 119 | model.LastName = model.LastName.Trim(); 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /RCB.JavaScript/Startup.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Builder; 2 | using Microsoft.AspNetCore.Hosting; 3 | using Microsoft.AspNetCore.SpaServices.Webpack; 4 | using Microsoft.Extensions.Configuration; 5 | using Microsoft.Extensions.DependencyInjection; 6 | using Microsoft.Extensions.Hosting; 7 | using RCB.JavaScript.Extensions; 8 | using RCB.JavaScript.Infrastructure; 9 | using RCB.JavaScript.Services; 10 | using Serilog; 11 | using Serilog.Context; 12 | 13 | namespace RCB.JavaScript 14 | { 15 | public class Startup 16 | { 17 | public Startup(IConfiguration configuration) 18 | { 19 | Configuration = configuration; 20 | } 21 | 22 | public IConfiguration Configuration { get; } 23 | 24 | // This method gets called by the runtime. Use this method to add services to the container. 25 | public void ConfigureServices(IServiceCollection services) 26 | { 27 | Configuration.GetSection("AppSettings").Bind(AppSettings.Default); 28 | 29 | services.AddLogging(loggingBuilder => 30 | loggingBuilder.AddSerilog(dispose: true)); 31 | 32 | services.AddControllersWithViews(opts => 33 | { 34 | opts.Filters.Add(); 35 | }); 36 | 37 | services.AddNodeServicesWithHttps(Configuration); 38 | 39 | #pragma warning disable CS0618 // Type or member is obsolete 40 | services.AddSpaPrerenderer(); 41 | #pragma warning restore CS0618 // Type or member is obsolete 42 | 43 | // Add your own services here. 44 | services.AddScoped(); 45 | services.AddScoped(); 46 | } 47 | 48 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 49 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 50 | { 51 | app.UseMiddleware(); 52 | 53 | // Adds an IP address to your log's context. 54 | app.Use(async (context, next) => { 55 | using (LogContext.PushProperty("IPAddress", context.Connection.RemoteIpAddress)) 56 | { 57 | await next.Invoke(); 58 | } 59 | }); 60 | 61 | // Build your own authorization system or use Identity. 62 | app.Use(async (context, next) => 63 | { 64 | var accountService = (AccountService)context.RequestServices.GetService(typeof(AccountService)); 65 | var verifyResult = accountService.Verify(context); 66 | if (!verifyResult.HasErrors) 67 | { 68 | context.Items.Add(Constants.HttpContextServiceUserItemKey, verifyResult.Value); 69 | } 70 | await next.Invoke(); 71 | // Do logging or other work that doesn't write to the Response. 72 | }); 73 | 74 | if (env.IsDevelopment()) 75 | { 76 | app.UseDeveloperExceptionPage(); 77 | #pragma warning disable CS0618 // Type or member is obsolete 78 | WebpackDevMiddleware.UseWebpackDevMiddleware(app, new WebpackDevMiddlewareOptions 79 | { 80 | HotModuleReplacement = true, 81 | ReactHotModuleReplacement = true 82 | }); 83 | #pragma warning restore CS0618 // Type or member is obsolete 84 | } 85 | else 86 | { 87 | app.UseExceptionHandler("/Main/Error"); 88 | app.UseHsts(); 89 | } 90 | 91 | app.UseDefaultFiles(); 92 | app.UseStaticFiles(); 93 | 94 | // Write streamlined request completion events, instead of the more verbose ones from the framework. 95 | // To use the default framework request logging instead, remove this line and set the "Microsoft" 96 | // level in appsettings.json to "Information". 97 | app.UseSerilogRequestLogging(); 98 | 99 | app.UseRouting(); 100 | app.UseEndpoints(endpoints => 101 | { 102 | endpoints.MapControllerRoute( 103 | name: "default", 104 | pattern: "{controller=Main}/{action=Index}/{id?}"); 105 | 106 | endpoints.MapFallbackToController("Index", "Main"); 107 | }); 108 | 109 | app.UseHttpsRedirection(); 110 | } 111 | } 112 | } -------------------------------------------------------------------------------- /RCB.JavaScript/Views/Main/Index.cshtml: -------------------------------------------------------------------------------- 1 | @inject Microsoft.AspNetCore.SpaServices.Prerendering.ISpaPrerenderer prerenderer 2 | 3 | @model RCB.JavaScript.Infrastructure.WebSessionContext 4 | 5 | @{ 6 | Layout = null; 7 | 8 | var prerenderResult = await prerenderer.RenderToString("ClientApp/dist/main-server", customDataParameter: Model); 9 | var isomorphicSessionDataJson = prerenderResult?.Globals?["session"]["isomorphic"]?.ToString(); 10 | var initialReduxStateJson = prerenderResult?.Globals?["initialReduxState"]?.ToString(); 11 | var completedTasksJson = prerenderResult?.Globals?["completedTasks"]?.ToString(); 12 | var helmetStringsPrerender = prerenderResult?.Globals?["helmetStrings"]?.ToString(); 13 | 14 | if (prerenderResult.StatusCode.HasValue) 15 | { 16 | Context.Response.StatusCode = prerenderResult?.StatusCode ?? 200; 17 | } 18 | } 19 | 20 | 21 | 22 | 23 | @Html.Raw(helmetStringsPrerender) 24 | 25 | 26 | 27 | 28 | 29 | @if (!AppSettings.Default.IsDevelopment) 30 | { 31 | 32 | } 33 | 34 | 39 | 40 | 41 | 42 |
43 |
44 | Loading 45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 | 53 | 58 | 59 | 60 | @* Save the request token in a div. CORS needs to make sure this token can't be read by javascript from other sources than ours *@ 61 |
62 |
63 | 64 |
@Html.Raw(prerenderResult?.Html)
65 | 66 | 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /RCB.JavaScript/Views/Shared/Error.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | ViewData["Title"] = "Error"; 3 | } 4 | 5 |

Error.

6 |

An error occurred while processing your request.

7 | 8 | @if (!string.IsNullOrEmpty((string)ViewData["RequestId"])) 9 | { 10 |

11 | Request ID: @ViewData["RequestId"] 12 |

13 | } 14 | 15 |

Development Mode

16 |

17 | Swapping to Development environment will display more detailed information about the error that occurred. 18 |

19 |

20 | Development environment should not be enabled in deployed applications, as it can result in sensitive information from exceptions being displayed to end users. For local debugging, development environment can be enabled by setting the ASPNETCORE_ENVIRONMENT environment variable to Development, and restarting the application. 21 |

22 | -------------------------------------------------------------------------------- /RCB.JavaScript/Views/_ViewImports.cshtml: -------------------------------------------------------------------------------- 1 | @using RCB.JavaScript 2 | @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers 3 | @addTagHelper *, Microsoft.AspNetCore.SpaServices 4 | -------------------------------------------------------------------------------- /RCB.JavaScript/Views/_ViewStart.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | Layout = "_Layout"; 3 | } 4 | -------------------------------------------------------------------------------- /RCB.JavaScript/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Serilog": { 3 | "Using": [ "Serilog.Sinks.Console" ], 4 | "MinimumLevel": { 5 | "Default": "Debug", 6 | "Override": { 7 | "Microsoft.Hosting.Lifetime": "Information", 8 | "Microsoft.AspNetCore.Server.Kestrel": "Warning", 9 | "Microsoft.AspNetCore.Mvc.Razor.Compilation.DefaultViewCompiler": "Information", 10 | "Microsoft.AspNetCore.DataProtection": "Information", 11 | "Microsoft.AspNetCore.Mvc.ModelBinding": "Warning", 12 | "Microsoft.AspNetCore.Routing": "Information", 13 | "Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker": "Information", 14 | "Microsoft.AspNetCore.StaticFiles.StaticFileMiddleware": "Information", 15 | "Microsoft.AspNetCore.Mvc.Infrastructure.SystemTextJsonResultExecutor": "Warning" 16 | } 17 | }, 18 | "Enrich": [ "FromLogContext", "WithMachineName", "WithProcessId", "WithThreadId" ], 19 | "WriteTo": [ 20 | { 21 | "Name": "Console", 22 | "Args": { 23 | "theme": "Serilog.Sinks.SystemConsole.Themes.AnsiConsoleTheme::Code, Serilog.Sinks.Console", 24 | "outputTemplate": "# [{Timestamp:HH:mm:ss} {Level:u3}] [{SourceContext}] {Message}{NewLine}{Exception}" 25 | } 26 | }, 27 | { 28 | "Name": "File", 29 | "Args": { 30 | "path": "Logs\\log.log", 31 | "rollingInterval": "Day", 32 | "restrictedToMinimumLevel": "Error", 33 | "formatter": "Serilog.Formatting.Json.JsonFormatter" 34 | } 35 | } 36 | ] 37 | }, 38 | "AllowedHosts": "*" 39 | } -------------------------------------------------------------------------------- /RCB.JavaScript/appsettings.Production.json: -------------------------------------------------------------------------------- 1 | { 2 | "Serilog": { 3 | "Using": [ "Serilog.Sinks.Console" ], 4 | "MinimumLevel": { 5 | "Default": "Information", 6 | "Override": { 7 | "System.Net.Http": "Warning", 8 | "Microsoft": "Error", 9 | "Microsoft.Hosting.Lifetime": "Information", 10 | "Serilog": "Error" 11 | } 12 | }, 13 | "Enrich": [ "FromLogContext", "WithMachineName", "WithProcessId", "WithThreadId" ], 14 | "WriteTo": [ 15 | { 16 | "Name": "Console", 17 | "Args": { 18 | "theme": "Serilog.Sinks.SystemConsole.Themes.AnsiConsoleTheme::Code, Serilog.Sinks.Console", 19 | "outputTemplate": "# [{Timestamp:HH:mm:ss} {Level:u3}] [{SourceContext}] {Message}{NewLine}{Exception}" 20 | } 21 | }, 22 | { 23 | "Name": "File", 24 | "Args": { 25 | "path": "Logs\\log.json", 26 | "rollingInterval": "Day", 27 | "formatter": "Serilog.Formatting.Json.JsonFormatter" 28 | } 29 | } 30 | ] 31 | }, 32 | "AllowedHosts": "*" 33 | } -------------------------------------------------------------------------------- /RCB.JavaScript/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "AppSettings": { 3 | }, 4 | "AllowedHosts": "*" 5 | } -------------------------------------------------------------------------------- /RCB.JavaScript/build.before.js: -------------------------------------------------------------------------------- 1 | var args = process.argv.slice(2); 2 | 3 | const modeTypes = { 4 | PRODUCTION: "PRODUCTION", 5 | DEVELOPMENT: "DEVELOPMENT", 6 | CLEAN: "CLEAN" 7 | }; 8 | 9 | var mode = modeTypes.DEVELOPMENT; 10 | 11 | // Note that those paths are mentioned in '.csproj' file in project building scenarios. 12 | var wwwrootDistDir = "wwwroot/dist"; 13 | var clientAppDistDir = "ClientApp/dist"; 14 | var productionBuildFileName = "production_build"; 15 | var productionBuildFilePath = wwwrootDistDir + "/" + productionBuildFileName; 16 | 17 | // Detect mode. 18 | args.forEach(arg => { 19 | var splitted = arg.toLowerCase().split("="); 20 | if (splitted.length < 2) { 21 | return; 22 | } 23 | var param = splitted[0].replace(/\-/g, ""); 24 | var value = splitted[1]; 25 | 26 | switch (param) { 27 | case "mode": 28 | mode = modeTypes.PRODUCTION.toLowerCase() === value ? modeTypes.PRODUCTION : modeTypes.DEVELOPMENT; 29 | } 30 | }); 31 | 32 | var fs = require("fs"); 33 | var fsAsync = fs.promises; 34 | var rimraf = require("rimraf"); 35 | 36 | const exists = (path) => { 37 | return fs.existsSync(path); 38 | }; 39 | 40 | const createEmptyFileAsync = async (filePath) => { 41 | let splitted = filePath.split("/"); 42 | 43 | if (splitted.length > 1) { 44 | // Create intermediate directories if necessary. 45 | 46 | var dirsToCreate = splitted.slice(0, splitted.length - 1); 47 | await fsAsync.mkdir(dirsToCreate.join("/"), { recursive: true }); 48 | } 49 | 50 | // Create empty file. 51 | fs.closeSync(fs.openSync(filePath, 'w')); 52 | }; 53 | 54 | /** 55 | * Clean up unnecessary files. 56 | * */ 57 | const cleanUpAsync = async () => { 58 | 59 | console.log("Deleting compiled scripts..."); 60 | 61 | await rimraf(wwwrootDistDir, (error) => { 62 | if (error) { 63 | console.log(error); 64 | } 65 | }); 66 | 67 | await rimraf(clientAppDistDir, (error) => { 68 | if (error) { 69 | console.log(error); 70 | } 71 | }); 72 | 73 | }; 74 | 75 | const startAsync = async () => { 76 | 77 | console.log("======= build.before.js mode: " + mode + " ======="); 78 | 79 | var doesProductionBuildFileExist = exists(productionBuildFilePath) 80 | 81 | var shouldClean = 82 | // Previous mode was "production". 83 | // So we need to clean up compiled scripts. 84 | doesProductionBuildFileExist || 85 | // Or we need to clean up after development mode 86 | // to remove those unnecessary files. 87 | mode == modeTypes.PRODUCTION || 88 | // Clean up only. 89 | mode == modeTypes.CLEAN; 90 | 91 | // Create indicator for next build operations. 92 | var shouldCreateProductionBuildFile = mode == modeTypes.PRODUCTION; 93 | 94 | if (shouldClean) { 95 | await cleanUpAsync(); 96 | } 97 | 98 | setTimeout(async () => { 99 | if (shouldCreateProductionBuildFile) { 100 | await createEmptyFileAsync(productionBuildFilePath); 101 | } 102 | }, 1000); 103 | }; 104 | 105 | startAsync(); -------------------------------------------------------------------------------- /RCB.JavaScript/download.js: -------------------------------------------------------------------------------- 1 | // This module allows you to download file using args --uri=[URI] --path=[FILEPATH] 2 | 3 | var args = process.argv.slice(2).map(arg => { 4 | 5 | var splitted = arg.toLowerCase().split("="); 6 | if (splitted.length < 2) { 7 | return; 8 | } 9 | 10 | var key = splitted[0].replace(/\-/g, ""); 11 | var value = splitted[1]; 12 | 13 | return { key, value }; 14 | }); 15 | 16 | var fs = require('fs'), 17 | request = require('request'); 18 | 19 | var download = function (uri, path) { 20 | 21 | request.head(uri, function (err, res, body) { 22 | 23 | var splitted = path.split("/"); 24 | 25 | var dirsToCreate = splitted.slice(0, splitted.length - 1); 26 | 27 | console.log(dirsToCreate.join("/")); 28 | 29 | fs.mkdir(dirsToCreate.join("/"), { recursive: true }, function () { }); 30 | 31 | console.log("Downloading file '" + uri + "' to path " + "'" + path + "'..."); 32 | 33 | request(uri) 34 | .pipe(fs.createWriteStream(path)) 35 | .on('close', function () { console.log("Download complete."); }); 36 | }); 37 | 38 | }; 39 | 40 | args.forEach((keyValuePair, index) => { 41 | switch (keyValuePair.key) { 42 | case "uri": 43 | var uri = keyValuePair.value; 44 | var path = args[index + 1].value; 45 | if (path) { 46 | if (!fs.existsSync(path)) { 47 | download(uri, path); 48 | } 49 | } 50 | break; 51 | } 52 | }); -------------------------------------------------------------------------------- /RCB.JavaScript/hosting.Production.json: -------------------------------------------------------------------------------- 1 | { "urls": "http://+:7000;https://+:7001" } -------------------------------------------------------------------------------- /RCB.JavaScript/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "ClientApp", 4 | "target": "es5", 5 | "experimentalDecorators": true, 6 | "allowSyntheticDefaultImports": true, 7 | "paths": { 8 | "@Store/*": [ "./store/*" ], 9 | "@Core/*": [ "./core/*" ], 10 | "@Components/*": [ "./components/*" ], 11 | "@Images/*": [ "./images/*" ], 12 | "@Styles/*": [ "./styles/*" ], 13 | "@Pages/*": [ "./pages/*" ], 14 | "@Layouts/*": [ "./layouts/*" ], 15 | "@Services/*": [ "./services/*" ], 16 | "@Utils": [ "./utils.ts" ] 17 | } 18 | }, 19 | "exclude": [ 20 | "bin", 21 | "node_modules", 22 | "AspNet" 23 | ] 24 | } -------------------------------------------------------------------------------- /RCB.JavaScript/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webapp", 3 | "private": true, 4 | "version": "0.0.0", 5 | "devDependencies": { 6 | "@babel/core": "7.9.6", 7 | "@babel/plugin-proposal-class-properties": "7.8.3", 8 | "@babel/plugin-proposal-decorators": "7.8.3", 9 | "@babel/plugin-proposal-optional-catch-binding": "7.8.3", 10 | "@babel/plugin-transform-runtime": "7.9.6", 11 | "@babel/preset-env": "7.9.6", 12 | "@babel/preset-react": "7.9.4", 13 | "@hot-loader/react-dom": "16.13.0", 14 | "aspnet-prerendering": "3.0.1", 15 | "aspnet-webpack-react": "4.0.0", 16 | "babel-loader": "8.1.0", 17 | "babel-plugin-import": "1.13.0", 18 | "case-sensitive-paths-webpack-plugin": "2.3.0", 19 | "css-loader": "3.5.3", 20 | "cssnano": "4.1.10", 21 | "file-loader": "6.0.0", 22 | "ignore-loader": "0.1.2", 23 | "mini-css-extract-plugin": "0.9.0", 24 | "node-noop": "1.0.0", 25 | "node-sass": "4.14.1", 26 | "optimize-css-assets-webpack-plugin": "5.0.3", 27 | "react-dev-utils": "10.2.1", 28 | "react-hot-loader": "4.12.21", 29 | "rimraf": "3.0.2", 30 | "sass-loader": "8.0.2", 31 | "style-loader": "1.2.1", 32 | "terser-webpack-plugin": "3.0.1", 33 | "url-loader": "4.1.0", 34 | "webpack": "4.43.0", 35 | "webpack-cli": "3.3.11", 36 | "webpack-dev-middleware": "3.7.2", 37 | "webpack-hot-middleware": "2.25.0", 38 | "webpack-merge": "4.2.2" 39 | }, 40 | "dependencies": { 41 | "@reduxjs/toolkit": "1.3.6", 42 | "aspnet-webpack": "3.0.0", 43 | "aspnet-webpack-react": "4.0.0", 44 | "awesome-debounce-promise": "2.1.0", 45 | "axios": "0.19.2", 46 | "bootstrap": "4.4.1", 47 | "connected-react-router": "6.8.0", 48 | "core-js": "^3.6.5", 49 | "custom-event-polyfill": "1.0.7", 50 | "domain-wait": "^1.3.0", 51 | "event-source-polyfill": "1.0.12", 52 | "formik": "2.1.4", 53 | "history": "4.10.1", 54 | "nval-tippy": "^1.0.40", 55 | "query-string": "6.12.1", 56 | "react": "16.13.1", 57 | "react-bootstrap": "1.0.1", 58 | "react-dom": "16.13.1", 59 | "react-helmet": "6.0.0", 60 | "react-paginating": "1.4.0", 61 | "react-redux": "7.2.0", 62 | "react-router": "5.1.2", 63 | "react-router-bootstrap": "0.25.0", 64 | "react-router-dom": "5.1.2", 65 | "react-toastify": "5.5.0", 66 | "redux": "4.0.5", 67 | "redux-thunk": "2.3.0", 68 | "sass": "1.26.5", 69 | "serialize-javascript": "^4.0.0" 70 | }, 71 | "scripts": { 72 | "postinstall": "node download.js --uri=https://github.com/sass/node-sass/releases/download/v4.14.1/linux-x64-72_binding.node --path=node_modules/node-sass/vendor/linux-x64-72/binding.node", 73 | "build:dev": "node build.before.js --mode=development && node ./node_modules/webpack/bin/webpack.js --config webpack.config.vendor.js && node ./node_modules/webpack/bin/webpack.js --config webpack.config.js", 74 | "build:prod": "node build.before.js --mode=production && node ./node_modules/webpack/bin/webpack.js --config webpack.config.vendor.js --env.prod=true && node ./node_modules/webpack/bin/webpack.js --config webpack.config.js --env.prod=true" 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /RCB.JavaScript/wwwroot/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NickMaev/react-core-boilerplate/6f6e2eedd82dc8454bde543364557eb00413273e/RCB.JavaScript/wwwroot/favicon.ico -------------------------------------------------------------------------------- /RCB.TypeScript/.dockerignore: -------------------------------------------------------------------------------- 1 | [B|b]in 2 | [O|o]bj -------------------------------------------------------------------------------- /RCB.TypeScript/AppSettings.cs: -------------------------------------------------------------------------------- 1 | namespace RCB.TypeScript 2 | { 3 | public class AppSettings 4 | { 5 | public static AppSettings Default { get; } 6 | 7 | protected AppSettings() 8 | { 9 | } 10 | 11 | static AppSettings() 12 | { 13 | Default = new AppSettings(); 14 | } 15 | 16 | public bool IsDevelopment => Program.EnvironmentName == "Development"; 17 | } 18 | } -------------------------------------------------------------------------------- /RCB.TypeScript/ClientApp/boot-client.tsx: -------------------------------------------------------------------------------- 1 | // Import polyfills. 2 | import "core-js/stable"; 3 | import "custom-event-polyfill"; 4 | import "event-source-polyfill"; 5 | 6 | // Import global styles. 7 | import "bootstrap/dist/css/bootstrap.min.css"; 8 | import "@Styles/main.scss"; 9 | import "@Styles/loaders/queryLoader.scss"; 10 | import "react-toastify/dist/ReactToastify.css"; 11 | 12 | // Other imports. 13 | import * as React from "react"; 14 | import * as ReactDOM from "react-dom"; 15 | import configureStore from "@Store/configureStore"; 16 | import SessionManager, { IIsomorphicSessionData, ISsrSessionData } from "@Core/session"; 17 | import { AppContainer } from "react-hot-loader"; 18 | import { Provider } from "react-redux"; 19 | import { ConnectedRouter } from "connected-react-router"; 20 | import { createBrowserHistory } from "history"; 21 | import { isNode, showApplicationLoader, hideApplicationLoader } from "@Utils"; 22 | import * as RoutesModule from "./routes"; 23 | import { IApplicationState } from "@Store/index"; 24 | let routes = RoutesModule.routes; 25 | 26 | function setupSession() { 27 | if (!isNode()) { 28 | SessionManager.resetSession(); 29 | SessionManager.initSession({ 30 | isomorphic: window["session"] as IIsomorphicSessionData, 31 | ssr: {} as ISsrSessionData 32 | }); 33 | } 34 | }; 35 | 36 | function setupGlobalPlugins() { 37 | // Use this function to configure plugins on the client side. 38 | }; 39 | 40 | function setupEvents() { 41 | 42 | showApplicationLoader(); 43 | 44 | document.addEventListener("DOMContentLoaded", () => { 45 | hideApplicationLoader(); 46 | }); 47 | }; 48 | 49 | // Create browser history to use in the Redux store. 50 | const baseUrl = document.getElementsByTagName("base")[0].getAttribute("href")!; 51 | const history = createBrowserHistory({ basename: baseUrl }); 52 | 53 | // Get the application-wide store instance, prepopulating with state from the server where available. 54 | const initialState = (window as any).initialReduxState as IApplicationState; 55 | const store = configureStore(history, initialState); 56 | 57 | function renderApp() { 58 | // This code starts up the React app when it runs in a browser. 59 | // It sets up the routing configuration and injects the app into a DOM element. 60 | ReactDOM.hydrate( 61 | 62 | 63 | 64 | 65 | , 66 | document.getElementById("react-app") 67 | ); 68 | } 69 | 70 | // Setup the application and render it. 71 | setupSession(); 72 | setupGlobalPlugins(); 73 | setupEvents(); 74 | renderApp(); 75 | 76 | // Allow Hot Module Replacement. 77 | if (module.hot) { 78 | module.hot.accept("./routes", () => { 79 | routes = require("./routes").routes; 80 | renderApp(); 81 | }); 82 | } -------------------------------------------------------------------------------- /RCB.TypeScript/ClientApp/boot-server.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import SessionManager, { IWebSessionContext } from "@Core/session"; 3 | import configureStore from "@Store/configureStore"; 4 | import { createServerRenderer, RenderResult } from "aspnet-prerendering"; 5 | import { replace } from "connected-react-router"; 6 | import { addDomainWait, getCompletedTasks } from "domain-wait"; 7 | import { createMemoryHistory } from "history"; 8 | import { renderToString } from "react-dom/server"; 9 | import { Helmet } from "react-helmet"; 10 | import { Provider } from "react-redux"; 11 | import { StaticRouter } from "react-router-dom"; 12 | import serializeJavascript from "serialize-javascript"; 13 | import { routes } from "./routes"; 14 | import responseContext from "@Core/responseContext"; 15 | 16 | var renderHelmet = (): string => { 17 | var helmetData = Helmet.renderStatic(); 18 | var helmetStrings = ""; 19 | for (var key in helmetData) { 20 | if (helmetData.hasOwnProperty(key)) { 21 | helmetStrings += helmetData[key].toString(); 22 | } 23 | } 24 | return helmetStrings; 25 | }; 26 | 27 | var createGlobals = (session: IWebSessionContext, initialReduxState: object, helmetStrings: string) => { 28 | return { 29 | completedTasks: getCompletedTasks(), 30 | session, 31 | 32 | // Serialize Redux State with "serialize-javascript" library 33 | // prevents XSS atack in the path of React Router via browser. 34 | initialReduxState: serializeJavascript(initialReduxState, { isJSON: true }), 35 | helmetStrings 36 | }; 37 | }; 38 | 39 | /** 40 | * Represents NodeJS params. 41 | * */ 42 | interface INodeParams { 43 | /** 44 | * Origin url. 45 | * */ 46 | origin: string; 47 | baseUrl: string; 48 | url: string; 49 | location: { path: string }; 50 | data: any; 51 | domainTasks: Promise; 52 | } 53 | 54 | export default createServerRenderer((params: INodeParams) => { 55 | 56 | SessionManager.resetSession(); 57 | SessionManager.initSession(params.data as IWebSessionContext); 58 | 59 | return new Promise((resolve, reject) => { 60 | 61 | // Prepare Redux store with in-memory history, and dispatch a navigation event. 62 | // corresponding to the incoming URL. 63 | const basename = params.baseUrl.substring(0, params.baseUrl.length - 1); // Remove trailing slash. 64 | const urlAfterBasename = params.url.substring(basename.length); 65 | const store = configureStore(createMemoryHistory()); 66 | store.dispatch(replace(urlAfterBasename)); 67 | 68 | // Prepare an instance of the application and perform an inital render that will 69 | // cause any async tasks (e.g., data access) to begin. 70 | const routerContext: any = {}; 71 | const app = ( 72 | 73 | 74 | 75 | ); 76 | 77 | const renderApp = (): string => { 78 | return renderToString(app); 79 | }; 80 | 81 | addDomainWait(params); 82 | 83 | renderApp(); 84 | 85 | // If there's a redirection, just send this information back to the host application. 86 | if (routerContext.url) { 87 | resolve({ 88 | redirectUrl: routerContext.url, 89 | globals: createGlobals(params.data as IWebSessionContext, store.getState(), renderHelmet()), 90 | statusCode: responseContext.statusCode 91 | }); 92 | return; 93 | } 94 | 95 | // Once any async tasks are done, we can perform the final render. 96 | // We also send the redux store state, so the client can continue execution where the server left off. 97 | params.domainTasks.then(() => { 98 | 99 | resolve({ 100 | html: renderApp(), 101 | globals: createGlobals(params.data, store.getState(), renderHelmet()), 102 | statusCode: responseContext.statusCode 103 | }); 104 | 105 | }, reject); // Also propagate any errors back into the host application. 106 | }); 107 | }); -------------------------------------------------------------------------------- /RCB.TypeScript/ClientApp/components/person/PersonEditor.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { IPersonModel } from "@Models/IPersonModel"; 3 | import { Formik, Field } from 'formik'; 4 | import FormValidator from "@Components/shared/FormValidator"; 5 | 6 | export interface IProps { 7 | data: IPersonModel; 8 | onSubmit: (data: IPersonModel) => void; 9 | children: (renderEditor: () => JSX.Element, submit: () => void) => JSX.Element; 10 | } 11 | 12 | const PersonEditor: React.FC = (props: IProps) => { 13 | 14 | const formValidator = React.useRef(null); 15 | 16 | const onSubmitForm = (values: IPersonModel) => { 17 | if (!formValidator.current.isValid()) { 18 | // Form is not valid. 19 | return; 20 | } 21 | props.onSubmit(values); 22 | } 23 | 24 | // This function will be passed to children components as a parameter. 25 | // It's necessary to build custom markup with controls outside this component. 26 | const renderEditor = (values: IPersonModel) => { 27 | 28 | return formValidator.current = x}> 29 |
30 | (x => x.firstName)}> 31 | {({ field }) => ( 32 | <> 33 | 34 | 45 | 46 | )} 47 | 48 |
49 |
50 | (x => x.lastName)}> 51 | {({ field }) => ( 52 | <> 53 | 54 | 65 | 66 | )} 67 | 68 |
69 |
; 70 | } 71 | 72 | return { 76 | onSubmitForm(values); 77 | }} 78 | > 79 | {({ values, handleSubmit }) => { 80 | // Let's say that the children element is a parametrizable function. 81 | // So we will pass other elements to this functional component as children 82 | // elements of this one: 83 | // 84 | // {(renderEditor, handleSubmit) => <> 85 | // {renderEditor()} 86 | // 87 | // } 88 | // . 89 | return props.children(() => renderEditor(values), handleSubmit); 90 | }} 91 | ; 92 | } 93 | 94 | export default PersonEditor; -------------------------------------------------------------------------------- /RCB.TypeScript/ClientApp/components/shared/AppRoute.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Route, RouteProps, Redirect } from "react-router"; 3 | import SessionManager from "@Core/session"; 4 | import responseContext from "@Core/responseContext"; 5 | 6 | export interface IProps extends RouteProps { 7 | layout: React.ComponentClass; 8 | statusCode?: number; 9 | } 10 | 11 | const AppRoute: React.FC = 12 | ({ component: Component, layout: Layout, statusCode: statusCode, path: Path, ...rest }: IProps) => { 13 | 14 | var isLoginPath = Path === "/login"; 15 | 16 | if (!SessionManager.isAuthenticated && !isLoginPath) { 17 | return ; 18 | } 19 | 20 | if (SessionManager.isAuthenticated && isLoginPath) { 21 | return ; 22 | } 23 | 24 | if (statusCode == null) { 25 | responseContext.statusCode = 200; 26 | } else { 27 | responseContext.statusCode = statusCode; 28 | } 29 | 30 | return ( 31 | 32 | 33 | 34 | )} />; 35 | }; 36 | 37 | export default AppRoute; -------------------------------------------------------------------------------- /RCB.TypeScript/ClientApp/components/shared/Footer.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | const Footer: React.FC = () => { 4 | return ; 9 | } 10 | 11 | export default Footer; -------------------------------------------------------------------------------- /RCB.TypeScript/ClientApp/components/shared/FormValidator.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { NValTippy } from "nval-tippy"; 3 | 4 | export interface IProps extends React.DetailedHTMLProps, HTMLFormElement> { 5 | children: any; 6 | } 7 | 8 | export default class FormValidator extends React.Component { 9 | constructor(props) { 10 | super(props); 11 | } 12 | 13 | private validator: NValTippy; 14 | private elForm: HTMLFormElement; 15 | 16 | public isValid = (): boolean => { 17 | return this.validator.isValid(); 18 | } 19 | 20 | componentDidMount() { 21 | this.validator = new NValTippy(this.elForm); 22 | } 23 | 24 | render() { 25 | return
this.elForm = x}>{this.props.children}
; 26 | } 27 | } -------------------------------------------------------------------------------- /RCB.TypeScript/ClientApp/components/shared/Paginator.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import Paginating from "react-paginating"; 3 | import { Pagination } from "react-bootstrap"; 4 | 5 | export interface IProps { 6 | totalResults: number; 7 | limitPerPage: number; 8 | currentPage: number; 9 | onChangePage: (pageNum: number) => void; 10 | } 11 | 12 | export default class Paginator extends React.Component { 13 | 14 | constructor(props) { 15 | super(props); 16 | } 17 | 18 | protected firstPageBtn: HTMLElement; 19 | protected lastPageBtn: HTMLElement; 20 | 21 | public setFirstPage = (): void => { 22 | var link = this.firstPageBtn.firstChild as HTMLLinkElement; 23 | link.click(); 24 | } 25 | 26 | public setLastPage = (): void => { 27 | var link = this.lastPageBtn.firstChild as HTMLLinkElement; 28 | link.click(); 29 | } 30 | 31 | render() { 32 | return 37 | {({ 38 | pages, 39 | currentPage, 40 | hasNextPage, 41 | hasPreviousPage, 42 | previousPage, 43 | nextPage, 44 | totalPages, 45 | getPageItemProps 46 | }) => ( 47 | 48 | 49 | this.firstPageBtn = x as any} 51 | key={`first`} 52 | {...getPageItemProps({ 53 | total: totalPages, 54 | pageValue: 1, 55 | onPageChange: (num, e) => this.props.onChangePage(num) 56 | })} 57 | > 58 | first 59 | 60 | 61 | {hasPreviousPage && ( 62 | this.props.onChangePage(num) 68 | })} 69 | > 70 | {`<`} 71 | 72 | )} 73 | 74 | {pages.map(page => { 75 | return this.props.onChangePage(num) 82 | })} 83 | > 84 | {page} 85 | ; 86 | })} 87 | 88 | {hasNextPage && ( 89 | this.props.onChangePage(num) 95 | })} 96 | > 97 | {`>`} 98 | 99 | )} 100 | 101 | this.lastPageBtn = x as any} 103 | key={`last`} 104 | {...getPageItemProps({ 105 | total: totalPages, 106 | pageValue: totalPages, 107 | onPageChange: (num, e) => this.props.onChangePage(num) 108 | })} 109 | > 110 | last 111 | 112 | 113 | 114 | )} 115 | 116 | } 117 | } -------------------------------------------------------------------------------- /RCB.TypeScript/ClientApp/components/shared/TopMenu.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { withRouter } from "react-router"; 3 | import { Redirect, NavLink } from "react-router-dom"; 4 | import AccountService from "@Services/AccountService"; 5 | import { Nav, Navbar, Dropdown } from "react-bootstrap"; 6 | import { LinkContainer } from 'react-router-bootstrap' 7 | import SessionManager from "@Core/session"; 8 | 9 | const TopMenu: React.FC = () => { 10 | 11 | const [isLogout, setLogout] = useState(false); 12 | 13 | const onClickSignOut = async () => { 14 | var accountService = new AccountService(); 15 | await accountService.logout(); 16 | setLogout(true); 17 | } 18 | 19 | if (isLogout) { 20 | return ; 21 | } 22 | 23 | return 24 | 25 | RCB.TypeScript 26 | 27 | 28 | 29 | 37 | 38 | 49 | 50 | 51 | ; 52 | } 53 | 54 | // Attach the React Router to the component to have an opportunity 55 | // to interract with it: use some navigation components, 56 | // have an access to React Router fields in the component's props, etc. 57 | export default withRouter(TopMenu); -------------------------------------------------------------------------------- /RCB.TypeScript/ClientApp/core/Result.ts: -------------------------------------------------------------------------------- 1 | export default class Result { 2 | public value: T; 3 | public errors: string[]; 4 | public get hasErrors(): boolean { 5 | return this.errors != null && Array.isArray(this.errors) && this.errors.length > 0; 6 | } 7 | 8 | constructor(value: T, ...errors: string[]) { 9 | this.value = value; 10 | this.errors = errors[0] == undefined || errors[0] == null ? [] : errors; 11 | } 12 | } -------------------------------------------------------------------------------- /RCB.TypeScript/ClientApp/core/ServiceBase.ts: -------------------------------------------------------------------------------- 1 | import Result from "./Result"; 2 | import Axios, { AxiosRequestConfig } from "axios"; 3 | import { transformUrl } from "domain-wait"; 4 | import queryString from "query-string"; 5 | import { isNode, showErrors, getNodeProcess } from "@Utils"; 6 | import SessionManager from "./session"; 7 | 8 | export interface IRequestOptions { 9 | url: string; 10 | data?: any; 11 | method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE"; 12 | } 13 | 14 | export interface ISendFormDataOptions { 15 | url: string; 16 | data: FormData; 17 | method: "POST" | "PUT" | "PATCH"; 18 | } 19 | 20 | /** 21 | * Represents base class of the isomorphic service. 22 | */ 23 | export abstract class ServiceBase { 24 | 25 | /** 26 | * Make request with JSON data. 27 | * @param opts 28 | */ 29 | public async requestJson(opts: IRequestOptions): Promise> { 30 | 31 | var axiosResult = null; 32 | var result = null; 33 | 34 | opts.url = transformUrl(opts.url); // Allow requests also for the Node. 35 | 36 | var processQuery = (url: string, data: any): string => { 37 | if (data) { 38 | return `${url}?${queryString.stringify(data)}`; 39 | } 40 | return url; 41 | }; 42 | 43 | var axiosRequestConfig : AxiosRequestConfig; 44 | 45 | if (isNode()) { 46 | 47 | const ssrSessionData = SessionManager.getSessionContext().ssr; 48 | const { cookie } = ssrSessionData; 49 | 50 | // Make SSR requests 'authorized' from the NodeServices to the web server. 51 | axiosRequestConfig = { 52 | headers: { 53 | Cookie: cookie 54 | } 55 | } 56 | } 57 | 58 | try { 59 | switch (opts.method) { 60 | case "GET": 61 | axiosResult = await Axios.get(processQuery(opts.url, opts.data), axiosRequestConfig); 62 | break; 63 | case "POST": 64 | axiosResult = await Axios.post(opts.url, opts.data, axiosRequestConfig); 65 | break; 66 | case "PUT": 67 | axiosResult = await Axios.put(opts.url, opts.data, axiosRequestConfig); 68 | break; 69 | case "PATCH": 70 | axiosResult = await Axios.patch(opts.url, opts.data, axiosRequestConfig); 71 | break; 72 | case "DELETE": 73 | axiosResult = await Axios.delete(processQuery(opts.url, opts.data), axiosRequestConfig); 74 | break; 75 | } 76 | result = new Result(axiosResult.data.value, ...axiosResult.data.errors); 77 | } catch (error) { 78 | result = new Result(null, error.message); 79 | } 80 | 81 | if (result.hasErrors) { 82 | showErrors(...result.errors); 83 | } 84 | 85 | return result; 86 | } 87 | 88 | /** 89 | * Allows you to send files to the server. 90 | * @param opts 91 | */ 92 | public async sendFormData(opts: ISendFormDataOptions): Promise> { 93 | let axiosResult = null; 94 | let result = null; 95 | 96 | opts.url = transformUrl(opts.url); // Allow requests also for Node. 97 | 98 | var axiosOpts = { 99 | headers: { 100 | 'Content-Type': 'multipart/form-data' 101 | } 102 | }; 103 | 104 | try { 105 | switch (opts.method) { 106 | case "POST": 107 | axiosResult = await Axios.post(opts.url, opts.data, axiosOpts); 108 | break; 109 | case "PUT": 110 | axiosResult = await Axios.put(opts.url, opts.data, axiosOpts); 111 | break; 112 | case "PATCH": 113 | axiosResult = await Axios.patch(opts.url, opts.data, axiosOpts); 114 | break; 115 | } 116 | result = new Result(axiosResult.data.value, ...axiosResult.data.errors); 117 | } catch (error) { 118 | result = new Result(null, error.message); 119 | } 120 | 121 | if (result.hasErrors) { 122 | showErrors(...result.errors); 123 | } 124 | 125 | return result; 126 | } 127 | } -------------------------------------------------------------------------------- /RCB.TypeScript/ClientApp/core/responseContext.ts: -------------------------------------------------------------------------------- 1 | const responseContext = { 2 | statusCode: 200 3 | }; 4 | 5 | export default responseContext; -------------------------------------------------------------------------------- /RCB.TypeScript/ClientApp/core/session/index.ts: -------------------------------------------------------------------------------- 1 | import { IWebSessionContext, IServiceUser } from "./models"; 2 | 3 | /** 4 | * User's session context manager. 5 | */ 6 | class SessionManager { 7 | 8 | private static _isInitialized: boolean = false; 9 | 10 | private static _context: IWebSessionContext = {}; 11 | 12 | public static resetSession(): void { 13 | this._isInitialized = false; 14 | this._context = {}; 15 | } 16 | 17 | public static initSession(sessionContext: IWebSessionContext): void { 18 | if (this._isInitialized) { 19 | throw Error("SessionManager: already initialized."); 20 | } 21 | 22 | this._context = (sessionContext || { 23 | isomorphic: {}, 24 | ssr: {} 25 | }) as IWebSessionContext; 26 | 27 | this._isInitialized = true; 28 | } 29 | 30 | private static throwIfNotInitialized() { 31 | if (!this._isInitialized) { 32 | throw Error("SessionManager: you have to call 'SessionManager.initSession' for initialization."); 33 | } 34 | } 35 | 36 | public static getSessionContext(): IWebSessionContext { 37 | this.throwIfNotInitialized(); 38 | return this._context; 39 | } 40 | 41 | public static getServiceUser(): IServiceUser { 42 | let context = this.getSessionContext(); 43 | if (context) { 44 | const isomorphicData = context.isomorphic; 45 | if (isomorphicData) { 46 | return isomorphicData.serviceUser; 47 | } else { 48 | throw Error("SessionManager: isomorphic session was not initialized.") 49 | } 50 | } 51 | throw Error("SessionManager: current session was not initialized.") 52 | } 53 | 54 | public static setServiceUser(serviceUser: IServiceUser) { 55 | let context = this.getSessionContext(); 56 | context.isomorphic.serviceUser = serviceUser; 57 | } 58 | 59 | public static get isAuthenticated(): boolean { 60 | return this.getServiceUser() != null; 61 | } 62 | } 63 | 64 | export default SessionManager; 65 | export * from "./models"; -------------------------------------------------------------------------------- /RCB.TypeScript/ClientApp/core/session/models.ts: -------------------------------------------------------------------------------- 1 | export interface IServiceUser { 2 | login: string; 3 | } 4 | 5 | /** 6 | * Session data which is used only for prerenderer. 7 | * */ 8 | export interface ISsrSessionData { 9 | cookie: string; 10 | } 11 | 12 | /** 13 | * Isomorphic session data. 14 | */ 15 | export interface IIsomorphicSessionData { 16 | serviceUser?: IServiceUser; 17 | } 18 | 19 | /** 20 | * Represents the session context. 21 | */ 22 | export interface IWebSessionContext { 23 | /** 24 | * Public data which is also used by browser. 25 | * */ 26 | isomorphic?: IIsomorphicSessionData; 27 | /** 28 | * Data for server side rendering. 29 | * */ 30 | ssr?: ISsrSessionData; 31 | } -------------------------------------------------------------------------------- /RCB.TypeScript/ClientApp/global.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | declare module '*.png'; 3 | declare module '*.jpg'; 4 | declare module '*.jpeg'; 5 | declare module '*.gif'; 6 | declare module '*.svg'; -------------------------------------------------------------------------------- /RCB.TypeScript/ClientApp/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NickMaev/react-core-boilerplate/6f6e2eedd82dc8454bde543364557eb00413273e/RCB.TypeScript/ClientApp/images/logo.png -------------------------------------------------------------------------------- /RCB.TypeScript/ClientApp/layouts/AuthorizedLayout.tsx: -------------------------------------------------------------------------------- 1 | import TopMenu from "@Components/shared/TopMenu"; 2 | import * as React from "react"; 3 | import "@Styles/authorizedLayout.scss"; 4 | import { ToastContainer } from "react-toastify"; 5 | import Footer from "@Components/shared/Footer"; 6 | 7 | interface IProps { 8 | children?: React.ReactNode; 9 | } 10 | 11 | type Props = IProps; 12 | 13 | export default class AuthorizedLayout extends React.Component { 14 | public render() { 15 | 16 | return
17 | 18 | {this.props.children} 19 | 20 |
21 |
; 22 | } 23 | } -------------------------------------------------------------------------------- /RCB.TypeScript/ClientApp/layouts/GuestLayout.tsx: -------------------------------------------------------------------------------- 1 | import "@Styles/guestLayout.scss"; 2 | import * as React from "react"; 3 | import { RouteComponentProps } from "react-router"; 4 | import { ToastContainer } from "react-toastify"; 5 | 6 | interface IProps { 7 | children: any; 8 | } 9 | 10 | type Props = IProps & RouteComponentProps ; 11 | 12 | export default class GuestLayout extends React.Component { 13 | public render() { 14 | 15 | return
16 |
17 | {this.props.children} 18 |
19 | 20 |
; 21 | } 22 | } -------------------------------------------------------------------------------- /RCB.TypeScript/ClientApp/models/ILoginModel.ts: -------------------------------------------------------------------------------- 1 | export interface ILoginModel { 2 | login: string; 3 | password: string; 4 | rememberMe?: boolean; 5 | } -------------------------------------------------------------------------------- /RCB.TypeScript/ClientApp/models/IPersonModel.ts: -------------------------------------------------------------------------------- 1 | export interface IPersonModel { 2 | id: number; 3 | firstName: string; 4 | lastName: string; 5 | } -------------------------------------------------------------------------------- /RCB.TypeScript/ClientApp/pages/HomePage.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { RouteComponentProps } from "react-router"; 3 | import { Helmet } from "react-helmet"; 4 | import logo from "@Images/logo.png"; 5 | 6 | type Props = RouteComponentProps<{}>; 7 | 8 | const HomePage: React.FC = () => { 9 | return
10 | 11 | Home page - RCB.TypeScript 12 | 13 | 14 |
15 | 16 | 17 | 18 |

Happy coding!

19 |
; 20 | } 21 | 22 | export default HomePage; -------------------------------------------------------------------------------- /RCB.TypeScript/ClientApp/pages/NotFoundPage.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { RouteComponentProps } from "react-router"; 3 | import { Helmet } from "react-helmet"; 4 | 5 | type Props = RouteComponentProps<{}>; 6 | 7 | const NotFoundPage: React.FC = () => { 8 | return
9 | 10 | Page not found - RCB.TypeScript 11 | 12 | 13 |
14 | 15 |

16 | 404 - Page not found 17 |

18 |
; 19 | } 20 | 21 | export default NotFoundPage; -------------------------------------------------------------------------------- /RCB.TypeScript/ClientApp/routes.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import GuestLayout from "@Layouts/GuestLayout"; 3 | import AuthorizedLayout from '@Layouts/AuthorizedLayout'; 4 | import LoginPage from '@Pages/LoginPage'; 5 | import AppRoute from "@Components/shared/AppRoute"; 6 | import HomePage from '@Pages/HomePage'; 7 | import ExamplesPage from '@Pages/ExamplesPage'; 8 | import { Switch } from 'react-router-dom'; 9 | import NotFoundPage from '@Pages/NotFoundPage'; 10 | 11 | export const routes = 12 | 13 | 14 | 15 | 16 | ; -------------------------------------------------------------------------------- /RCB.TypeScript/ClientApp/services/AccountService.ts: -------------------------------------------------------------------------------- 1 | import Result from "@Core/Result"; 2 | import { ILoginModel } from "@Models/ILoginModel"; 3 | import { ServiceBase } from "@Core/ServiceBase"; 4 | import SessionManager, { IServiceUser } from "@Core/session"; 5 | 6 | export default class AccountService extends ServiceBase { 7 | 8 | public async login(loginModel: ILoginModel) : Promise> { 9 | var result = await this.requestJson({ 10 | url: "api/Account/Login", 11 | method: "POST", 12 | data: loginModel 13 | }); 14 | 15 | if (!result.hasErrors) { 16 | SessionManager.setServiceUser(result.value); 17 | } 18 | 19 | return result; 20 | } 21 | 22 | public async logout(): Promise> { 23 | var result = await this.requestJson({ 24 | url: "api/Account/Logout", 25 | method: "POST" 26 | }); 27 | 28 | if (!result.hasErrors) { 29 | SessionManager.setServiceUser(null); 30 | } 31 | 32 | return result; 33 | } 34 | } -------------------------------------------------------------------------------- /RCB.TypeScript/ClientApp/services/PersonService.ts: -------------------------------------------------------------------------------- 1 | import Result from "@Core/Result"; 2 | import { ServiceBase } from "@Core/ServiceBase"; 3 | import SessionManager, { IServiceUser } from "@Core/session"; 4 | import { IPersonModel } from "@Models/IPersonModel"; 5 | 6 | export default class PersonService extends ServiceBase { 7 | 8 | public async search(term: string = null): Promise> { 9 | if (term == null) { 10 | term = ""; 11 | } 12 | var result = await this.requestJson({ 13 | url: `/api/Person/Search?term=${term}`, 14 | method: "GET" 15 | }); 16 | return result; 17 | } 18 | 19 | public async update(model: IPersonModel): Promise> { 20 | var result = await this.requestJson({ 21 | url: `/api/Person/${model.id}`, 22 | method: "PATCH", 23 | data: model 24 | }); 25 | return result; 26 | } 27 | 28 | public async delete(id: number): Promise> { 29 | var result = await this.requestJson({ 30 | url: `/api/Person/${id}`, 31 | method: "DELETE" 32 | }); 33 | return result; 34 | } 35 | 36 | public async add(model: IPersonModel): Promise> { 37 | var result = await this.requestJson({ 38 | url: "/api/Person/Add", 39 | method: "POST", 40 | data: model 41 | }); 42 | return result; 43 | } 44 | } -------------------------------------------------------------------------------- /RCB.TypeScript/ClientApp/store/configureStore.ts: -------------------------------------------------------------------------------- 1 | import * as StoreModule from "./index"; 2 | import thunk from "redux-thunk"; 3 | import { createStore, applyMiddleware, compose, combineReducers, StoreEnhancer, Store, StoreEnhancerStoreCreator, ReducersMapObject } from 'redux'; 4 | import { routerMiddleware, LOCATION_CHANGE } from "connected-react-router"; 5 | import { IApplicationState, reducers } from "@Store/index"; 6 | import { History } from "history"; 7 | 8 | export default function configureStore(history: History, initialState?: IApplicationState) { 9 | 10 | // Build middleware. These are functions that can process the actions before they reach the store. 11 | const windowIfDefined = typeof window === "undefined" ? null : window as any; 12 | 13 | // If devTools is installed, connect to it. 14 | const devToolsExtension = windowIfDefined && windowIfDefined.__REDUX_DEVTOOLS_EXTENSION__ as () => StoreEnhancer; 15 | const createStoreWithMiddleware = compose( 16 | applyMiddleware(thunk, routerMiddleware(history)), 17 | devToolsExtension ? devToolsExtension() : (next: StoreEnhancerStoreCreator) => next 18 | )(createStore); 19 | 20 | // Combine all reducers and instantiate the app-wide store instance. 21 | const allReducers = buildRootReducer(reducers, history); 22 | const store = createStoreWithMiddleware(allReducers, initialState as any) as Store; 23 | 24 | // Enable Webpack hot module replacement for reducers. 25 | if (module.hot) { 26 | module.hot.accept("@Store/index", () => { 27 | const nextRootReducer = require("@Store/index"); 28 | store.replaceReducer(buildRootReducer(nextRootReducer.reducers, history)); 29 | }); 30 | } 31 | 32 | return store; 33 | } 34 | 35 | const routerReducer = (history) => { 36 | const initialState = { 37 | location: history.location, 38 | action: history.action, 39 | }; 40 | return (state = initialState, arg: any = {}) => { 41 | if (arg.type === LOCATION_CHANGE) { 42 | return { ...state, ...arg.payload }; 43 | } 44 | return state; 45 | } 46 | }; 47 | 48 | function buildRootReducer(allReducers: ReducersMapObject, history) { 49 | return combineReducers({...allReducers, ...{ router: routerReducer(history) }} as any); 50 | } -------------------------------------------------------------------------------- /RCB.TypeScript/ClientApp/store/index.ts: -------------------------------------------------------------------------------- 1 | import * as loginStore from "@Store/loginStore"; 2 | import * as personStore from "@Store/personStore"; 3 | import { connect } from "react-redux"; 4 | 5 | // The top-level state object. 6 | export interface IApplicationState { 7 | login: loginStore.ILoginStoreState; 8 | person: personStore.IPersonStoreState; 9 | } 10 | 11 | // Whenever an action is dispatched, Redux will update each top-level application state property using 12 | // the reducer with the matching name. It's important that the names match exactly, and that the reducer 13 | // acts on the corresponding ApplicationState property type. 14 | export const reducers = { 15 | login: loginStore.reducer, 16 | person: personStore.reducer 17 | }; 18 | 19 | // This type can be used as a hint on action creators so that its 'dispatch' and 'getState' params are 20 | // correctly typed to match your store. 21 | export interface IAppThunkAction { 22 | (dispatch: (action: TAction) => void, getState: () => IApplicationState): void; 23 | } 24 | 25 | export interface IAppThunkActionAsync { 26 | (dispatch: (action: TAction) => void, getState: () => IApplicationState) : Promise 27 | } 28 | 29 | export function withStore>( 30 | component: TComponent, 31 | stateSelector: (state: IApplicationState) => TStoreState, 32 | actionCreators: TActionCreators 33 | ): TComponent { 34 | return connect(stateSelector, actionCreators)(component); 35 | } 36 | -------------------------------------------------------------------------------- /RCB.TypeScript/ClientApp/store/loginStore.ts: -------------------------------------------------------------------------------- 1 | import { createSlice, PayloadAction, Dispatch } from '@reduxjs/toolkit'; 2 | import { ILoginModel } from '@Models/ILoginModel'; 3 | import AccountService from '@Services/AccountService'; 4 | 5 | // Declare an interface of the store's state. 6 | export interface ILoginStoreState { 7 | isFetching: boolean; 8 | isLoginSuccess: boolean; 9 | } 10 | 11 | // Create the slice. 12 | const slice = createSlice({ 13 | name: "login", 14 | initialState: { 15 | isFetching: false, 16 | isLoginSuccess: false 17 | } as ILoginStoreState, 18 | reducers: { 19 | setFetching: (state, action: PayloadAction) => { 20 | state.isFetching = action.payload; 21 | }, 22 | setSuccess: (state, action: PayloadAction) => { 23 | state.isLoginSuccess = action.payload; 24 | } 25 | } 26 | }); 27 | 28 | // Export reducer from the slice. 29 | export const { reducer } = slice; 30 | 31 | // Define action creators. 32 | export const actionCreators = { 33 | login: (model: ILoginModel) => async (dispatch: Dispatch) => { 34 | dispatch(slice.actions.setFetching(true)); 35 | 36 | const service = new AccountService(); 37 | 38 | const result = await service.login(model); 39 | 40 | if (!result.hasErrors) { 41 | dispatch(slice.actions.setSuccess(true)); 42 | } 43 | 44 | dispatch(slice.actions.setFetching(false)); 45 | } 46 | }; 47 | -------------------------------------------------------------------------------- /RCB.TypeScript/ClientApp/store/personStore.ts: -------------------------------------------------------------------------------- 1 | import { createSlice, PayloadAction, Dispatch } from '@reduxjs/toolkit'; 2 | import { IPersonModel } from '@Models/IPersonModel'; 3 | import PersonService from '@Services/PersonService'; 4 | 5 | // Declare an interface of the store's state. 6 | export interface IPersonStoreState { 7 | isFetching: boolean; 8 | collection: IPersonModel[]; 9 | } 10 | 11 | // Create the slice. 12 | const slice = createSlice({ 13 | name: "person", 14 | initialState: { 15 | isFetching: false, 16 | collection: [] 17 | } as IPersonStoreState, 18 | reducers: { 19 | setFetching: (state, action: PayloadAction) => { 20 | state.isFetching = action.payload; 21 | }, 22 | setData: (state, action: PayloadAction) => { 23 | state.collection = action.payload; 24 | }, 25 | addData: (state, action: PayloadAction) => { 26 | state.collection = [...state.collection, action.payload]; 27 | }, 28 | updateData: (state, action: PayloadAction) => { 29 | // We need to clone collection (Redux-way). 30 | var collection = [...state.collection]; 31 | var entry = collection.find(x => x.id === action.payload.id); 32 | entry.firstName = action.payload.firstName; 33 | entry.lastName = action.payload.lastName; 34 | state.collection = [...state.collection]; 35 | }, 36 | deleteData: (state, action: PayloadAction<{ id: number }>) => { 37 | state.collection = state.collection.filter(x => x.id !== action.payload.id); 38 | } 39 | } 40 | }); 41 | 42 | // Export reducer from the slice. 43 | export const { reducer } = slice; 44 | 45 | // Define action creators. 46 | export const actionCreators = { 47 | search: (term?: string) => async (dispatch: Dispatch) => { 48 | dispatch(slice.actions.setFetching(true)); 49 | 50 | const service = new PersonService(); 51 | 52 | const result = await service.search(term); 53 | 54 | if (!result.hasErrors) { 55 | dispatch(slice.actions.setData(result.value)); 56 | } 57 | 58 | dispatch(slice.actions.setFetching(false)); 59 | 60 | return result; 61 | }, 62 | add: (model: IPersonModel) => async (dispatch: Dispatch) => { 63 | dispatch(slice.actions.setFetching(true)); 64 | 65 | const service = new PersonService(); 66 | 67 | const result = await service.add(model); 68 | 69 | if (!result.hasErrors) { 70 | model.id = result.value; 71 | dispatch(slice.actions.addData(model)); 72 | } 73 | 74 | dispatch(slice.actions.setFetching(false)); 75 | 76 | return result; 77 | }, 78 | update: (model: IPersonModel) => async (dispatch: Dispatch) => { 79 | dispatch(slice.actions.setFetching(true)); 80 | 81 | const service = new PersonService(); 82 | 83 | const result = await service.update(model); 84 | 85 | if (!result.hasErrors) { 86 | dispatch(slice.actions.updateData(model)); 87 | } 88 | 89 | dispatch(slice.actions.setFetching(false)); 90 | 91 | return result; 92 | }, 93 | delete: (id: number) => async (dispatch: Dispatch) => { 94 | dispatch(slice.actions.setFetching(true)); 95 | 96 | const service = new PersonService(); 97 | 98 | const result = await service.delete(id); 99 | 100 | if (!result.hasErrors) { 101 | dispatch(slice.actions.deleteData({ id })); 102 | } 103 | 104 | dispatch(slice.actions.setFetching(false)); 105 | 106 | return result; 107 | } 108 | }; 109 | -------------------------------------------------------------------------------- /RCB.TypeScript/ClientApp/styles/authorizedLayout.scss: -------------------------------------------------------------------------------- 1 | #authorizedLayout { 2 | background: white !important; 3 | } 4 | -------------------------------------------------------------------------------- /RCB.TypeScript/ClientApp/styles/guestLayout.scss: -------------------------------------------------------------------------------- 1 | #guestLayout { 2 | background: #f8f8f8; 3 | 4 | #loginContainer { 5 | width: 400px; 6 | position: absolute; 7 | left: 0; 8 | right: 0; 9 | top: 20%; 10 | margin-left: auto; 11 | margin-right: auto; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /RCB.TypeScript/ClientApp/styles/loaders/applicationLoader.css: -------------------------------------------------------------------------------- 1 | #applicationLoader { 2 | background: white; 3 | width: 100%; 4 | height: 100%; 5 | position: absolute; 6 | left: 0; 7 | right: 0; 8 | margin: 0 auto; 9 | text-align: center; 10 | z-index: 9999; 11 | display: table; 12 | } 13 | 14 | #applicationLoader > div { 15 | display: table-cell; 16 | vertical-align: middle; 17 | pointer-events: none; 18 | } 19 | 20 | #applicationLoader.hidden { 21 | display: none; 22 | } 23 | 24 | #applicationLoader .spinner { 25 | margin-left: -1px; 26 | width: 19px; 27 | text-align: center; 28 | display: inline-block; 29 | } 30 | 31 | #applicationLoader .spinner > div { 32 | width: 3px; 33 | height: 3px; 34 | background-color: #5c5c5c; 35 | border-radius: 100%; 36 | display: inline-block; 37 | -webkit-animation: sk-bouncedelay 1.4s infinite ease-in-out both; 38 | animation: sk-bouncedelay 1.4s infinite ease-in-out both; 39 | } 40 | 41 | #applicationLoader .spinner .bounce1 { 42 | -webkit-animation-delay: -0.32s; 43 | animation-delay: -0.32s; 44 | } 45 | 46 | #applicationLoader .spinner .bounce2 { 47 | -webkit-animation-delay: -0.16s; 48 | animation-delay: -0.16s; 49 | } 50 | 51 | @-webkit-keyframes sk-bouncedelay { 52 | 0%, 80%, 100% { 53 | -webkit-transform: scale(0) 54 | } 55 | 56 | 40% { 57 | -webkit-transform: scale(1.0) 58 | } 59 | } 60 | 61 | @keyframes sk-bouncedelay { 62 | 0%, 80%, 100% { 63 | -webkit-transform: scale(0); 64 | transform: scale(0); 65 | } 66 | 67 | 40% { 68 | -webkit-transform: scale(1.0); 69 | transform: scale(1.0); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /RCB.TypeScript/ClientApp/styles/loaders/queryLoader.scss: -------------------------------------------------------------------------------- 1 | #queryLoader { 2 | background: rgba(255, 255, 255, 0.7); 3 | width: 100%; 4 | height: 100%; 5 | position: absolute; 6 | left: 0; 7 | right: 0; 8 | margin: 0 auto; 9 | text-align: center; 10 | z-index: 9999; 11 | display: table; 12 | 13 | > div { 14 | display: table-cell; 15 | vertical-align: middle; 16 | pointer-events: none; 17 | } 18 | 19 | &.hidden { 20 | display: none; 21 | background: rgba(255, 255, 255, 0); 22 | } 23 | 24 | .spinner { 25 | width: 56px; 26 | height: 56px; 27 | border: 8px solid rgba(196, 196, 196, 0.25); 28 | border-top-color: rgb(31, 172, 255); 29 | border-radius: 50%; 30 | position: relative; 31 | animation: loader-rotate 1s linear infinite; 32 | top: 50%; 33 | margin: -28px auto 0; 34 | } 35 | } 36 | 37 | @keyframes loader-rotate { 38 | 0% { 39 | transform: rotate(0); 40 | } 41 | 42 | 100% { 43 | transform: rotate(360deg); 44 | } 45 | } -------------------------------------------------------------------------------- /RCB.TypeScript/ClientApp/styles/main.scss: -------------------------------------------------------------------------------- 1 | .footer { 2 | width: 100%; 3 | height: 80px; 4 | padding: 10px; 5 | border-top: 1px solid #e7e7e7; 6 | 7 | p { 8 | margin: 0; 9 | padding: 0; 10 | } 11 | } 12 | 13 | .layout{ 14 | height:100%; 15 | } 16 | 17 | ul.pagination{ 18 | &> li > span { 19 | cursor: pointer; 20 | } 21 | } 22 | 23 | .tippy-content { 24 | font-size: 14px; 25 | } 26 | 27 | label { 28 | margin: 5px !important; 29 | 30 | &.required { 31 | &:after { 32 | content: '*'; 33 | color: red; 34 | padding-left: 4px; 35 | font-size: 18px; 36 | position: absolute; 37 | } 38 | } 39 | } 40 | 41 | .navbar-dark .navbar-nav { 42 | .active > .nav-link, 43 | .nav-link.active, 44 | .nav-link.show, 45 | .show > .nav-link { 46 | color: #fff; 47 | } 48 | } -------------------------------------------------------------------------------- /RCB.TypeScript/ClientApp/utils.ts: -------------------------------------------------------------------------------- 1 | import { IAppThunkActionAsync } from "@Store/index"; 2 | import { Dispatch } from "@reduxjs/toolkit"; 3 | import { toast } from "react-toastify"; 4 | 5 | // NodeJs process. 6 | declare var process: any; 7 | 8 | /** 9 | * Is server prerendering by NodeJs. 10 | * There can't be any DOM elements such as: window, document, etc. 11 | */ 12 | export function isNode(): boolean { 13 | return typeof process === 'object' && process.versions && !!process.versions.node; 14 | } 15 | 16 | /** 17 | * Get NodeJs process. 18 | * */ 19 | export function getNodeProcess(): any { 20 | if (isNode()) { 21 | return process; 22 | } 23 | return null; 24 | } 25 | 26 | /** 27 | * Show error messages on page. 28 | * @param messages 29 | */ 30 | export function showErrors(...messages: string[]): void { 31 | 32 | messages.forEach(x => { 33 | if (!Array.isArray(x)) { 34 | toast.error(x); 35 | } 36 | else { 37 | (x as any).forEach((y: string) => toast.error(y)); 38 | } 39 | }); 40 | } 41 | 42 | /** 43 | * Show information message on page. 44 | * @param message 45 | */ 46 | export function showInfo(message: string): void { 47 | toast.info(message); 48 | } 49 | 50 | const getApplicationLoader = (): HTMLElement => { 51 | if (isNode()) { 52 | return null; 53 | } 54 | return document.getElementById("applicationLoader"); 55 | }; 56 | 57 | const getQueryLoader = (): HTMLElement => { 58 | if (isNode()) { 59 | return null; 60 | } 61 | return document.getElementById("queryLoader"); 62 | }; 63 | 64 | /** 65 | * Show main application loader. 66 | * */ 67 | export function showApplicationLoader(): void { 68 | let loader = getApplicationLoader(); 69 | if (loader) { 70 | loader.className = ""; 71 | } 72 | } 73 | 74 | /** 75 | * Hide main application loader. 76 | * */ 77 | export function hideApplicationLoader() { 78 | let loader = getApplicationLoader(); 79 | if (loader) { 80 | loader.className = "hidden"; 81 | } 82 | } 83 | 84 | /** 85 | * Show query loader. 86 | * */ 87 | export function showQueryLoader() { 88 | let loader = getQueryLoader(); 89 | if (loader) { 90 | loader.className = ""; 91 | } 92 | } 93 | 94 | /** 95 | * Hide query loader. 96 | * */ 97 | export function hideQueryLoader() { 98 | let loader = getQueryLoader(); 99 | if (loader) { 100 | loader.className = "hidden"; 101 | } 102 | } 103 | 104 | /** 105 | * Clone object. 106 | * @param object input object. 107 | */ 108 | export function clone(object: T): T { 109 | return JSON.parse(JSON.stringify(object)); 110 | } 111 | 112 | /** 113 | * Get promise from the store's action creator async function. 114 | * Use this to intercept the results of your requests. 115 | * @param asyncActionCreator 116 | */ 117 | export function getPromiseFromAction(asyncActionCreator: IAppThunkActionAsync): Promise { 118 | return (asyncActionCreator as any) as Promise; 119 | } 120 | 121 | /** 122 | * Get promise from the store's action creator async function. 123 | * Use this to intercept the results of your requests. 124 | * @param asyncActionCreator 125 | */ 126 | export function getPromiseFromActionCreator(asyncActionCreator: (dispatch: Dispatch) => Promise): Promise { 127 | return (asyncActionCreator as any) as Promise; 128 | } 129 | 130 | export function isObjectEmpty(obj): boolean { 131 | for (var key in obj) { 132 | if (obj.hasOwnProperty(key)) 133 | return false; 134 | } 135 | return true; 136 | } 137 | 138 | /** 139 | * Paginate an array for the client side. 140 | * @param array input array. 141 | * @param pageNumber page number. 142 | * @param limitPerPage entries per page. 143 | */ 144 | export function paginate(array: T[], pageNumber: number, limitPerPage: number): T[] { 145 | let rowOffset = Math.ceil((pageNumber - 1) * limitPerPage); 146 | return array.slice(rowOffset, rowOffset + limitPerPage); 147 | } -------------------------------------------------------------------------------- /RCB.TypeScript/Constants.cs: -------------------------------------------------------------------------------- 1 | namespace RCB.TypeScript 2 | { 3 | public static class Constants 4 | { 5 | public static string AuthorizationCookieKey => "Auth"; 6 | public static string HttpContextServiceUserItemKey => "ServiceUser"; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /RCB.TypeScript/Controllers/AccountController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.AspNetCore.Mvc; 3 | using Microsoft.Extensions.Configuration; 4 | using RCB.TypeScript.Models; 5 | using RCB.TypeScript.Services; 6 | 7 | namespace RCB.TypeScript.Controllers 8 | { 9 | [ApiController] 10 | [Route("api/[controller]")] 11 | public class AccountController : ControllerBase 12 | { 13 | private AccountService AccountService { get; set; } 14 | 15 | public AccountController(AccountService accountService) 16 | { 17 | AccountService = accountService; 18 | } 19 | 20 | [HttpPost("[action]")] 21 | public IActionResult Login([FromBody]LoginModel model) 22 | { 23 | var result = AccountService.Login(HttpContext, model.Login, model.Password); 24 | return Json(result); 25 | } 26 | 27 | [HttpPost("[action]")] 28 | public IActionResult Logout() 29 | { 30 | var result = AccountService.Logout(HttpContext); 31 | return Json(result); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /RCB.TypeScript/Controllers/ControllerBase.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | using Microsoft.AspNetCore.Mvc.Filters; 3 | using RCB.TypeScript.Infrastructure; 4 | 5 | namespace RCB.TypeScript.Controllers 6 | { 7 | public class ControllerBase : Controller 8 | { 9 | protected ServiceUser ServiceUser { get; set; } 10 | 11 | public override void OnActionExecuting(ActionExecutingContext context) 12 | { 13 | ControllerContext 14 | .HttpContext 15 | .Items 16 | .TryGetValue( 17 | Constants.HttpContextServiceUserItemKey, 18 | out object serviceUser); 19 | ServiceUser = serviceUser as ServiceUser; 20 | base.OnActionExecuting(context); 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /RCB.TypeScript/Controllers/MainController.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using System.Linq; 3 | using Microsoft.AspNetCore.Mvc; 4 | using RCB.TypeScript.Infrastructure; 5 | 6 | namespace RCB.TypeScript.Controllers 7 | { 8 | public class MainController : ControllerBase 9 | { 10 | public IActionResult Index() 11 | { 12 | var webSessionContext = new WebSessionContext 13 | { 14 | Ssr = new SsrSessionData 15 | { 16 | Cookie = string.Join(", ", Request.Cookies.Select(x => $"{x.Key}={x.Value};")) 17 | }, 18 | Isomorphic = new IsomorphicSessionData 19 | { 20 | ServiceUser = ServiceUser 21 | } 22 | }; 23 | 24 | return View(webSessionContext); 25 | } 26 | 27 | public IActionResult Error() 28 | { 29 | ViewData["RequestId"] = Activity.Current?.Id ?? HttpContext.TraceIdentifier; 30 | return View(); 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /RCB.TypeScript/Controllers/PersonController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | using RCB.TypeScript.Models; 3 | using RCB.TypeScript.Services; 4 | 5 | namespace RCB.TypeScript.Controllers 6 | { 7 | [ApiController] 8 | [Route("api/[controller]")] 9 | public class PersonController : ControllerBase 10 | { 11 | private PersonService PersonService { get; } 12 | 13 | public PersonController(PersonService personService) 14 | { 15 | PersonService = personService; 16 | } 17 | 18 | [HttpGet("[action]")] 19 | public IActionResult Search([FromQuery]string term = null) 20 | { 21 | return Json(PersonService.Search(term)); 22 | } 23 | 24 | [HttpPost("[action]")] 25 | public IActionResult Add(PersonModel model) 26 | { 27 | if (model == null) 28 | return BadRequest($"{nameof(model)} is null."); 29 | var result = PersonService.Add(model); 30 | return Json(result); 31 | } 32 | 33 | [HttpPatch("{id:int}")] 34 | public IActionResult Update(PersonModel model) 35 | { 36 | if (model == null) 37 | return BadRequest($"{nameof(model)} is null."); 38 | var result = PersonService.Update(model); 39 | return Json(result); 40 | } 41 | 42 | [HttpDelete("{id:int}")] 43 | public IActionResult Delete(int id) 44 | { 45 | if (id <= 0) 46 | return BadRequest($"{nameof(id)} <= 0."); 47 | var result = PersonService.Delete(id); 48 | return Json(result); 49 | } 50 | } 51 | } -------------------------------------------------------------------------------- /RCB.TypeScript/Dockerfile: -------------------------------------------------------------------------------- 1 | #See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging. 2 | 3 | FROM mcr.microsoft.com/dotnet/core/aspnet:3.1-buster-slim AS base 4 | RUN apt-get update -yq \ 5 | && apt-get install curl gnupg -yq \ 6 | && curl -sL https://deb.nodesource.com/setup_12.x | bash \ 7 | && apt-get install nodejs -yq 8 | WORKDIR /app 9 | EXPOSE 80 10 | EXPOSE 443 11 | 12 | FROM mcr.microsoft.com/dotnet/core/sdk:3.1-buster AS build 13 | RUN apt-get update -yq \ 14 | && apt-get install curl gnupg -yq \ 15 | && curl -sL https://deb.nodesource.com/setup_12.x | bash \ 16 | && apt-get install nodejs -yq 17 | WORKDIR /src 18 | COPY RCB.TypeScript.csproj RCB.TypeScript/ 19 | RUN dotnet restore "RCB.TypeScript/RCB.TypeScript.csproj" 20 | COPY . RCB.TypeScript/ 21 | WORKDIR "/src/RCB.TypeScript" 22 | RUN dotnet build "RCB.TypeScript.csproj" -c Release -o /app/build 23 | 24 | FROM build AS publish 25 | RUN dotnet publish "RCB.TypeScript.csproj" -c Release -o /app/publish 26 | 27 | FROM base AS final 28 | WORKDIR /app 29 | COPY --from=publish /app/publish . 30 | ENTRYPOINT ["dotnet", "RCB.TypeScript.dll"] -------------------------------------------------------------------------------- /RCB.TypeScript/Infrastructure/ExceptionMiddleware.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Microsoft.AspNetCore.Http; 4 | using Serilog; 5 | 6 | namespace RCB.TypeScript.Infrastructure 7 | { 8 | public class ExceptionMiddleware 9 | { 10 | private readonly RequestDelegate _next; 11 | 12 | public ExceptionMiddleware(RequestDelegate next) 13 | { 14 | _next = next; 15 | } 16 | 17 | public async Task InvokeAsync(HttpContext httpContext) 18 | { 19 | try 20 | { 21 | await _next(httpContext); 22 | } 23 | catch (Exception ex) 24 | { 25 | Log.Error(ex, "Exception was thrown during the request."); 26 | throw; 27 | } 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /RCB.TypeScript/Infrastructure/Result.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using Newtonsoft.Json; 4 | 5 | namespace RCB.TypeScript.Infrastructure 6 | { 7 | public class Result 8 | { 9 | public List Errors { get; set; } = new List(); 10 | 11 | [JsonIgnore] 12 | public bool HasErrors => Errors != null && Errors.Any(); 13 | 14 | public Result() 15 | { 16 | 17 | } 18 | 19 | public Result(params string[] errors) 20 | { 21 | this.Errors = errors.ToList(); 22 | } 23 | 24 | public void AddError(string error) 25 | { 26 | this.Errors.Add(error); 27 | } 28 | 29 | public void AddErrors(string[] errors) 30 | { 31 | this.Errors.AddRange(errors); 32 | } 33 | } 34 | 35 | public class Result : Result 36 | { 37 | public T Value { get; set; } 38 | 39 | public Result(T value) 40 | { 41 | this.Value = value; 42 | } 43 | 44 | public Result(params string[] errors) : base(errors) 45 | { 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /RCB.TypeScript/Infrastructure/SerilogMvcLoggingAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.AspNetCore.Mvc.Filters; 3 | using Microsoft.Extensions.DependencyInjection; 4 | using Serilog; 5 | 6 | namespace RCB.TypeScript.Infrastructure 7 | { 8 | [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] 9 | public class SerilogMvcLoggingAttribute : ActionFilterAttribute 10 | { 11 | public override void OnActionExecuting(ActionExecutingContext context) 12 | { 13 | var diagnosticContext = context.HttpContext.RequestServices.GetService(); 14 | diagnosticContext.Set("ActionName", context.ActionDescriptor.DisplayName); 15 | diagnosticContext.Set("ActionId", context.ActionDescriptor.Id); 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /RCB.TypeScript/Infrastructure/ServiceBase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.ExceptionServices; 3 | 4 | namespace RCB.TypeScript.Infrastructure 5 | { 6 | public abstract class ServiceBase 7 | { 8 | protected static Result Ok() 9 | { 10 | return new Result(); 11 | } 12 | 13 | protected static Result Ok(T value) 14 | { 15 | return new Result(value); 16 | } 17 | 18 | protected static Result Error(params string[] errors) 19 | { 20 | return new Result(errors); 21 | } 22 | 23 | protected static Result Error(params string[] errors) 24 | { 25 | return new Result(errors); 26 | } 27 | 28 | protected static Result FatalError(params string[] errors) 29 | { 30 | return new Result(errors); 31 | } 32 | 33 | protected static Result FatalError(string errorMessage, Exception e) 34 | { 35 | #if DEBUG 36 | ExceptionDispatchInfo.Capture(e).Throw(); 37 | #endif 38 | return new Result(errorMessage); 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /RCB.TypeScript/Infrastructure/ServiceUser.cs: -------------------------------------------------------------------------------- 1 | namespace RCB.TypeScript.Infrastructure 2 | { 3 | public class ServiceUser 4 | { 5 | public string Login { get; set; } 6 | } 7 | } -------------------------------------------------------------------------------- /RCB.TypeScript/Infrastructure/WebSessionModels.cs: -------------------------------------------------------------------------------- 1 | namespace RCB.TypeScript.Infrastructure 2 | { 3 | /// 4 | /// Represents public session of the web application 5 | /// that can be shared in browser's window object. 6 | /// 7 | public class IsomorphicSessionData 8 | { 9 | public ServiceUser ServiceUser { get; set; } 10 | } 11 | 12 | /// 13 | /// Represents session for the server side rendering. 14 | /// 15 | public class SsrSessionData 16 | { 17 | public string Cookie { get; set; } 18 | } 19 | 20 | /// 21 | /// Represents the isomorphic session for web application. 22 | /// 23 | public class WebSessionContext 24 | { 25 | /// 26 | /// Contains public session that you can share in the browser's window object. 27 | /// 28 | public IsomorphicSessionData Isomorphic { get; set; } 29 | /// 30 | /// Contains private session that can be used only by NodeServices. 31 | /// 32 | public SsrSessionData Ssr { get; set; } 33 | } 34 | } -------------------------------------------------------------------------------- /RCB.TypeScript/Models/LoginModel.cs: -------------------------------------------------------------------------------- 1 | namespace RCB.TypeScript.Models 2 | { 3 | public class LoginModel 4 | { 5 | public string Login { get; set; } 6 | public string Password { get; set; } 7 | public bool RememberMe { get; set; } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /RCB.TypeScript/Models/PersonModel.cs: -------------------------------------------------------------------------------- 1 | namespace RCB.TypeScript.Models 2 | { 3 | public class PersonModel 4 | { 5 | public int Id { get; set; } 6 | public string FirstName { get; set; } 7 | public string LastName { get; set; } 8 | 9 | public PersonModel(int id, string firstName, string lastName) 10 | { 11 | Id = id; 12 | FirstName = firstName; 13 | LastName = lastName; 14 | } 15 | 16 | public PersonModel() 17 | { 18 | 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /RCB.TypeScript/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Hosting; 2 | using Microsoft.Extensions.Configuration; 3 | using Microsoft.Extensions.Hosting; 4 | using Serilog; 5 | using System; 6 | using System.IO; 7 | 8 | namespace RCB.TypeScript 9 | { 10 | public class Program 11 | { 12 | public static string EnvironmentName => 13 | Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Production"; 14 | 15 | public static Action BuildConfiguration = 16 | builder => builder 17 | .SetBasePath(Directory.GetCurrentDirectory()) 18 | .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) 19 | .AddJsonFile($"appsettings.{EnvironmentName}.json", optional: true) 20 | .AddEnvironmentVariables(); 21 | 22 | public static int Main(string[] args) 23 | { 24 | Console.WriteLine("Starting RCB.TypeScript..."); 25 | 26 | var builder = new ConfigurationBuilder(); 27 | BuildConfiguration(builder); 28 | 29 | Log.Logger = 30 | new LoggerConfiguration() 31 | .ReadFrom.Configuration(builder.Build()) 32 | .CreateLogger(); 33 | 34 | try 35 | { 36 | var hostBuilder = CreateHostBuilder(args, builder); 37 | 38 | var host = hostBuilder.Build(); 39 | 40 | host.Run(); 41 | 42 | return 0; 43 | } 44 | catch (Exception ex) 45 | { 46 | Log.Fatal(ex, "Host terminated unexpectedly."); 47 | return 1; 48 | } 49 | finally 50 | { 51 | Log.CloseAndFlush(); 52 | } 53 | } 54 | 55 | public static IHostBuilder CreateHostBuilder(string[] args, IConfigurationBuilder configurationBuilder) 56 | { 57 | return Host 58 | .CreateDefaultBuilder(args) 59 | .UseSerilog() 60 | .ConfigureWebHostDefaults(webBuilder => 61 | { 62 | webBuilder 63 | .UseIISIntegration() 64 | .ConfigureKestrel(serverOptions => 65 | { 66 | // Set properties and call methods on options. 67 | }) 68 | .UseConfiguration( 69 | configurationBuilder 70 | .AddJsonFile("hosting.json", optional: true, reloadOnChange: true) 71 | .AddJsonFile($"hosting.{EnvironmentName}.json", optional: true) 72 | .Build() 73 | ) 74 | .UseStartup(); 75 | }); 76 | } 77 | } 78 | } -------------------------------------------------------------------------------- /RCB.TypeScript/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:4250", 7 | "sslPort": 44354 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": true, 14 | "environmentVariables": { 15 | "ASPNETCORE_ENVIRONMENT": "Development" 16 | } 17 | }, 18 | "RCB.TypeScript": { 19 | "commandName": "Project", 20 | "launchBrowser": true, 21 | "environmentVariables": { 22 | "ASPNETCORE_ENVIRONMENT": "Development" 23 | }, 24 | "applicationUrl": "http://localhost:7000;https://localhost:7001" 25 | }, 26 | "Docker": { 27 | "commandName": "Docker", 28 | "launchBrowser": true, 29 | "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}", 30 | "publishAllPorts": true, 31 | "useSSL": true 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /RCB.TypeScript/RCB.TypeScript.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp3.1 5 | true 6 | true 7 | 3.7 8 | false 9 | 10 | 11 | 12 | true 13 | 69dd95ae-881a-45b3-930e-89dd3138250a 14 | Linux 15 | . 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | PreserveNewest 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | %(DistFiles.Identity) 86 | PreserveNewest 87 | 88 | 89 | 90 | 91 | -------------------------------------------------------------------------------- /RCB.TypeScript/README.FIRSTRUN.txt: -------------------------------------------------------------------------------- 1 | The project contains a fake authorization system, so you can change it to Identity or another. 2 | 3 | # Installation 4 | 0. Install of the latest stable Node.js: https://nodejs.org/en/ 5 | 1. At the first run you must close the project if it runs in Visual Studio or another IDE. 6 | Open project's folder in console and run command `npm install`. 7 | 2. Type `npm run build:dev` for development, it will compile the main and vendor bundles. 8 | 3. Build and run the project. 9 | 10 | # Modify WebPack vendor config 11 | If you modify the WebPack vendor config, you must manually recompile the vendor bundle. 12 | Type `npm run build:dev` to do this. 13 | 14 | # Known issues 15 | * WebPack Hot Module Replacement [HMR] doesn't work with IIS 16 | Will be fixed. Use Kestrel for development instead. 17 | * HTTP Error 500 18 | Probably you don't have the latest version of Node.js. 19 | * HTTP Error 502.5 20 | You must install the latest ".NET Core SDK" and ".NET Core Runtime" 21 | using this link: https://dotnet.microsoft.com/download 22 | * HTTP error 500 when hosted in Azure 23 | Set the "WEBSITE_NODE_DEFAULT_VERSION" to 6.11.2 in the "app settings" in Azure. 24 | 25 | # Other issues 26 | If you will have any issue with project starting, you can see errors in logs ("/logs" directory). 27 | Also feel free to use the issue tracker: https://github.com/NickMaev/react-core-boilerplate/issues 28 | Don't forget to mention the version of the React Core Boilerplate in your issue (e.g. TypeScript, JavaScript). -------------------------------------------------------------------------------- /RCB.TypeScript/Services/AccountService.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Http; 2 | using RCB.TypeScript.Infrastructure; 3 | 4 | namespace RCB.TypeScript.Services 5 | { 6 | public class AccountService : ServiceBase 7 | { 8 | public Result Login(HttpContext context, string login, string password) 9 | { 10 | context.Response.Cookies.Append(Constants.AuthorizationCookieKey, login); 11 | 12 | return Ok(new ServiceUser 13 | { 14 | Login = login 15 | }); 16 | } 17 | 18 | public Result Verify(HttpContext context) 19 | { 20 | var cookieValue = context.Request.Cookies[Constants.AuthorizationCookieKey]; 21 | if (string.IsNullOrEmpty(cookieValue)) 22 | return Error(); 23 | return Ok(new ServiceUser 24 | { 25 | Login = cookieValue 26 | }); 27 | } 28 | 29 | public Result Logout(HttpContext context) 30 | { 31 | context.Response.Cookies.Delete(Constants.AuthorizationCookieKey); 32 | return Ok(); 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /RCB.TypeScript/Services/PersonService.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using RCB.TypeScript.Infrastructure; 4 | using RCB.TypeScript.Models; 5 | 6 | namespace RCB.TypeScript.Services 7 | { 8 | public class PersonService : ServiceBase 9 | { 10 | protected static List PeopleList { get; } 11 | 12 | static PersonService() 13 | { 14 | PeopleList = new List 15 | { 16 | new PersonModel(1, "Bill", "Gates"), 17 | new PersonModel(2, "Jeffrey", "Richter"), 18 | new PersonModel(3, "Dennis", "Ritchie"), 19 | new PersonModel(4, "Ken", "Thompson"), 20 | new PersonModel(5, "Steve", "Jobs"), 21 | new PersonModel(6, "Steve", "Ballmer"), 22 | new PersonModel(7, "Alan", "Turing") 23 | }; 24 | } 25 | 26 | public virtual Result> Search(string term = null) 27 | { 28 | if (!string.IsNullOrEmpty(term)) 29 | { 30 | term = term.ToLower(); 31 | term = term.Trim(); 32 | 33 | var result = 34 | PeopleList 35 | .Where(x => 36 | x.FirstName.ToLower().Contains(term) || 37 | x.LastName.ToLower().Contains(term) 38 | ) 39 | .ToList(); 40 | 41 | return Ok(result); 42 | } 43 | 44 | return Ok(PeopleList); 45 | } 46 | 47 | public virtual Result Add(PersonModel model) 48 | { 49 | if(model == null) 50 | return Error(); 51 | if(string.IsNullOrEmpty(model.FirstName)) 52 | return Error("First name not defined."); 53 | if(string.IsNullOrEmpty(model.LastName)) 54 | return Error("Last name not defined."); 55 | 56 | TrimStrings(model); 57 | 58 | var personExists = 59 | PeopleList 60 | .Any(x => 61 | x.FirstName == model.FirstName && 62 | x.LastName == model.LastName 63 | ); 64 | if(personExists) 65 | { 66 | return Error("Person with the same first name and last name already exists."); 67 | } 68 | 69 | var newId = PeopleList.Max(x => x?.Id ?? 0) + 1; 70 | model.Id = newId; 71 | 72 | PeopleList.Add(model); 73 | 74 | return Ok(model.Id); 75 | } 76 | 77 | public virtual Result Update(PersonModel model) 78 | { 79 | if (model == null) 80 | return Error(); 81 | if (model.Id <= 0) 82 | return Error($"{model.Id} <= 0."); 83 | var person = PeopleList.Where(x => x.Id == model.Id).FirstOrDefault(); 84 | if (person == null) 85 | return Error($"Person with id = {model.Id} not found."); 86 | 87 | TrimStrings(model); 88 | 89 | var personExists = 90 | PeopleList 91 | .Any(x => 92 | x.Id != model.Id && 93 | x.FirstName == model.FirstName && 94 | x.LastName == model.LastName 95 | ); 96 | if(personExists) 97 | { 98 | return Error("Person with the same first name and last name already exists."); 99 | } 100 | 101 | person.FirstName = model.FirstName; 102 | person.LastName = model.LastName; 103 | 104 | return Ok(); 105 | } 106 | 107 | public virtual Result Delete(int id) 108 | { 109 | var unit = PeopleList.Where(x => x.Id == id).FirstOrDefault(); 110 | if (unit == null) 111 | return Error($"Can't find person with Id = {id}."); 112 | PeopleList.Remove(unit); 113 | return Ok(); 114 | } 115 | 116 | private static void TrimStrings(PersonModel model) 117 | { 118 | model.FirstName = model.FirstName.Trim(); 119 | model.LastName = model.LastName.Trim(); 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /RCB.TypeScript/Startup.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Builder; 2 | using Microsoft.AspNetCore.Hosting; 3 | using Microsoft.AspNetCore.SpaServices.Webpack; 4 | using Microsoft.Extensions.Configuration; 5 | using Microsoft.Extensions.DependencyInjection; 6 | using Microsoft.Extensions.Hosting; 7 | using RCB.TypeScript.Extensions; 8 | using RCB.TypeScript.Infrastructure; 9 | using RCB.TypeScript.Services; 10 | using Serilog; 11 | using Serilog.Context; 12 | 13 | namespace RCB.TypeScript 14 | { 15 | public class Startup 16 | { 17 | public Startup(IConfiguration configuration) 18 | { 19 | Configuration = configuration; 20 | } 21 | 22 | public IConfiguration Configuration { get; } 23 | 24 | // This method gets called by the runtime. Use this method to add services to the container. 25 | public void ConfigureServices(IServiceCollection services) 26 | { 27 | Configuration.GetSection("AppSettings").Bind(AppSettings.Default); 28 | 29 | services.AddLogging(loggingBuilder => 30 | loggingBuilder.AddSerilog(dispose: true)); 31 | 32 | services.AddControllersWithViews(opts => 33 | { 34 | opts.Filters.Add(); 35 | }); 36 | 37 | services.AddNodeServicesWithHttps(Configuration); 38 | 39 | #pragma warning disable CS0618 // Type or member is obsolete 40 | services.AddSpaPrerenderer(); 41 | #pragma warning restore CS0618 // Type or member is obsolete 42 | 43 | // Add your own services here. 44 | services.AddScoped(); 45 | services.AddScoped(); 46 | } 47 | 48 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 49 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 50 | { 51 | app.UseMiddleware(); 52 | 53 | // Adds an IP address to your log's context. 54 | app.Use(async (context, next) => { 55 | using (LogContext.PushProperty("IPAddress", context.Connection.RemoteIpAddress)) 56 | { 57 | await next.Invoke(); 58 | } 59 | }); 60 | 61 | // Build your own authorization system or use Identity. 62 | app.Use(async (context, next) => 63 | { 64 | var accountService = (AccountService)context.RequestServices.GetService(typeof(AccountService)); 65 | var verifyResult = accountService.Verify(context); 66 | if (!verifyResult.HasErrors) 67 | { 68 | context.Items.Add(Constants.HttpContextServiceUserItemKey, verifyResult.Value); 69 | } 70 | await next.Invoke(); 71 | // Do logging or other work that doesn't write to the Response. 72 | }); 73 | 74 | if (env.IsDevelopment()) 75 | { 76 | app.UseDeveloperExceptionPage(); 77 | #pragma warning disable CS0618 // Type or member is obsolete 78 | WebpackDevMiddleware.UseWebpackDevMiddleware(app, new WebpackDevMiddlewareOptions 79 | { 80 | HotModuleReplacement = true, 81 | ReactHotModuleReplacement = true 82 | }); 83 | #pragma warning restore CS0618 // Type or member is obsolete 84 | } 85 | else 86 | { 87 | app.UseExceptionHandler("/Main/Error"); 88 | app.UseHsts(); 89 | } 90 | 91 | app.UseDefaultFiles(); 92 | app.UseStaticFiles(); 93 | 94 | // Write streamlined request completion events, instead of the more verbose ones from the framework. 95 | // To use the default framework request logging instead, remove this line and set the "Microsoft" 96 | // level in appsettings.json to "Information". 97 | app.UseSerilogRequestLogging(); 98 | 99 | app.UseRouting(); 100 | app.UseEndpoints(endpoints => 101 | { 102 | endpoints.MapControllerRoute( 103 | name: "default", 104 | pattern: "{controller=Main}/{action=Index}/{id?}"); 105 | 106 | endpoints.MapFallbackToController("Index", "Main"); 107 | }); 108 | 109 | app.UseHttpsRedirection(); 110 | } 111 | } 112 | } -------------------------------------------------------------------------------- /RCB.TypeScript/Views/Main/Index.cshtml: -------------------------------------------------------------------------------- 1 | @inject Microsoft.AspNetCore.SpaServices.Prerendering.ISpaPrerenderer prerenderer 2 | 3 | @model RCB.TypeScript.Infrastructure.WebSessionContext 4 | 5 | @{ 6 | Layout = null; 7 | 8 | var prerenderResult = await prerenderer.RenderToString("ClientApp/dist/main-server", customDataParameter: Model); 9 | var isomorphicSessionDataJson = prerenderResult?.Globals?["session"]["isomorphic"]?.ToString(); 10 | var initialReduxStateJson = prerenderResult?.Globals?["initialReduxState"]?.ToString(); 11 | var completedTasksJson = prerenderResult?.Globals?["completedTasks"]?.ToString(); 12 | var helmetStringsPrerender = prerenderResult?.Globals?["helmetStrings"]?.ToString(); 13 | 14 | if (prerenderResult.StatusCode.HasValue) 15 | { 16 | Context.Response.StatusCode = prerenderResult?.StatusCode ?? 200; 17 | } 18 | } 19 | 20 | 21 | 22 | 23 | @Html.Raw(helmetStringsPrerender) 24 | 25 | 26 | 27 | 28 | 29 | @if (!AppSettings.Default.IsDevelopment) 30 | { 31 | 32 | } 33 | 34 | 39 | 40 | 41 | 42 |
43 |
44 | Loading 45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 | 53 | 58 | 59 | 60 | @* Save the request token in a div. CORS needs to make sure this token can't be read by javascript from other sources than ours *@ 61 |
62 |
63 | 64 |
@Html.Raw(prerenderResult?.Html)
65 | 66 | 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /RCB.TypeScript/Views/Shared/Error.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | ViewData["Title"] = "Error"; 3 | } 4 | 5 |

Error.

6 |

An error occurred while processing your request.

7 | 8 | @if (!string.IsNullOrEmpty((string)ViewData["RequestId"])) 9 | { 10 |

11 | Request ID: @ViewData["RequestId"] 12 |

13 | } 14 | 15 |

Development Mode

16 |

17 | Swapping to Development environment will display more detailed information about the error that occurred. 18 |

19 |

20 | Development environment should not be enabled in deployed applications, as it can result in sensitive information from exceptions being displayed to end users. For local debugging, development environment can be enabled by setting the ASPNETCORE_ENVIRONMENT environment variable to Development, and restarting the application. 21 |

22 | -------------------------------------------------------------------------------- /RCB.TypeScript/Views/_ViewImports.cshtml: -------------------------------------------------------------------------------- 1 | @using RCB.TypeScript 2 | @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers 3 | @addTagHelper *, Microsoft.AspNetCore.SpaServices 4 | -------------------------------------------------------------------------------- /RCB.TypeScript/Views/_ViewStart.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | Layout = "_Layout"; 3 | } 4 | -------------------------------------------------------------------------------- /RCB.TypeScript/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Serilog": { 3 | "Using": [ "Serilog.Sinks.Console" ], 4 | "MinimumLevel": { 5 | "Default": "Debug", 6 | "Override": { 7 | "Microsoft.Hosting.Lifetime": "Information", 8 | "Microsoft.AspNetCore.Server.Kestrel": "Warning", 9 | "Microsoft.AspNetCore.Mvc.Razor.Compilation.DefaultViewCompiler": "Information", 10 | "Microsoft.AspNetCore.DataProtection": "Information", 11 | "Microsoft.AspNetCore.Mvc.ModelBinding": "Warning", 12 | "Microsoft.AspNetCore.Routing": "Information", 13 | "Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker": "Information", 14 | "Microsoft.AspNetCore.StaticFiles.StaticFileMiddleware": "Information", 15 | "Microsoft.AspNetCore.Mvc.Infrastructure.SystemTextJsonResultExecutor": "Warning" 16 | } 17 | }, 18 | "Enrich": [ "FromLogContext", "WithMachineName", "WithProcessId", "WithThreadId" ], 19 | "WriteTo": [ 20 | { 21 | "Name": "Console", 22 | "Args": { 23 | "theme": "Serilog.Sinks.SystemConsole.Themes.AnsiConsoleTheme::Code, Serilog.Sinks.Console", 24 | "outputTemplate": "# [{Timestamp:HH:mm:ss} {Level:u3}] [{SourceContext}] {Message}{NewLine}{Exception}" 25 | } 26 | }, 27 | { 28 | "Name": "File", 29 | "Args": { 30 | "path": "Logs\\log.log", 31 | "rollingInterval": "Day", 32 | "restrictedToMinimumLevel": "Error", 33 | "formatter": "Serilog.Formatting.Json.JsonFormatter" 34 | } 35 | } 36 | ] 37 | }, 38 | "AllowedHosts": "*" 39 | } -------------------------------------------------------------------------------- /RCB.TypeScript/appsettings.Production.json: -------------------------------------------------------------------------------- 1 | { 2 | "Serilog": { 3 | "Using": [ "Serilog.Sinks.Console" ], 4 | "MinimumLevel": { 5 | "Default": "Information", 6 | "Override": { 7 | "System.Net.Http": "Warning", 8 | "Microsoft": "Error", 9 | "Microsoft.Hosting.Lifetime": "Information", 10 | "Serilog": "Error" 11 | } 12 | }, 13 | "Enrich": [ "FromLogContext", "WithMachineName", "WithProcessId", "WithThreadId" ], 14 | "WriteTo": [ 15 | { 16 | "Name": "Console", 17 | "Args": { 18 | "theme": "Serilog.Sinks.SystemConsole.Themes.AnsiConsoleTheme::Code, Serilog.Sinks.Console", 19 | "outputTemplate": "# [{Timestamp:HH:mm:ss} {Level:u3}] [{SourceContext}] {Message}{NewLine}{Exception}" 20 | } 21 | }, 22 | { 23 | "Name": "File", 24 | "Args": { 25 | "path": "Logs\\log.json", 26 | "rollingInterval": "Day", 27 | "formatter": "Serilog.Formatting.Json.JsonFormatter" 28 | } 29 | } 30 | ] 31 | }, 32 | "AllowedHosts": "*" 33 | } -------------------------------------------------------------------------------- /RCB.TypeScript/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "AppSettings": { 3 | }, 4 | "AllowedHosts": "*" 5 | } -------------------------------------------------------------------------------- /RCB.TypeScript/build.before.js: -------------------------------------------------------------------------------- 1 | var args = process.argv.slice(2); 2 | 3 | const modeTypes = { 4 | PRODUCTION: "PRODUCTION", 5 | DEVELOPMENT: "DEVELOPMENT", 6 | CLEAN: "CLEAN" 7 | }; 8 | 9 | var mode = modeTypes.DEVELOPMENT; 10 | 11 | // Note that those paths are mentioned in '.csproj' file in project building scenarios. 12 | var wwwrootDistDir = "wwwroot/dist"; 13 | var clientAppDistDir = "ClientApp/dist"; 14 | var productionBuildFileName = "production_build"; 15 | var productionBuildFilePath = wwwrootDistDir + "/" + productionBuildFileName; 16 | 17 | // Detect mode. 18 | args.forEach(arg => { 19 | var splitted = arg.toLowerCase().split("="); 20 | if (splitted.length < 2) { 21 | return; 22 | } 23 | var param = splitted[0].replace(/\-/g, ""); 24 | var value = splitted[1]; 25 | 26 | switch (param) { 27 | case "mode": 28 | mode = modeTypes.PRODUCTION.toLowerCase() === value ? modeTypes.PRODUCTION : modeTypes.DEVELOPMENT; 29 | } 30 | }); 31 | 32 | var fs = require("fs"); 33 | var fsAsync = fs.promises; 34 | var rimraf = require("rimraf"); 35 | 36 | const exists = (path) => { 37 | return fs.existsSync(path); 38 | }; 39 | 40 | const createEmptyFileAsync = async (filePath) => { 41 | let splitted = filePath.split("/"); 42 | 43 | if (splitted.length > 1) { 44 | // Create intermediate directories if necessary. 45 | 46 | var dirsToCreate = splitted.slice(0, splitted.length - 1); 47 | await fsAsync.mkdir(dirsToCreate.join("/"), { recursive: true }); 48 | } 49 | 50 | // Create empty file. 51 | fs.closeSync(fs.openSync(filePath, 'w')); 52 | }; 53 | 54 | /** 55 | * Clean up unnecessary files. 56 | * */ 57 | const cleanUpAsync = async () => { 58 | 59 | console.log("Deleting compiled scripts..."); 60 | 61 | await rimraf(wwwrootDistDir, (error) => { 62 | if (error) { 63 | console.log(error); 64 | } 65 | }); 66 | 67 | await rimraf(clientAppDistDir, (error) => { 68 | if (error) { 69 | console.log(error); 70 | } 71 | }); 72 | 73 | }; 74 | 75 | const startAsync = async () => { 76 | 77 | console.log("======= build.before.js mode: " + mode + " ======="); 78 | 79 | var doesProductionBuildFileExist = exists(productionBuildFilePath) 80 | 81 | var shouldClean = 82 | // Previous mode was "production". 83 | // So we need to clean up compiled scripts. 84 | doesProductionBuildFileExist || 85 | // Or we need to clean up after development mode 86 | // to remove those unnecessary files. 87 | mode == modeTypes.PRODUCTION || 88 | // Clean up only. 89 | mode == modeTypes.CLEAN; 90 | 91 | // Create indicator for next build operations. 92 | var shouldCreateProductionBuildFile = mode == modeTypes.PRODUCTION; 93 | 94 | if (shouldClean) { 95 | await cleanUpAsync(); 96 | } 97 | 98 | setTimeout(async () => { 99 | if (shouldCreateProductionBuildFile) { 100 | await createEmptyFileAsync(productionBuildFilePath); 101 | } 102 | }, 1000); 103 | }; 104 | 105 | startAsync(); -------------------------------------------------------------------------------- /RCB.TypeScript/download.js: -------------------------------------------------------------------------------- 1 | // This module allows you to download file using args --uri=[URI] --path=[FILEPATH] 2 | 3 | var args = process.argv.slice(2).map(arg => { 4 | 5 | var splitted = arg.toLowerCase().split("="); 6 | if (splitted.length < 2) { 7 | return; 8 | } 9 | 10 | var key = splitted[0].replace(/\-/g, ""); 11 | var value = splitted[1]; 12 | 13 | return { key, value }; 14 | }); 15 | 16 | var fs = require('fs'), 17 | request = require('request'); 18 | 19 | var download = function (uri, path) { 20 | 21 | request.head(uri, function (err, res, body) { 22 | 23 | var splitted = path.split("/"); 24 | 25 | var dirsToCreate = splitted.slice(0, splitted.length - 1); 26 | 27 | console.log(dirsToCreate.join("/")); 28 | 29 | fs.mkdir(dirsToCreate.join("/"), { recursive: true }, function () { }); 30 | 31 | console.log("Downloading file '" + uri + "' to path " + "'" + path + "'..."); 32 | 33 | request(uri) 34 | .pipe(fs.createWriteStream(path)) 35 | .on('close', function () { console.log("Download complete."); }); 36 | }); 37 | 38 | }; 39 | 40 | args.forEach((keyValuePair, index) => { 41 | switch (keyValuePair.key) { 42 | case "uri": 43 | var uri = keyValuePair.value; 44 | var path = args[index + 1].value; 45 | if (path) { 46 | if (!fs.existsSync(path)) { 47 | download(uri, path); 48 | } 49 | } 50 | break; 51 | } 52 | }); -------------------------------------------------------------------------------- /RCB.TypeScript/hosting.Production.json: -------------------------------------------------------------------------------- 1 | { "urls": "https://+:7001;http://+:7000" } -------------------------------------------------------------------------------- /RCB.TypeScript/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webapp", 3 | "private": true, 4 | "version": "0.0.0", 5 | "devDependencies": { 6 | "@babel/core": "7.9.6", 7 | "@hot-loader/react-dom": "16.13.0", 8 | "@types/history": "4.7.5", 9 | "@types/react": "16.9.34", 10 | "@types/react-dom": "16.9.7", 11 | "@types/react-helmet": "5.0.15", 12 | "@types/react-redux": "7.1.8", 13 | "@types/react-router": "5.1.7", 14 | "@types/react-router-dom": "5.1.5", 15 | "@types/webpack": "4.41.12", 16 | "@types/webpack-env": "1.15.2", 17 | "aspnet-prerendering": "3.0.1", 18 | "babel-loader": "8.1.0", 19 | "babel-plugin-import": "1.13.0", 20 | "case-sensitive-paths-webpack-plugin": "2.3.0", 21 | "css-loader": "3.5.3", 22 | "cssnano": "4.1.10", 23 | "file-loader": "6.0.0", 24 | "fork-ts-checker-webpack-plugin": "4.1.3", 25 | "ignore-loader": "0.1.2", 26 | "mini-css-extract-plugin": "0.9.0", 27 | "node-noop": "1.0.0", 28 | "node-sass": "4.14.1", 29 | "optimize-css-assets-webpack-plugin": "5.0.3", 30 | "react-dev-utils": "10.2.1", 31 | "react-hot-loader": "4.12.21", 32 | "rimraf": "3.0.2", 33 | "sass-loader": "8.0.2", 34 | "style-loader": "1.2.1", 35 | "terser-webpack-plugin": "3.0.1", 36 | "ts-loader": "7.0.3", 37 | "ts-nameof": "4.2.2", 38 | "ts-nameof-loader": "1.0.2", 39 | "typescript": "3.8.3", 40 | "url-loader": "4.1.0", 41 | "webpack": "4.43.0", 42 | "webpack-cli": "3.3.11", 43 | "webpack-dev-middleware": "3.7.2", 44 | "webpack-hot-middleware": "2.25.0", 45 | "webpack-merge": "4.2.2", 46 | "aspnet-webpack-react": "4.0.0" 47 | }, 48 | "dependencies": { 49 | "@reduxjs/toolkit": "1.3.6", 50 | "aspnet-webpack": "3.0.0", 51 | "awesome-debounce-promise": "2.1.0", 52 | "axios": "0.19.2", 53 | "bootstrap": "^4.4.1", 54 | "connected-react-router": "6.8.0", 55 | "core-js": "^3.6.5", 56 | "custom-event-polyfill": "1.0.7", 57 | "domain-wait": "^1.3.0", 58 | "event-source-polyfill": "1.0.12", 59 | "formik": "2.1.4", 60 | "history": "4.10.1", 61 | "nval-tippy": "^1.0.40", 62 | "query-string": "6.12.1", 63 | "react": "16.13.1", 64 | "react-bootstrap": "1.0.1", 65 | "react-dom": "16.13.1", 66 | "react-helmet": "6.0.0", 67 | "react-paginating": "1.4.0", 68 | "react-redux": "7.2.0", 69 | "react-router": "5.1.2", 70 | "react-router-bootstrap": "0.25.0", 71 | "react-router-dom": "5.1.2", 72 | "react-toastify": "5.5.0", 73 | "redux": "4.0.5", 74 | "redux-thunk": "2.3.0", 75 | "sass": "1.26.5", 76 | "serialize-javascript": "^4.0.0" 77 | }, 78 | "scripts": { 79 | "postinstall": "node download.js --uri=https://github.com/sass/node-sass/releases/download/v4.14.1/linux-x64-72_binding.node --path=node_modules/node-sass/vendor/linux-x64-72/binding.node", 80 | "build:dev": "node build.before.js --mode=development && node ./node_modules/webpack/bin/webpack.js --config webpack.config.vendor.js && node ./node_modules/webpack/bin/webpack.js --config webpack.config.js", 81 | "build:prod": "node build.before.js --mode=production && node ./node_modules/webpack/bin/webpack.js --config webpack.config.vendor.js --env.prod=true && node ./node_modules/webpack/bin/webpack.js --config webpack.config.js --env.prod=true" 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /RCB.TypeScript/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "ClientApp", 4 | "module": "es2015", 5 | "moduleResolution": "node", 6 | "target": "es5", 7 | "jsx": "react", 8 | "sourceMap": true, 9 | "experimentalDecorators": true, 10 | "skipDefaultLibCheck": true, 11 | "strict": false, 12 | "noImplicitReturns": true, 13 | "allowUnusedLabels": false, 14 | "allowSyntheticDefaultImports": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "lib": [ "es6", "dom" ], 17 | "types": [ "webpack-env" ], 18 | "paths": { 19 | "@Store/*": [ "./store/*" ], 20 | "@Core/*": [ "./core/*" ], 21 | "@Components/*": [ "./components/*" ], 22 | "@Images/*": [ "./images/*" ], 23 | "@Styles/*": [ "./styles/*" ], 24 | "@Models/*": [ "./models/*" ], 25 | "@Pages/*": [ "./pages/*" ], 26 | "@Layouts/*": [ "./layouts/*" ], 27 | "@Services/*": [ "./services/*" ], 28 | "@Utils": [ "./utils.ts" ] 29 | } 30 | }, 31 | "exclude": [ 32 | "bin", 33 | "node_modules", 34 | "AspNet" 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /RCB.TypeScript/wwwroot/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NickMaev/react-core-boilerplate/6f6e2eedd82dc8454bde543364557eb00413273e/RCB.TypeScript/wwwroot/favicon.ico -------------------------------------------------------------------------------- /RCB.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.29709.97 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RCB.TypeScript", "RCB.TypeScript\RCB.TypeScript.csproj", "{4C50845F-9A28-4D5D-A23C-01876AD2931D}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RCB.JavaScript", "RCB.JavaScript\RCB.JavaScript.csproj", "{B0280670-40E0-411D-AFBF-0949269380BD}" 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 | {4C50845F-9A28-4D5D-A23C-01876AD2931D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 17 | {4C50845F-9A28-4D5D-A23C-01876AD2931D}.Debug|Any CPU.Build.0 = Debug|Any CPU 18 | {4C50845F-9A28-4D5D-A23C-01876AD2931D}.Release|Any CPU.ActiveCfg = Release|Any CPU 19 | {4C50845F-9A28-4D5D-A23C-01876AD2931D}.Release|Any CPU.Build.0 = Release|Any CPU 20 | {B0280670-40E0-411D-AFBF-0949269380BD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {B0280670-40E0-411D-AFBF-0949269380BD}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {B0280670-40E0-411D-AFBF-0949269380BD}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {B0280670-40E0-411D-AFBF-0949269380BD}.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 = {C7D66D96-5A29-442E-882D-EC542D510714} 30 | EndGlobalSection 31 | EndGlobal 32 | -------------------------------------------------------------------------------- /TemplateIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NickMaev/react-core-boilerplate/6f6e2eedd82dc8454bde543364557eb00413273e/TemplateIcon.png -------------------------------------------------------------------------------- /azure-pipelines.yml: -------------------------------------------------------------------------------- 1 | # ASP.NET Core (.NET Framework) 2 | # Build and test ASP.NET Core projects targeting the full .NET Framework. 3 | # Add steps that publish symbols, save build artifacts, and more: 4 | # https://docs.microsoft.com/azure/devops/pipelines/languages/dotnet-core 5 | 6 | trigger: 7 | - master 8 | 9 | pool: 10 | vmImage: 'windows-latest' 11 | 12 | variables: 13 | solution: '**/*.sln' 14 | buildPlatform: 'Any CPU' 15 | buildConfiguration: 'Release' 16 | 17 | steps: 18 | - task: NuGetToolInstaller@1 19 | 20 | - task: NuGetCommand@2 21 | inputs: 22 | restoreSolution: '$(solution)' 23 | 24 | - task: VSBuild@1 25 | inputs: 26 | solution: '$(solution)' 27 | msbuildArgs: '/p:DeployOnBuild=true /p:WebPublishMethod=Package /p:PackageAsSingleFile=true /p:SkipInvalidConfigurations=true /p:DesktopBuildPackageLocation="$(build.artifactStagingDirectory)\WebApp.zip" /p:DeployIisAppPath="Default Web Site"' 28 | platform: '$(buildPlatform)' 29 | configuration: '$(buildConfiguration)' 30 | 31 | - task: VSTest@2 32 | inputs: 33 | platform: '$(buildPlatform)' 34 | configuration: '$(buildConfiguration)' 35 | --------------------------------------------------------------------------------