├── .babelrc ├── .env ├── .env.development ├── .env.production ├── .env.test ├── .eslintrc.json ├── .gitignore ├── FakeApi ├── fakeApi.js └── help.txt ├── README.md ├── package.json ├── provider ├── server │ ├── development.js │ └── production.js ├── setup │ ├── constant.js │ ├── envLoader.js │ ├── fileVersion.js │ └── rateLimit.js └── webpack │ ├── development.js │ └── production.js ├── public ├── app-icon.png ├── asset │ └── img │ │ ├── error-404.png │ │ ├── loading.gif │ │ ├── rssr-logo-block.png │ │ └── rssr-logo.png ├── manifest.json └── sub-scripts.js └── src ├── App ├── App.js ├── Error404 │ └── Error404.js ├── Home │ ├── Home.js │ └── home.scss ├── Post │ └── Post.js └── Sign │ └── Sign.js ├── Component ├── Auth │ ├── InvalidUser.js │ ├── LoadingUser.js │ ├── ResetPassword.js │ ├── SignIn │ │ ├── ForgetPasswordForm.js │ │ ├── SignIn.js │ │ └── SignInForm.js │ ├── SignUp.js │ ├── UpdatedUser.js │ ├── ValidUser.js │ └── __action │ │ ├── authentication.js │ │ ├── firstSetup.js │ │ ├── setUserIsGuest.js │ │ ├── signingIn.js │ │ ├── signingOut.js │ │ └── updateUserDetail.js ├── Menu │ ├── Menu.js │ └── menu.scss └── OverLoading │ ├── OverLoading.js │ ├── __action │ └── toggleOverLoading.js │ └── overLoading.scss ├── Partial ├── Router │ └── Router.js ├── fetcher │ ├── DefaultErrors.js │ ├── clientFetcher.js │ ├── fetcher.js │ └── serverFetcher.js └── skeleton │ ├── debugLog.js │ ├── skeleton.js │ ├── skeletonClientProvider.js │ └── skeletonServerProvider.js ├── render ├── Template │ ├── Error.js │ └── Index.js ├── client.js └── server │ ├── fetchProvider.js │ ├── initialize.js │ ├── render.js │ └── server.js └── setup ├── api.js ├── axiosConfig.js ├── browserHistory.js ├── constant.js ├── localStorage.js ├── route.js ├── routeMap.js ├── store.js ├── style ├── public.scss └── var.scss └── utility ├── badConnectionAlert.js ├── convertErrorToResponse.js ├── errorLogger.js ├── fetching.js ├── isErrorData.js ├── isValidUser.js ├── jumpScrollToTop.js ├── random.js ├── responseValidation.js └── samplejQueryPlugin.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-react", 4 | ["@babel/preset-env", {"useBuiltIns": false}] 5 | ], 6 | "plugins": [ 7 | "@babel/plugin-transform-runtime", 8 | "@babel/plugin-proposal-object-rest-spread", 9 | ["@babel/plugin-proposal-decorators", { "legacy": true }], 10 | ["@babel/plugin-proposal-class-properties", { "loose" : true }] 11 | ] 12 | } -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | FILE_VERSION_TYPE='time' -------------------------------------------------------------------------------- /.env.development: -------------------------------------------------------------------------------- 1 | PORT=8000 2 | RSSR_REDUX_DEV_TOOLS=true 3 | RSSR_FETCHER_DEBUG=false 4 | RSSR_SKELETON_DEBUG=false 5 | API_HOST_IN_CLIENT='http://localhost:8000/fake-api' 6 | API_HOST_IN_SERVER='http://localhost:8000/fake-api' 7 | -------------------------------------------------------------------------------- /.env.production: -------------------------------------------------------------------------------- 1 | PORT=3000 2 | HOST='0.0.0.0' 3 | RSSR_REDUX_DEV_TOOLS=false 4 | RSSR_FETCHER_DEBUG=false 5 | RSSR_SKELETON_DEBUG=false 6 | API_HOST_IN_CLIENT='http://localhost:3000/fake-api' 7 | API_HOST_IN_SERVER='http://localhost:3000/fake-api' -------------------------------------------------------------------------------- /.env.test: -------------------------------------------------------------------------------- 1 | RSSR_REDUX_DEV_TOOLS=false 2 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "react-app", 3 | "globals": { 4 | "$": true, 5 | "jQuery": true 6 | }, 7 | "rules": { 8 | "default-case": "off" 9 | } 10 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /dist 3 | 4 | .DS_Store 5 | .sass-cache 6 | .idea 7 | 8 | 9 | package-lock.json 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | -------------------------------------------------------------------------------- /FakeApi/fakeApi.js: -------------------------------------------------------------------------------- 1 | const fakeApiData = { 2 | "posts": [ 3 | { 4 | "id": 0, 5 | "title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit", 6 | "body": "quia et suscipit. suscipit recusandae consequuntur expedita et cum. reprehenderit molestiae ut ut quas totam. nostrum rerum est autem sunt rem eveniet architecto" 7 | }, 8 | { 9 | "id": 1, 10 | "title": "qui est esse", 11 | "body": "est rerum tempore vitae. sequi sint nihil reprehenderit dolor beatae ea dolores neque. fugiat blanditiis voluptate porro vel nihil molestiae ut reiciendis. qui aperiam non debitis possimus qui neque nisi nulla" 12 | }, 13 | { 14 | "id": 2, 15 | "title": "ea molestias quasi exercitationem repellat qui ipsa sit aut", 16 | "body": "et iusto sed quo iure. voluptatem occaecati omnis eligendi aut ad. voluptatem doloribus vel accusantium quis pariatur. molestiae porro eius odio et labore et velit aut" 17 | }, 18 | { 19 | "id": 3, 20 | "title": "eum et est occaecati", 21 | "body": "ullam et saepe reiciendis voluptatem adipisci. sit amet autem assumenda provident rerum culpa. quis hic commodi nesciunt rem tenetur doloremque ipsam iure. quis sunt voluptatem rerum illo velit" 22 | }, 23 | { 24 | "id": 4, 25 | "title": "nesciunt quas odio", 26 | "body": "repudiandae veniam quaerat sunt sed. alias aut fugiat sit autem sed est. voluptatem omnis possimus esse voluptatibus quis. est aut tenetur dolor neque" 27 | }, 28 | { 29 | "id": 5, 30 | "title": "dolorem eum magni eos aperiam quia", 31 | "body": "ut aspernatur corporis harum nihil quis provident sequi. mollitia nobis aliquid molestiae. perspiciatis et ea nemo ab reprehenderit accusantium quas. voluptate dolores velit et doloremque molestiae" 32 | } 33 | ], 34 | "skeleton": { 35 | "dailyMessage": "Be happy! Life is too short." 36 | }, 37 | "signin": { 38 | "token": "salkhfasoidpaskdpksapdksakdpisapdiasdphdpksapdksakdpisapdiasdphdpksapdksakdpisapdiasdphdioashdoihsaoid" 39 | }, 40 | "signup": { 41 | "token": "salkhfasoidpaskdpksapdksakdpisapdiasdphdpksapdksakdpisapdiasdphdpksapdksakdpisapdiasdphdioashdoihsaoid" 42 | }, 43 | "userDetails": { 44 | "firstName": "dan", 45 | "lastName": "abramov", 46 | "phone": "+123456789", 47 | "email": "dan.abramov@gmail.com" 48 | }, 49 | "forgetPassword": { 50 | "message": "mail sent successfully. check your eamil." 51 | }, 52 | "resetPasswordTrust": { 53 | "message": "token is valid." 54 | }, 55 | "resetPasswordSubmit": { 56 | "message": "token is valid." 57 | } 58 | } 59 | 60 | 61 | 62 | module.exports = function (app) { 63 | 64 | app.use('/fake-api/:name/:id?', function (req, res) { 65 | const {name, id} = req.params; 66 | const delay = req.query.delay || 1 67 | let result; 68 | 69 | if (name) 70 | result = fakeApiData[name] 71 | 72 | if (id) { 73 | if (Array.isArray(result)) { 74 | const idResult = result.some(function (item) { 75 | const isEqual = String(item.id) === id; 76 | 77 | if (isEqual) 78 | result = item 79 | 80 | return isEqual; 81 | }) 82 | 83 | if (!idResult) 84 | result = undefined; 85 | } else { 86 | result = undefined; 87 | } 88 | } 89 | 90 | if (result) 91 | setTimeout(function () { 92 | res.status(200).json(result) 93 | },delay) 94 | else 95 | res.status(404).send('not found') 96 | 97 | }) 98 | } -------------------------------------------------------------------------------- /FakeApi/help.txt: -------------------------------------------------------------------------------- 1 | ::: FAKE API ::: 2 | We connected the Fake-API to the RSSR so you can see how it actually works. 3 | 4 | >>> HOW CAN RUN IT 5 | with 'npm run fake' you can launch it. 6 | 7 | >>> HOW CAN USE IT in code 8 | we imported fake api in sever files, so you can assess from 'http://localhost:8000/fake-api' in development and port 3000 for production. 9 | 10 | 11 | >>> HOW TO REMOVE IT: 12 | step1: remove ~/FakeApi directory. 13 | step2: change API_HOST_IN_CLIENT and API_HOST_IN_SERVER in ~/.env.development and ~/.env.production files to your real API address. 14 | step3: go to ~/server/development and ~/server/production files and remve fake api require (Marked with a symbol). 15 | step4: go to ~/src/setup/api.js and set your real routes. NOTICE: before remove or change this routes please find 'api.[ROUTE-NAME]' like 'api.forgetPassword' and do the necessary work. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 |
6 | 7 | # RSSR Boilerplate 8 | Welcome to RSSR(React-JS Server Side Rendering). Being here is a sign of your professionalism. 9 | 10 | RSSR is a SSR boilerplate for React js and contains: 11 | - SSR (Server Side Rendering) 12 | - User Authentication Structure 13 | - SEO optimization utilities 14 | - SCSS Style Namespace 15 | - and more … 16 | 17 | ## contain 18 | :: Base 19 | - React 17.0.1 (react-dom 17.0.1) 20 | - express 4.17.1 21 | - webpack 4.43.0 22 | - eslint 6.8.0 23 | - axios 0.21.0 24 | - history 4.10.1 25 | 26 | :: Useful side 27 | - node-sass 4.14.1 (support scss) 28 | - rssr-seo-optimization 0.0.1 (improve SEO) 29 | - dotenv 8.2.0 (support .env files) 30 | - cookie-parser 1.4.5 (support cookie in server mode) 31 | - express-rate-limit 5.1.3 (limit and filer requests in server) 32 | - local-storage 2.0.0 (good structuer for local storage) 33 | - trim-redux 2.3.0 (Redax simplification) 34 | - rssr-namespace 1.0.1 (set name space for SCSS (style) files.) 35 | 36 | :: utility (there is no force, You can simply delete) 37 | - bootstrap 4.5.3 38 | - jquery 3.5.1 39 | 40 | ## Documentation 41 | See [Documentation](https://github.com/rssr-org/RSSR-Documentation) in github. 42 | 43 | You can also watch videos of RSSR team at [aparat](https://www.aparat.com/user/video/user_list/userid/722589/usercat/413997) and [youtube](https://www.youtube.com/channel/UCNkuorlYEWReSMglMp25yCw), . 44 | 45 | ## Usage Notice 46 | The core of RSSR is stable but needs some changes before it can be released publicly. You can fork, review and star it but DO NOT USE it for your enterprise projects until the final release! 47 | 48 | For more information, follow us at : [Telegram channel](https://t.me/rssr_org). 49 | 50 | 51 | ## Know more 52 | 53 | #### what is SSR? 54 | Server Side Rendering is a popular technique for rendering a normally 55 | client-side single page app (SPA) on the server and then sending 56 | a fully rendered page to the client. The client’s JavaScript bundle 57 | can then take over and the SPA can operate as normal. One major 58 | benefit of using SSR is in having an app that can be crawled 59 | for its content even for crawlers that don’t execute JavaScript code. 60 | This can help with SEO and with providing meta data to social media channels. 61 | 62 | 63 | #### What is a Boilerplate? 64 | In programming, the term boilerplate code refers to blocks of code used over and over again. 65 | 66 | Let’s assume your development stack consists of several libraries, 67 | such as React, Babel, Express, Jest, Webpack, etc. When you 68 | start a new project, you initialize all these libraries 69 | and configure them to work with each other. 70 | 71 | With every new project that you start, you will be repeating yourself. 72 | You could also introduce inconsistencies in how these libraries 73 | are set up in each project. This can cause confusion when you 74 | switch between projects. 75 | 76 | This is where boilerplates come in. A boilerplate is a template that 77 | you can clone and reuse for every project. 78 | 79 | 80 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "RSSR", 3 | "version": "0.2.0", 4 | "repository": { 5 | "type": "git", 6 | "url": "git+https://github.com/rssr-org/RSSR.git" 7 | }, 8 | "author": "RSSR-ORG", 9 | "license": "MIT", 10 | "bugs": { 11 | "url": "https://github.com/rssr-org/RSSR/issues" 12 | }, 13 | "homepage": "https://github.com/rssr-org/RSSR#readme", 14 | "scripts": { 15 | "dev": "node ./provider/server/development.js", 16 | "build": "webpack --config ./provider/webpack/production.js --debug --progress --display-error-details --profile --colors", 17 | "start": "node ./provider/server/production.js", 18 | "start-pm2-low": "pm2 start ./provider/server/production.js --name RSSR", 19 | "start-pm2": "npm run start-pm2-low -- -i max", 20 | "up-low": "pm2 delete RSSR & npm run start-pm2-low", 21 | "up": "pm2 delete RSSR & npm run start-pm2", 22 | "lint": "eslint src", 23 | "test": "echo 'do test'" 24 | }, 25 | "dependencies": { 26 | "axios": "^0.21.0", 27 | "cookie-parser": "^1.4.5", 28 | "dotenv": "^8.2.0", 29 | "express": "^4.17.1", 30 | "express-rate-limit": "^5.1.3", 31 | "rssr-seo-optimization": "0.0.1", 32 | "serialize-javascript": "^5.0.1" 33 | }, 34 | "devDependencies": { 35 | "@babel/core": "^7.12.9", 36 | "@babel/plugin-proposal-class-properties": "^7.12.1", 37 | "@babel/plugin-proposal-decorators": "^7.12.1", 38 | "@babel/plugin-proposal-object-rest-spread": "^7.12.1", 39 | "@babel/plugin-transform-runtime": "^7.12.1", 40 | "@babel/preset-env": "^7.12.7", 41 | "@babel/preset-react": "^7.12.7", 42 | "babel-eslint": "^10.1.0", 43 | "babel-loader": "^8.2.2", 44 | "bootstrap": "^4.5.3", 45 | "css-hot-loader": "^1.4.4", 46 | "css-loader": "^3.5.3", 47 | "dotenv-webpack": "^1.7.0", 48 | "eslint": "^7.5.0", 49 | "eslint-config-react-app": "^6.0.0", 50 | "eslint-loader": "^4.0.2", 51 | "eslint-plugin-flowtype": "^5.2.0", 52 | "eslint-plugin-import": "^2.22.0", 53 | "eslint-plugin-jsx-a11y": "^6.3.1", 54 | "eslint-plugin-react": "^7.20.3", 55 | "eslint-plugin-react-hooks": "^4.0.8", 56 | "expose-loader": "^0.7.5", 57 | "history": "^4.10.1", 58 | "ignore-loader": "^0.1.2", 59 | "jquery": "^3.5.1", 60 | "js-cookie": "^2.2.1", 61 | "local-storage": "^2.0.0", 62 | "mini-css-extract-plugin": "^0.9.0", 63 | "node-sass": "^4.14.1", 64 | "optimize-css-assets-webpack-plugin": "^5.0.3", 65 | "popper.js": "^1.16.1", 66 | "prop-types": "^15.7.2", 67 | "querystringify": "^2.1.1", 68 | "react": "^17.0.1", 69 | "react-dom": "^17.0.1", 70 | "react-helmet-async": "^1.0.6", 71 | "react-lazy-load-image-component": "^1.4.3", 72 | "react-router-dom": "^5.1.2", 73 | "react-toastify": "^5.5.0", 74 | "react-tooltip": "^4.2.5", 75 | "rssr-namespace": "^1.0.1", 76 | "rssr-open-browser": "^1.0.0", 77 | "sass-loader": "^8.0.2", 78 | "terser-webpack-plugin": "^2.3.6", 79 | "trim-redux": "^2.3.0", 80 | "webpack": "^4.43.0", 81 | "webpack-cli": "^3.3.11", 82 | "webpack-dev-middleware": "^3.7.2", 83 | "webpack-hot-middleware": "^2.25.0", 84 | "webpack-hot-server-middleware": "^0.6.0" 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /provider/server/development.js: -------------------------------------------------------------------------------- 1 | process.env.NODE_ENV = 'development'; 2 | require('../setup/envLoader') 3 | require('../setup/fileVersion') 4 | 5 | const cookieParser = require('cookie-parser') 6 | const express = require('express') 7 | const webpack = require('webpack') 8 | const config = require('../webpack/development') 9 | const webpackDevMiddleware = require('webpack-dev-middleware') 10 | const webpackHotMiddleware = require('webpack-hot-middleware') 11 | const webpackHotServerMiddleware = require('webpack-hot-server-middleware') 12 | const {DIST_ROUTE, PUBLIC_NAME} = require('../setup/constant') 13 | 14 | 15 | 16 | 17 | 18 | 19 | // express app 20 | const app = express() 21 | 22 | //----- REMOVE THIS PART AND 'fakeApi.js' FILE IN REAL PROJECTS -----// 23 | require('../../FakeApi/fakeApi')(app) 24 | //-------------------------------------------------------------------// 25 | 26 | // cookie 27 | app.use(cookieParser()) 28 | 29 | // static files 30 | app.use(express.static(PUBLIC_NAME)) 31 | 32 | // create webpack compiler 33 | const compiler = webpack(config) 34 | 35 | // make bundled project source files accessible from memory 36 | app.use(webpackDevMiddleware(compiler, { 37 | publicPath: DIST_ROUTE, 38 | serverSideRender: true 39 | })) 40 | 41 | // recompile webpack when file changes 42 | app.use(webpackHotMiddleware(compiler.compilers.find(compiler => compiler.name === 'client'))) 43 | 44 | // hot update Webpack bundles on the server 45 | app.use(webpackHotServerMiddleware(compiler)) 46 | 47 | 48 | 49 | 50 | 51 | // run server 52 | const PORT = process.env.PORT || 8000; 53 | 54 | app.listen(PORT, error => { 55 | if (error) { 56 | return console.error('Error in server/development.js: ', error); 57 | } else { 58 | console.log(`development server running at http://localhost:${PORT}`); 59 | } 60 | }) 61 | -------------------------------------------------------------------------------- /provider/server/production.js: -------------------------------------------------------------------------------- 1 | process.env.NODE_ENV = 'production'; 2 | require('../setup/envLoader') 3 | require('../setup/fileVersion') 4 | 5 | const {DIST_PATH, DIST_ROUTE, PUBLIC_NAME, SERVER_DIST_PATH} = require('../setup/constant') 6 | const cookieParser = require('cookie-parser') 7 | const seoOptimization = require('rssr-seo-optimization') 8 | const rateLimit = require('../setup/rateLimit') 9 | const express = require('express') 10 | const serverRenderer = require(SERVER_DIST_PATH).default 11 | 12 | 13 | 14 | 15 | 16 | // express app 17 | const app = express() 18 | 19 | //----- REMOVE THIS PART AND 'fakeApi.js' FILE IN REAL PROJECTS -----// 20 | require('../../FakeApi/fakeApi')(app) 21 | //-------------------------------------------------------------------// 22 | 23 | // cookie 24 | app.use(cookieParser()) 25 | 26 | // make bundled final project source files accessible 27 | app.use(DIST_ROUTE, express.static(DIST_PATH)) 28 | 29 | // load static files 30 | app.use(express.static(PUBLIC_NAME)) 31 | 32 | // Redirect from www to non-www and remove slash at the end of URL 33 | seoOptimization(app) 34 | 35 | // limit the request number of each user in 'windowMs' milliseconds 36 | rateLimit(app) 37 | 38 | // load server script and render app (do react SSR) 39 | app.use(serverRenderer()) 40 | 41 | 42 | 43 | 44 | 45 | // run server 46 | const PORT = process.env.PORT || 3000 47 | const HOST = process.env.HOST || '0.0.0.0' 48 | 49 | app.listen(PORT, HOST, error => { 50 | if (error) 51 | return console.error('Error in server/production.js: ', error); 52 | else 53 | console.log(`production server running at http://localhost:${PORT} and ${HOST} host.`); 54 | }) 55 | -------------------------------------------------------------------------------- /provider/setup/constant.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | const C = {}; 4 | 5 | // dist 6 | C.DIST_NAME = 'dist'; 7 | C.DIST_ROUTE = '/' + C.DIST_NAME; 8 | C.DIST_PATH = path.resolve(process.cwd(), '.' + C.DIST_ROUTE); 9 | 10 | // client 11 | C.CLIENT_NAME = 'client.js'; 12 | C.CLIENT_ROUTE = './src/render/' + C.CLIENT_NAME; 13 | 14 | // server 15 | C.SERVER_NAME = 'server.js'; 16 | C.SERVER_ROUTE = './src/render/server/' + C.SERVER_NAME; 17 | C.SERVER_DIST_PATH = path.resolve(C.DIST_PATH, C.SERVER_NAME); 18 | 19 | // public 20 | C.PUBLIC_NAME = 'public'; 21 | 22 | // style 23 | C.SCSS_PATH = path.resolve(process.cwd(), './src/setup/style'); 24 | 25 | // Development > open browser 26 | C.OPEN_BROWSER_URL = 'http://localhost:' + process.env.PORT || 8000; 27 | 28 | module.exports = C -------------------------------------------------------------------------------- /provider/setup/envLoader.js: -------------------------------------------------------------------------------- 1 | // load environment variable of .env file 2 | const dotenv = require('dotenv') 3 | dotenv.config() 4 | 5 | // load environment variable of .env.[NODE_ENV] files 6 | const fs = require('fs') 7 | const envPath = fs.readFileSync('.env.' + process.env.NODE_ENV); 8 | const envConfig = dotenv.parse(envPath) 9 | for (const k in envConfig) { 10 | process.env[k] = envConfig[k] 11 | } 12 | -------------------------------------------------------------------------------- /provider/setup/fileVersion.js: -------------------------------------------------------------------------------- 1 | 2 | // define global.FILE_VERSION for dist file version. see render/Index.js template. 3 | // global.FILE_VERSION is 'npm' or 'random' or 'disable' 4 | switch (process.env.FILE_VERSION_TYPE) { 5 | case 'npm': 6 | // define global.FILE_VERSION for dist file version. see render/Index.js template. 7 | const npmVersion = require("../../package").version; 8 | // value of npm package.js verion property 9 | global.FILE_VERSION = '?v=' + npmVersion; 10 | break; 11 | case 'time': 12 | // time stamp of now 13 | const timeStampVersion = new Date().getTime(); 14 | // random 24 char string 15 | global.FILE_VERSION = '?v=' + timeStampVersion; 16 | break; 17 | case 'disable': 18 | // without version 19 | global.FILE_VERSION = ''; 20 | break; 21 | default: 22 | console.error('process.env.FILE_VERSION is not valid!', global.FILE_VERSION) 23 | } 24 | -------------------------------------------------------------------------------- /provider/setup/rateLimit.js: -------------------------------------------------------------------------------- 1 | // limit request number 2 | const rateLimit = require("express-rate-limit"); 3 | 4 | /** 5 | * rateLimit 6 | * 7 | * limit the request number of each user in windowMs 8 | * read more: https://www.npmjs.com/package/express-rate-limit 9 | * 10 | * @param app : an express base of server [const app = expres()] 11 | */ 12 | module.exports = function (app) { 13 | app.enable("trust proxy"); 14 | 15 | const limiter = rateLimit({ 16 | windowMs: 60 * 1000, // 1 minutes 17 | max: 20 // 20 requests in each 1 minute 18 | }); 19 | 20 | // apply to all requests without loading static file (because defined in above) 21 | app.use(limiter); 22 | } 23 | -------------------------------------------------------------------------------- /provider/webpack/development.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const Dotenv = require('dotenv-webpack'); 3 | const OpenBrowserPlugin = require('rssr-open-browser'); 4 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 5 | const {CLIENT_NAME, DIST_ROUTE, SCSS_PATH, CLIENT_ROUTE, SERVER_ROUTE, SERVER_NAME, OPEN_BROWSER_URL} = require('../setup/constant'); 6 | 7 | 8 | 9 | //?quiet=true 10 | module.exports = [ 11 | //---------------- client ----------------// 12 | { 13 | name: 'client', 14 | mode: 'development', 15 | target: 'web', 16 | devtool: 'source-map', 17 | entry: ['webpack-hot-middleware/client?name=client&reload=true', CLIENT_ROUTE], 18 | output: { 19 | filename: CLIENT_NAME, 20 | publicPath: DIST_ROUTE, 21 | }, 22 | module: { 23 | rules: [ 24 | { 25 | test: /\.(js|jsx)$/, 26 | exclude: /(node_modules[\\\/])/, 27 | use: [ 28 | "babel-loader", 29 | "eslint-loader" 30 | ] 31 | }, 32 | { 33 | test: /\.(css|scss)$/, 34 | use: [ 35 | { 36 | loader: 'css-hot-loader?cssModule=true', 37 | }, 38 | { 39 | loader: MiniCssExtractPlugin.loader 40 | }, 41 | { 42 | loader: 'css-loader', 43 | options: { 44 | // modules: true, 45 | // localIdentName: '[local]__[hash:base64:5]', 46 | sourceMap: true, 47 | importLoaders: 1 48 | } 49 | }, 50 | { 51 | loader: 'sass-loader', 52 | options: { 53 | sassOptions: { 54 | outputStyle: 'compressed', 55 | includePaths: [SCSS_PATH] 56 | } 57 | } 58 | }, 59 | { 60 | loader: 'rssr-namespace/loader.js' 61 | } 62 | ] 63 | }, 64 | { 65 | test: require.resolve('jquery'), 66 | use: [{ 67 | loader: 'expose-loader', 68 | options: 'jQuery' 69 | }, { 70 | loader: 'expose-loader', 71 | options: '$' 72 | }] 73 | } 74 | ], 75 | }, 76 | plugins: [ 77 | new webpack.ProvidePlugin({ 78 | $: "jquery", 79 | jQuery: "jquery" 80 | }), 81 | new MiniCssExtractPlugin({ 82 | filename: 'styles.css' 83 | }), 84 | new OpenBrowserPlugin({url: OPEN_BROWSER_URL}), 85 | new Dotenv({systemvars: true}), 86 | new webpack.HotModuleReplacementPlugin(), 87 | new webpack.IgnorePlugin(/async-local-storage/) 88 | ] 89 | }, 90 | 91 | //---------------- server ----------------// 92 | { 93 | name: 'server', 94 | mode: 'development', 95 | target: 'node', 96 | devtool: 'source-map', 97 | entry: ['webpack-hot-middleware/client?name=server&reload=true', SERVER_ROUTE], 98 | output: { 99 | filename: SERVER_NAME, 100 | libraryTarget: 'commonjs2', 101 | publicPath: DIST_ROUTE, 102 | }, 103 | module: { 104 | rules: [ 105 | { 106 | test: /\.(js|jsx)$/, 107 | exclude: /(node_modules[\\\/])/, 108 | use: [ 109 | "babel-loader", 110 | "eslint-loader" 111 | ] 112 | }, 113 | { 114 | test: /\.(css|scss)$/, 115 | use: 'ignore-loader' 116 | } 117 | ], 118 | }, 119 | plugins: [ 120 | new webpack.HotModuleReplacementPlugin() 121 | ] 122 | } 123 | ]; 124 | -------------------------------------------------------------------------------- /provider/webpack/production.js: -------------------------------------------------------------------------------- 1 | process.env.NODE_ENV = 'production'; 2 | require('../setup/envLoader') 3 | 4 | const webpack = require('webpack'); 5 | const Dotenv = require('dotenv-webpack'); 6 | const TerserPlugin = require('terser-webpack-plugin'); 7 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 8 | const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin'); 9 | const {CLIENT_NAME, DIST_PATH, SCSS_PATH, CLIENT_ROUTE, SERVER_ROUTE, SERVER_NAME} = require('../setup/constant'); 10 | 11 | 12 | 13 | module.exports = [ 14 | //---------------- client ----------------// 15 | { 16 | name: 'client', 17 | mode: 'production', 18 | target: 'web', 19 | performance: {hints: false}, 20 | entry: CLIENT_ROUTE, 21 | output: { 22 | path: DIST_PATH, 23 | filename: CLIENT_NAME, 24 | publicPath: DIST_PATH, 25 | }, 26 | module: { 27 | rules: [ 28 | { 29 | test: /\.(js|jsx)$/, 30 | exclude: /(node_modules[\\\/])/, 31 | use: [ 32 | { 33 | loader: 'babel-loader' 34 | } 35 | ] 36 | }, 37 | { 38 | test: /\.(css|scss)$/, 39 | use: [ 40 | { 41 | loader: MiniCssExtractPlugin.loader 42 | }, 43 | { 44 | loader: 'css-loader', 45 | options: { 46 | importLoaders: 1 47 | } 48 | }, 49 | { 50 | loader: 'sass-loader', 51 | options: { 52 | sassOptions: { 53 | outputStyle: 'compressed', 54 | includePaths: [SCSS_PATH] 55 | } 56 | } 57 | }, 58 | { 59 | loader: 'rssr-namespace/loader.js' 60 | } 61 | ] 62 | }, 63 | { 64 | test: require.resolve('jquery'), 65 | use: [{ 66 | loader: 'expose-loader', 67 | options: 'jQuery' 68 | }, { 69 | loader: 'expose-loader', 70 | options: '$' 71 | }] 72 | } 73 | ] 74 | }, 75 | plugins: [ 76 | new webpack.ProvidePlugin({ 77 | $: "jquery", 78 | jQuery: "jquery" 79 | }), 80 | new MiniCssExtractPlugin({ 81 | filename: 'styles.css' 82 | }), 83 | new Dotenv({systemvars: true}), 84 | new webpack.optimize.OccurrenceOrderPlugin(), 85 | new webpack.IgnorePlugin(/async-local-storage/) 86 | ], 87 | optimization: { 88 | minimize: true, 89 | minimizer: [ 90 | new TerserPlugin({ 91 | terserOptions: { 92 | output: { 93 | comments: false, 94 | } 95 | }, 96 | extractComments: { 97 | banner: false 98 | }, 99 | extractComments: false, 100 | }), 101 | new OptimizeCssAssetsPlugin({ 102 | cssProcessorOptions: {discardComments: {removeAll: true}} 103 | }) 104 | ] 105 | } 106 | }, 107 | 108 | //---------------- server ----------------// 109 | { 110 | name: 'server', 111 | mode: 'production', 112 | target: 'node', 113 | performance: {hints: false}, 114 | entry: SERVER_ROUTE, 115 | output: { 116 | path: DIST_PATH, 117 | filename: SERVER_NAME, 118 | libraryTarget: 'commonjs2', 119 | publicPath: DIST_PATH, 120 | }, 121 | module: { 122 | rules: [ 123 | { 124 | test: /\.(js|jsx)$/, 125 | exclude: /(node_modules[\\\/])/, 126 | use: [ 127 | { 128 | loader: 'babel-loader', 129 | } 130 | ] 131 | }, 132 | { 133 | test: /\.(css|scss)$/, 134 | use: 'ignore-loader' 135 | } 136 | ] 137 | } 138 | } 139 | ]; 140 | -------------------------------------------------------------------------------- /public/app-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rssr-org/RSSR/a860c9fbbeec04265f0959cd2658addd4b806751/public/app-icon.png -------------------------------------------------------------------------------- /public/asset/img/error-404.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rssr-org/RSSR/a860c9fbbeec04265f0959cd2658addd4b806751/public/asset/img/error-404.png -------------------------------------------------------------------------------- /public/asset/img/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rssr-org/RSSR/a860c9fbbeec04265f0959cd2658addd4b806751/public/asset/img/loading.gif -------------------------------------------------------------------------------- /public/asset/img/rssr-logo-block.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rssr-org/RSSR/a860c9fbbeec04265f0959cd2658addd4b806751/public/asset/img/rssr-logo-block.png -------------------------------------------------------------------------------- /public/asset/img/rssr-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rssr-org/RSSR/a860c9fbbeec04265f0959cd2658addd4b806751/public/asset/img/rssr-logo.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "RSSR", 3 | "name": "React SSR", 4 | "description": "react server sider rendering", 5 | "icons": [ 6 | { 7 | "src": "app-icon.png", 8 | "sizes": "256x256 128x128 64x64 32x32 24x24 16x16", 9 | "type": "image/png" 10 | } 11 | ], 12 | "dir": "rtl", 13 | "lang": "fa", 14 | "start_url": ".", 15 | "display": "standalone", 16 | "theme_color": "#000000", 17 | "background_color": "#ffffff" 18 | } 19 | -------------------------------------------------------------------------------- /public/sub-scripts.js: -------------------------------------------------------------------------------- 1 | // your out-source scripts for production mode like hotjar, google analytics -------------------------------------------------------------------------------- /src/App/App.js: -------------------------------------------------------------------------------- 1 | import React, {Fragment, useEffect} from 'react'; 2 | import axios from "axios"; 3 | import {Helmet} from "react-helmet-async"; 4 | import {ToastContainer} from 'react-toastify'; 5 | import Router from "../Partial/Router/Router"; 6 | import Menu from "../Component/Menu/Menu"; 7 | import {firstSetup} from "../Component/Auth/__action/firstSetup"; 8 | import OverLoading from "../Component/OverLoading/OverLoading"; 9 | import {api} from "../setup/api"; 10 | import {skeleton} from "../Partial/skeleton/skeleton"; 11 | 12 | 13 | function App() { 14 | useEffect(() => { 15 | // user Authentication, get cart, set theme and more. 16 | firstSetup(); 17 | }, []) 18 | 19 | return ( 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | ) 28 | } 29 | 30 | const skeletonFetch = function () { 31 | return axios({url: api.skeleton}) 32 | } 33 | 34 | export default skeleton(App, skeletonFetch, 8000); 35 | -------------------------------------------------------------------------------- /src/App/Error404/Error404.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Helmet} from "react-helmet-async"; 3 | import {browserHistory} from "../../setup/browserHistory"; 4 | import {Link} from "react-router-dom"; 5 | import {route} from "../../setup/route"; 6 | // import axios from "axios"; 7 | 8 | 9 | function Error404() { 10 | 11 | // function traffic() { 12 | // const routes = ['/', '/post/0', '/post/1', '/post/2', '/post/3', '/sign/in', '/sign/up', '/404'] 13 | // 14 | // for (let i = 1; i <= 700; i++) { 15 | // const randomRoute = routes[Math.floor(Math.random() * routes.length)] 16 | // let proccessTimeStart = Date.now(); 17 | // 18 | // axios({ 19 | // url: 'http://localhost:8000' + randomRoute 20 | // }) 21 | // .then(() => { 22 | // console.log('OK: ' + randomRoute + ' ' + (Date.now() - proccessTimeStart) + 'ms') 23 | // }) 24 | // .catch(() => { 25 | // console.log('error: ' + randomRoute + ' ' + (Date.now() - proccessTimeStart) + 'ms') 26 | // }) 27 | // } 28 | // } 29 | 30 | return ( 31 |
32 | 33 |
34 |
35 |

Oops, page not found!

36 |
37 | page not found 38 |
39 | {/**/} 40 | 41 | Home 42 |
43 |
44 |
45 | ) 46 | } 47 | 48 | export default Error404; 49 | -------------------------------------------------------------------------------- /src/App/Home/Home.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Helmet} from "react-helmet-async"; 3 | import {Link} from "react-router-dom"; 4 | import {api} from "../../setup/api"; 5 | import {route} from "../../setup/route"; 6 | import {fetcher} from "../../Partial/fetcher/fetcher"; 7 | import Namespace from "rssr-namespace"; 8 | import {fetching} from "../../setup/utility/fetching"; 9 | import "./home.scss"; 10 | 11 | 12 | 13 | 14 | 15 | function Home(props) { 16 | const {homepage} = props; 17 | 18 | return ( 19 | 20 |
21 | 22 | 23 |
24 |
RSSR Boilderplate
25 |

26 | We are no better than anyone else, 27 | We are not in competition with anyone, 28 | We want to be the best version of ourselves. 29 |
30 |
31 | see Documentation 32 |

33 |
34 | 35 |
36 | { 37 | (homepage.isLoading) ? 38 | ( 39 |
40 | loading 41 |
fetching data ...
42 |
43 | ) 44 | : 45 | ( 46 | homepage.map((post) => ( 47 |
48 | 49 |
50 |

{post.title}

51 |

{post.body}

52 | See more 53 |
54 | 55 |
56 | )) 57 | ) 58 | } 59 |
60 |
61 |
62 | ) 63 | } 64 | 65 | 66 | const fetch = () => fetching({url: api.posts}) 67 | 68 | export default fetcher(Home, fetch, 'homepage'); 69 | -------------------------------------------------------------------------------- /src/App/Home/home.scss: -------------------------------------------------------------------------------- 1 | @namespace "home"; 2 | 3 | .card { 4 | color : #404040; 5 | 6 | span { 7 | font-size : 13px; 8 | opacity : 0.3; 9 | } 10 | 11 | &:hover { 12 | text-decoration : none; 13 | 14 | span { 15 | opacity : 1; 16 | } 17 | 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/App/Post/Post.js: -------------------------------------------------------------------------------- 1 | import React, {Fragment} from 'react'; 2 | import {Helmet} from "react-helmet-async"; 3 | import {Link} from "react-router-dom"; 4 | import {api} from "../../setup/api"; 5 | import {route} from "../../setup/route"; 6 | import {fetcher} from "../../Partial/fetcher/fetcher"; 7 | import {fetching} from "../../setup/utility/fetching"; 8 | 9 | 10 | function Post(props) { 11 | const postId = Number(props.match.params.postId) 12 | const {post} = props 13 | 14 | return ( 15 |
16 | 17 |
18 | { 19 | (post !== null) ? ( 20 | 21 |

{post.title + ' ' + postId}

22 |

{post.body}

23 |
24 | ) 25 | : 26 | ( 27 |
28 | loading post ... 29 |
30 | ) 31 | } 32 |
33 |
34 | < previous 35 | next > 36 |
37 |
38 | ); 39 | }; 40 | 41 | 42 | const fetch = ({match, req}) => { 43 | return fetching({ 44 | url: api.post(match.params.postId + '?delay=800') 45 | }); 46 | } 47 | 48 | export default fetcher(Post, fetch, 'post'); -------------------------------------------------------------------------------- /src/App/Sign/Sign.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import SignIn from "../../Component/Auth/SignIn/SignIn"; 3 | import SignUp from "../../Component/Auth/SignUp"; 4 | import ValidUser from "../../Component/Auth/ValidUser"; 5 | import InvalidUser from "../../Component/Auth/InvalidUser"; 6 | import {Link} from "react-router-dom"; 7 | import {route} from "../../setup/route"; 8 | import {signingOut} from "../../Component/Auth/__action/signingOut"; 9 | 10 | const Sign = props => { 11 | const isSignIn = props.match.params.type === 'in'; 12 | 13 | 14 | return ( 15 |
16 |
17 |
18 | 19 |

20 | { 21 | isSignIn ? 'Login' : 'Register' 22 | } 23 |

24 | { 25 | isSignIn ? : 26 | } 27 |
28 | 29 | 30 | { 31 | (detail) => ( 32 |
33 |

Hi {detail.firstName}

34 | see 35 |
36 | Home page 37 |
38 | or 39 |
40 | 41 |
42 | ) 43 | } 44 |
45 |
46 |
47 |
48 | ) 49 | } 50 | 51 | export default Sign; 52 | -------------------------------------------------------------------------------- /src/Component/Auth/InvalidUser.js: -------------------------------------------------------------------------------- 1 | import {connect} from "trim-redux"; 2 | import {isValidUser} from "../../setup/utility/isValidUser"; 3 | 4 | const InvalidUser = props => { 5 | return !isValidUser(false) ? props.children : ''; 6 | } 7 | 8 | export default connect(s => ({user: s.user}))(InvalidUser); -------------------------------------------------------------------------------- /src/Component/Auth/LoadingUser.js: -------------------------------------------------------------------------------- 1 | import {connect} from "trim-redux"; 2 | 3 | const LoadingUser = props => !props.user.updated ? props.children : ''; 4 | 5 | export default connect(s => ({user: s.user}))(LoadingUser); -------------------------------------------------------------------------------- /src/Component/Auth/ResetPassword.js: -------------------------------------------------------------------------------- 1 | import React, {useEffect, useState} from 'react'; 2 | import {toast} from 'react-toastify'; 3 | import axios from "axios"; 4 | import {api} from "../../setup/api"; 5 | import {route} from "../../setup/route"; 6 | import {regexp} from "../../setup/constant"; 7 | import {browserHistory} from "../../setup/browserHistory"; 8 | import {badConnectionAlert} from "../../setup/utility/badConnectionAlert"; 9 | 10 | 11 | 12 | 13 | function ResetPassword(props) { 14 | 15 | const [result, setResult] = useState('LOADING') // LOADING || FORM || ERROR 16 | const [newpassword, setNewpassword] = useState('') 17 | const [repassword, setRepassword] = useState('') 18 | 19 | 20 | 21 | useEffect(() => { 22 | axios({ 23 | url: api.resetPassword.trust, 24 | method: 'POST', 25 | data: { 26 | token: props.match.params.token 27 | } 28 | }) 29 | .then(() => { 30 | setResult('FORM'); 31 | }) 32 | .catch((err) => { 33 | if (err.response && err.response.status === 400) 34 | setResult('ERROR'); 35 | else 36 | badConnectionAlert('reset Password trust'); 37 | }); 38 | }, [props.match.params.token]); 39 | 40 | 41 | 42 | 43 | 44 | function submitForm(e) { 45 | //-------------------------------------------// 46 | e.preventDefault() 47 | e.stopPropagation() 48 | const form = e.target 49 | if (form.checkValidity() === false) { 50 | form.classList.add('was-validated') 51 | return false; 52 | } 53 | form.className.replace(" was-validated", "") 54 | //-------------------------------------------// 55 | 56 | axios({ 57 | url: api.resetPassword.submit, 58 | method: 'POST', 59 | data: { 60 | password: newpassword, 61 | token: props.match.params.token 62 | } 63 | }) 64 | .then(() => { 65 | toast.success('Password successfully changed!'); 66 | browserHistory.replace(route.home); 67 | }) 68 | .catch(() => { 69 | badConnectionAlert('reset Password submit'); 70 | }) 71 | } 72 | 73 | 74 | 75 | 76 | 77 | return ( 78 |
79 |
80 |
81 |

Change password

82 | { 83 | (result === 'FORM') ? 84 | ( 85 |
86 |
87 | 88 | setNewpassword(e.target.value)} 94 | required/> 95 |
96 | 97 |
98 | 99 | setRepassword(e.target.value)} 105 | required/> 106 |
107 | 108 | 109 |
110 | ) 111 | : 112 | ( 113 | result === 'LOADING' ? 114 | User Validation, please wait... 115 | : 116 | User Token is not valid! 117 | ) 118 | } 119 |
120 |
121 |
122 | ); 123 | } 124 | 125 | export default ResetPassword; 126 | -------------------------------------------------------------------------------- /src/Component/Auth/SignIn/ForgetPasswordForm.js: -------------------------------------------------------------------------------- 1 | import React, {useState} from 'react'; 2 | import {connect} from "trim-redux"; 3 | import {regexp} from "../../../setup/constant"; 4 | import axios from "axios"; 5 | import {api} from "../../../setup/api"; 6 | import {route} from "../../../setup/route"; 7 | import {toast} from "react-toastify"; 8 | import {badConnectionAlert} from "../../../setup/utility/badConnectionAlert"; 9 | 10 | 11 | function ForgetPasswordForm(props) { 12 | const [ isLoading, setIsLoading ] = useState(false) 13 | const [ email, setEmail ] = useState('') 14 | const {user, showSignInForm} = props 15 | 16 | 17 | function submitForgetPassword(e) { 18 | //-------------------------------------------// 19 | e.preventDefault() 20 | e.stopPropagation() 21 | const form = e.target 22 | if (form.checkValidity() === false) { 23 | form.classList.add('was-validated') 24 | return false; 25 | } 26 | form.className.replace(" was-validated", "") 27 | //-------------------------------------------// 28 | 29 | setIsLoading(true); 30 | 31 | axios({ 32 | url: api.forgetPassword, 33 | // method: 'POST', 34 | data: { 35 | "email": email, 36 | // server must add token number to end of url and redirect to it 37 | "callback": window.location.origin + route.resetPassword('') 38 | } 39 | }) 40 | .then(() => { 41 | const message = ( 42 |
43 | Recovery email sent. 44 |
45 | Please check your email inbox or spam and click on link. 46 |
47 | {email} 48 |
49 | Apply again if you did not receive it. 50 |
51 | ); 52 | toast.success(message, {autoClose: false}); 53 | }) 54 | .catch((err) => { 55 | setIsLoading(false); 56 | 57 | if (err.status === 400) 58 | toast.error('E-mail is not valid'); 59 | else 60 | badConnectionAlert('Submit forget password'); 61 | }); 62 | } 63 | 64 | 65 | 66 | 67 | 68 | return ( 69 |
70 |
71 |
Password recovery
72 | 73 |
74 |
75 | 76 | setEmail(e.target.value)} 82 | required/> 83 |
E-mail is not valid. please enter your email account like: sample@gmail.com
84 |
85 | 88 |
89 | ) 90 | } 91 | 92 | export default connect(s => ({user: s.user}))(ForgetPasswordForm); 93 | -------------------------------------------------------------------------------- /src/Component/Auth/SignIn/SignIn.js: -------------------------------------------------------------------------------- 1 | import React, {useState} from 'react'; 2 | import ForgetPasswordForm from "./ForgetPasswordForm"; 3 | import SignInForm from "./SignInForm"; 4 | 5 | const SignIn = () => { 6 | const [showSignInForm, setShowSignInForm] = useState(true); 7 | 8 | return ( 9 | showSignInForm ? 10 | setShowSignInForm(false)}/> 11 | : 12 | setShowSignInForm(true)}/> 13 | ) 14 | }; 15 | 16 | export default SignIn; 17 | -------------------------------------------------------------------------------- /src/Component/Auth/SignIn/SignInForm.js: -------------------------------------------------------------------------------- 1 | import React, {useState} from 'react'; 2 | import {toast} from "react-toastify"; 3 | import axios from "axios"; 4 | import {api} from "../../../setup/api"; 5 | import {signingIn} from "../__action/signingIn"; 6 | import {regexp} from "../../../setup/constant"; 7 | import {connect} from "trim-redux"; 8 | import {Link} from "react-router-dom"; 9 | import {route} from "../../../setup/route"; 10 | import {badConnectionAlert} from "../../../setup/utility/badConnectionAlert"; 11 | 12 | function SignInForm(props) { 13 | 14 | const [isLoading, setIsLoading] = useState(false) 15 | const [username, setUsername] = useState('') 16 | const [rememberMe, setRememberMe] = useState(true) 17 | const [password, setPassword] = useState('') 18 | const {user, showForgetPasswordForm} = props 19 | 20 | 21 | 22 | 23 | 24 | function submitSignIn(e) { 25 | //-------------------------------------------// 26 | e.preventDefault() 27 | e.stopPropagation() 28 | const form = e.target 29 | if (form.checkValidity() === false) { 30 | form.classList.add('was-validated') 31 | return false; 32 | } 33 | form.className.replace(" was-validated", "") 34 | //-------------------------------------------// 35 | 36 | setIsLoading(true); 37 | 38 | axios({ 39 | url: api.signin, 40 | method: 'POST', 41 | data: {username, password} 42 | }) 43 | .then((response) => { 44 | // set token to localStorage if remember me is checked and get user details 45 | signingIn(response.data.token, rememberMe) 46 | .then(function () { 47 | toast.success('logged-in successfully!', {autoClose: 1200}); 48 | }) 49 | .catch(function () { 50 | toast.success('logged-in successfully but occur an error in fetch user details, try again or tell to support!', {autoClose: false}); 51 | }); 52 | }) 53 | .catch(() => { 54 | badConnectionAlert('Sing in'); 55 | }) 56 | .then(() => { 57 | setIsLoading(false); 58 | }) 59 | } 60 | 61 | 62 | 63 | 64 | 65 | return ( 66 |
67 |
68 | 69 | setUsername(e.target.value)} 75 | required/> 76 |
77 | 78 |
79 | 80 | setPassword(e.target.value)} 86 | required/> 87 |
88 | 89 |
90 |
91 | setRememberMe(e.target.checked)} 97 | /> 98 | 99 |
100 | 101 | 105 |
106 | 107 | 110 | 111 | Sign up 112 |
113 | ); 114 | } 115 | 116 | export default connect(s => ({user: s.user}))(SignInForm); 117 | -------------------------------------------------------------------------------- /src/Component/Auth/SignUp.js: -------------------------------------------------------------------------------- 1 | import React, {useState} from 'react'; 2 | import {connect} from "trim-redux"; 3 | import {Link, withRouter} from "react-router-dom"; 4 | import {toast} from "react-toastify"; 5 | import {regexp} from "../../setup/constant"; 6 | import axios from "axios"; 7 | import {api} from "../../setup/api"; 8 | import {signingIn} from "./__action/signingIn"; 9 | import {route} from "../../setup/route"; 10 | 11 | 12 | 13 | function SignUp(props) { 14 | 15 | const [isLoading, setIsLoading] = useState(false) 16 | const [username, setUsername] = useState('') 17 | const [password, setPassword] = useState('') 18 | const {user} = props 19 | 20 | 21 | 22 | 23 | 24 | function submitSignUp(e) { 25 | //-------------------------------------------// 26 | e.preventDefault() 27 | e.stopPropagation() 28 | const form = e.target 29 | if (form.checkValidity() === false) { 30 | form.classList.add('was-validated') 31 | return false; 32 | } 33 | form.className.replace(" was-validated", "") 34 | //-------------------------------------------// 35 | 36 | setIsLoading(true); 37 | 38 | axios({ 39 | url: api.signup, 40 | method: 'POST', 41 | data: {username, password} 42 | }) 43 | .then((response) => { 44 | // set token to localStorage if remember me checked and get user details 45 | signingIn(response.data.token, true) 46 | .then(function () { 47 | toast.success('signed-up successfully!', {autoClose: 1200}); 48 | }) 49 | .catch(function () { 50 | toast.success('signed-up successfully but occur an error in fetch user details, try again or tell to support!', {autoClose: false}); 51 | }); 52 | }) 53 | .catch(() => { 54 | toast.error('Server Error,try again or tell to support!'); 55 | }) 56 | .then(() => { 57 | setIsLoading(false); 58 | }) 59 | } 60 | 61 | 62 | 63 | 64 | 65 | return ( 66 |
67 |
68 | 69 | setUsername(e.target.value)} 76 | required/> 77 |
78 | 79 |
80 | 81 | setPassword(e.target.value)} 87 | required/> 88 |
password is not valid!
89 |
90 | 91 | 94 | 95 | Sign in 96 |
97 | ); 98 | } 99 | 100 | 101 | export default withRouter(connect(s => ({user: s.user}))(SignUp)); 102 | -------------------------------------------------------------------------------- /src/Component/Auth/UpdatedUser.js: -------------------------------------------------------------------------------- 1 | import {connect} from "trim-redux"; 2 | 3 | const UpdatedUser = props => props.user.updated ? props.children : ''; 4 | 5 | export default connect(s => ({user: s.user}))(UpdatedUser); -------------------------------------------------------------------------------- /src/Component/Auth/ValidUser.js: -------------------------------------------------------------------------------- 1 | import {connect} from "trim-redux"; 2 | import {isValidUser} from "../../setup/utility/isValidUser"; 3 | 4 | // updated with not null token 5 | const ValidUser = props => { 6 | const result = () => { 7 | if (typeof props.children === "function") 8 | return (props.user.detail !== undefined) ? props.children(props.user.detail) : '' 9 | else 10 | return props.children; 11 | }; 12 | 13 | return isValidUser() ? result() : '' 14 | }; 15 | 16 | export default connect(s => ({user: s.user}))(ValidUser); 17 | -------------------------------------------------------------------------------- /src/Component/Auth/__action/authentication.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import {api} from "../../../setup/api"; 3 | import {setStore} from "trim-redux"; 4 | import {toast} from "react-toastify"; 5 | import {signingOut} from "./signingOut"; 6 | 7 | 8 | /** 9 | * authentication 10 | * this method do two action. validation token and get user details and set to redux. 11 | * 12 | * @param token : user authentication key. like "eyJ0eXAiOiJKV1QiLCJhbGciOiJ...." 13 | * @returns {Promise}: when user is valid do then and when invalid do catch 14 | */ 15 | export const authentication = () => { 16 | return axios({url: api.userDetails}) 17 | .then((response) => { 18 | // token is valid and user details ready to use 19 | setStore({ 20 | user: { 21 | updated: true, 22 | detail : response.data 23 | } 24 | }); 25 | }) 26 | .catch((e) => { 27 | // token is invalid or occur an error 28 | signingOut(); 29 | toast.error('authentication error. please log in again.'); 30 | console.error(e); 31 | }); 32 | } 33 | -------------------------------------------------------------------------------- /src/Component/Auth/__action/firstSetup.js: -------------------------------------------------------------------------------- 1 | import {setUserIsGuest} from "./setUserIsGuest"; 2 | import {authentication} from "./authentication"; 3 | import Cookies from "js-cookie"; 4 | 5 | export const firstSetup = function () { 6 | const token = Cookies.get('token'); 7 | if (token) { 8 | // Real user 9 | // when token exists it means that one user has logged-in before 10 | // but it does not mean that the user is valid, so token needs authentication. 11 | // 12 | // when the server says the token is valid, then it's a real and valid user, and 13 | // when the server says it is NOT valid then signingOut() method will run and 14 | // we set user as a Guest user and remove token from localstorage. 15 | authentication() 16 | .then(() => { 17 | /** 18 | * @@@ GET_USER_CART 19 | */ 20 | }) 21 | .catch(() => { 22 | /** 23 | * @@@ Clear_USER_Cart 24 | */ 25 | }) 26 | } else { 27 | // Guest user 28 | setUserIsGuest() 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Component/Auth/__action/setUserIsGuest.js: -------------------------------------------------------------------------------- 1 | import {setStore} from "trim-redux"; 2 | 3 | 4 | /** 5 | * set "updated: true" when the user authentication is done by server and the user information is updated and final 6 | * set "token: null" when user is invalid (is guest) 7 | * NOTICE: The user object of guest user does not have "detail" property. 8 | */ 9 | export const setUserIsGuest = function () { 10 | setStore({user: {updated: true}}); 11 | } 12 | -------------------------------------------------------------------------------- /src/Component/Auth/__action/signingIn.js: -------------------------------------------------------------------------------- 1 | import {authentication} from "./authentication"; 2 | import Cookies from "js-cookie"; 3 | 4 | /** 5 | * signing in user 6 | * 7 | * @param token : user authentication key. like "eyJ0eXAiOiJKV1QiLCJhbGciOiJ...." 8 | * @param rememberMe : state of "remember me" checkbox checked 9 | */ 10 | export const signingIn = (token, rememberMe) => { 11 | // push token to cookie just when user checked "remember me" checkbox. 12 | // NOTICE: if user did not check the "remember me" checkbox, stay logged-in until the page refreshes. 13 | // NOTICE: when your server side setting cookike 14 | // you must remove this part of code 15 | if (rememberMe) 16 | Cookies.set('token', token) 17 | 18 | // token validation and get user detail 19 | return authentication() 20 | .then(() => { 21 | /** 22 | * @@@ GET_USER_CART 23 | */ 24 | }); 25 | } 26 | -------------------------------------------------------------------------------- /src/Component/Auth/__action/signingOut.js: -------------------------------------------------------------------------------- 1 | import {setUserIsGuest} from "./setUserIsGuest"; 2 | import Cookies from "js-cookie"; 3 | 4 | 5 | /** 6 | * signing out user 7 | * clear user data in redux and set Guest User value to it. 8 | */ 9 | export const signingOut = () => { 10 | // clear user detail from redux 11 | setUserIsGuest(); 12 | 13 | // clear user token cookie 14 | Cookies.remove('token') 15 | 16 | /** 17 | * @@@ Clear_USER_Cart 18 | */ 19 | } 20 | -------------------------------------------------------------------------------- /src/Component/Auth/__action/updateUserDetail.js: -------------------------------------------------------------------------------- 1 | import {authentication} from "./authentication"; 2 | 3 | // refetch user details 4 | export const updateUserDetail = () => { 5 | return authentication() 6 | } 7 | -------------------------------------------------------------------------------- /src/Component/Menu/Menu.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Link} from "react-router-dom"; 3 | import {route} from "../../setup/route"; 4 | import {signingOut} from "../Auth/__action/signingOut"; 5 | import ValidUser from "../Auth/ValidUser"; 6 | import InvalidUser from "../Auth/InvalidUser"; 7 | import LoadingUser from "../Auth/LoadingUser"; 8 | import Namespace from "rssr-namespace"; 9 | import "./menu.scss" 10 | import {connect} from "trim-redux"; 11 | 12 | 13 | function Menu({skeleton}) { 14 | 15 | return ( 16 | 17 | 64 | 65 | ) 66 | } 67 | 68 | export default connect(state => ({skeleton: state.skeleton}))(Menu); -------------------------------------------------------------------------------- /src/Component/Menu/menu.scss: -------------------------------------------------------------------------------- 1 | @namespace "menu"; 2 | 3 | .navbar-brand img { 4 | max-width : 50px; 5 | } -------------------------------------------------------------------------------- /src/Component/OverLoading/OverLoading.js: -------------------------------------------------------------------------------- 1 | import React, {useEffect} from 'react'; 2 | import "./overLoading.scss" 3 | import {toggleOverLoading} from "./__action/toggleOverLoading"; 4 | 5 | const OverLoading = () => { 6 | useEffect(() => { 7 | toggleOverLoading(false) 8 | }, []); 9 | 10 | // if (IS_DEVELOPMENT) 11 | // return '' 12 | 13 | return
; 14 | }; 15 | 16 | export default OverLoading; 17 | -------------------------------------------------------------------------------- /src/Component/OverLoading/__action/toggleOverLoading.js: -------------------------------------------------------------------------------- 1 | import {IS_SERVER} from "../../../setup/constant"; 2 | 3 | export const toggleOverLoading = (visible) => { 4 | if (IS_SERVER) 5 | return; 6 | 7 | document.getElementById('over-loading-wrap').style.display = (visible ? 'block' : 'none') 8 | }; -------------------------------------------------------------------------------- /src/Component/OverLoading/overLoading.scss: -------------------------------------------------------------------------------- 1 | #over-loading-wrap { 2 | background : rgba(255, 255, 255, 0.5) url("/asset/img/loading.gif") no-repeat center center; 3 | height : 100%; 4 | left : 0; 5 | min-height : 220px; 6 | min-width : 220px; 7 | position : fixed; 8 | top : 0; 9 | width : 100%; 10 | z-index : 9999999; 11 | } -------------------------------------------------------------------------------- /src/Partial/Router/Router.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Route, Switch} from "react-router-dom"; 3 | import {routeMap} from "../../setup/routeMap"; 4 | import {jumpScrollToTop} from "../../setup/utility/jumpScrollToTop"; 5 | import {IS_PRODUCTION} from "../../setup/constant"; 6 | 7 | 8 | function Router() { 9 | return ( 10 | 11 | { 12 | routeMap.map((route, index) => { 13 | return 14 | }) 15 | } 16 | { 17 | IS_PRODUCTION ? jumpScrollToTop() : '' 18 | } 19 | 20 | ); 21 | }; 22 | 23 | export default Router; -------------------------------------------------------------------------------- /src/Partial/fetcher/DefaultErrors.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Helmet} from "react-helmet-async"; 3 | import Error404 from "../../App/Error404/Error404"; 4 | import {browserHistory} from "../../setup/browserHistory"; 5 | 6 | 7 | // use for catching axios error during the fetching process with fetcher 8 | // NOTICE: all non0-200 response status will be catched by this view unless you write catch in fetch function for them 9 | const DefaultErrors = (props) => { 10 | const {status, code, data} = props.data; 11 | 12 | if (status === 404) 13 | return 14 | 15 | return ( 16 |
17 | 18 |
19 |
20 |

Error {status}

21 |
{JSON.stringify(data)}
22 | 23 | {code ? code : ''} 24 | 25 |
26 | 30 |
31 |
32 |
33 | ); 34 | }; 35 | 36 | export default DefaultErrors; 37 | -------------------------------------------------------------------------------- /src/Partial/fetcher/clientFetcher.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import {connect, setStore} from "trim-redux"; 3 | import {defaultState} from "../../setup/store"; 4 | import {parse} from "querystringify"; 5 | import {isErrorData} from "../../setup/utility/isErrorData"; 6 | import {responseValidation} from "../../setup/utility/responseValidation"; 7 | import {convertErrorToResponse} from "../../setup/utility/convertErrorToResponse"; 8 | import DefaultErrors from "./DefaultErrors"; 9 | import axios from "axios"; 10 | 11 | 12 | /** 13 | * provider Fetcher HOC of client side 14 | * 15 | * Fetcher is a HOC which wraps 'TheComponent' 16 | * in order to handle fetching data actions of 'TheComponent'. 17 | * Fetcher in client contians all fetch actions. 18 | * 19 | * @param TheComponent : React Component 20 | * @returns {Fecher} : Fetcher HOC of client side 21 | */ 22 | export const clientFetcher = function (TheComponent) { 23 | 24 | const stateName = TheComponent.stateName; 25 | 26 | class Fecher extends Component { 27 | constructor(props) { 28 | super(props); 29 | 30 | this.setParams(); 31 | 32 | if (this.needFetch()) 33 | this.fetchProvider() 34 | else 35 | this.debugLog(false) 36 | } 37 | 38 | 39 | 40 | 41 | 42 | needFetch() { 43 | let needFetch = false; 44 | try { 45 | needFetch = JSON.stringify(this.props[stateName]) === JSON.stringify(defaultState[stateName]) 46 | } catch (err) { 47 | console.error('⚠ data is not valid.', err); 48 | } 49 | return needFetch 50 | } 51 | 52 | 53 | 54 | 55 | 56 | // params passed to fetch() on the client 57 | setParams() { 58 | this.ftechParams = { 59 | match: this.props.match, 60 | query: parse(window.location.search) 61 | } 62 | 63 | return true; 64 | } 65 | 66 | 67 | 68 | 69 | 70 | // fetch data and insert to 'stateName' 71 | fetchProvider() { 72 | this.debugLog(true); 73 | 74 | const request = TheComponent.fetch(this.ftechParams); 75 | 76 | this.cancelRequest = request.cancel; 77 | 78 | request.then((response) => { 79 | responseValidation(response); 80 | setStore(stateName, response.data); 81 | }) 82 | .catch(function (err) { 83 | // ignore canceled request 84 | if (axios.isCancel(err)) 85 | return; 86 | 87 | const response = convertErrorToResponse(err); 88 | setStore(stateName, response.data); 89 | }) 90 | .then(() => { 91 | delete this.cancelRequest; 92 | }) 93 | } 94 | 95 | 96 | 97 | 98 | 99 | // log fetch type in development environment 100 | debugLog(inClient) { 101 | if (JSON.parse(process.env.RSSR_FETCHER_DEBUG)) 102 | console.info((inClient ? '🙎‍♂️' : '🌎') + ' fetch ' + this.props.match.url + ' in ' + (inClient ? 'client' : 'server')); 103 | } 104 | 105 | 106 | 107 | 108 | 109 | resetDataHolder() { 110 | const defaultValue = defaultState[stateName]; 111 | setStore(stateName, defaultValue); 112 | 113 | // when try to fetch but the last equal fetch was not completed 114 | if (this.cancelRequest) { 115 | this.cancelRequest(); 116 | delete this.cancelRequest; 117 | } 118 | } 119 | 120 | 121 | 122 | 123 | 124 | shouldComponentUpdate(nextProps, nextState) { 125 | return this.setParams() 126 | } 127 | 128 | 129 | 130 | 131 | 132 | // update when route updates. For example click on '/post/2' in mounted component with path '/post/1' 133 | // needFetch() needs switching between 2 route paths with equal component 134 | componentDidUpdate(prevProps) { 135 | if (this.props.location.key === prevProps.location.key && !this.needFetch()) 136 | return; 137 | 138 | // update match 139 | this.ftechParams.match = this.props.match; 140 | 141 | // to show loading 142 | this.resetDataHolder(); 143 | 144 | // get data of new route 145 | this.fetchProvider(); 146 | } 147 | 148 | 149 | 150 | 151 | // then clear state to refetching data on next mounting 152 | componentWillUnmount() { 153 | this.resetDataHolder(); 154 | } 155 | 156 | 157 | 158 | 159 | 160 | render() { 161 | const data = this.props[stateName]; 162 | if (isErrorData(data)) 163 | return 164 | 165 | return ; 166 | } 167 | } 168 | 169 | 170 | return connect(s => ({[stateName]: s[stateName]}))(Fecher); 171 | } 172 | -------------------------------------------------------------------------------- /src/Partial/fetcher/fetcher.js: -------------------------------------------------------------------------------- 1 | import {IS_SERVER} from "../../setup/constant"; 2 | import {serverFetcher} from "./serverFetcher"; 3 | import {clientFetcher} from "./clientFetcher"; 4 | 5 | 6 | 7 | /** 8 | * fetcher is a HOC provider 9 | * 10 | * Fetcher is a HOC and wrap 'TheComponent' 11 | * to can handel fetching data actions of 'TheComponent' 12 | * 13 | * Fetcher in client contian all fetch actions 14 | * but in server just an interface 15 | * to pass duct to 'TheComponent' when type of fetch is props base 16 | * an in redux base is an empty component 17 | * (need empty component to avoid React 'compoenet not found' error) 18 | * 19 | * TheComponent: React Component 20 | * returns {Fecher}: Fetcher Component 21 | * 22 | * param TheComponent : React component 23 | * param fetchFn : fetch function 24 | * param reduxState : name of redux state : default is name of TheComponent 25 | * returns {Fecher} 26 | */ 27 | export const fetcher = (TheComponent, fetchFn, stateName) => { 28 | let Fecher; 29 | 30 | TheComponent.stateName = stateName; 31 | TheComponent.fetch = fetchFn 32 | 33 | if (IS_SERVER) 34 | Fecher = serverFetcher(TheComponent, stateName); 35 | else 36 | Fecher = clientFetcher(TheComponent); 37 | 38 | // clone static props 39 | Object.getOwnPropertyNames(TheComponent).forEach(function (key) { 40 | if (!Fecher.hasOwnProperty(key) && key !== 'caller' && key !== 'arguments') 41 | Fecher[key] = TheComponent[key]; 42 | }) 43 | 44 | return Fecher; 45 | } 46 | -------------------------------------------------------------------------------- /src/Partial/fetcher/serverFetcher.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import {connect} from "trim-redux"; 3 | import {isErrorData} from "../../setup/utility/isErrorData"; 4 | import DefaultErrors from "./DefaultErrors"; 5 | 6 | /** 7 | * provider Fetcher HOC of server side 8 | * 9 | * Fetcher HOC in server is just an interface which passes the fetched data in 10 | * redux state to 'TheComponent' 11 | * 12 | * @param TheComponent : React Component 13 | * @returns {Fecher} : Fetcher HOC of server side 14 | */ 15 | export const serverFetcher = function (TheComponent, stateName) { 16 | 17 | let Fecher = function (props) { 18 | 19 | const data = props[stateName] 20 | 21 | // handle errors 22 | if (isErrorData(data)) 23 | return 24 | 25 | // connect to redux 26 | const mstp = state => ({ 27 | [stateName]: state[stateName] 28 | }) 29 | 30 | TheComponent = connect(mstp)(TheComponent); 31 | 32 | return ; 33 | } 34 | 35 | return connect(s => ({[stateName]: s[stateName]}))(Fecher); 36 | } 37 | -------------------------------------------------------------------------------- /src/Partial/skeleton/debugLog.js: -------------------------------------------------------------------------------- 1 | // active switch for debugging logs 2 | const debug = JSON.parse(process.env.RSSR_SKELETON_DEBUG); 3 | 4 | export const debugLog = function (msgKey) { 5 | if (!debug) 6 | return; 7 | 8 | let msg; 9 | switch (msgKey) { 10 | case "INVALID_CACHE_VALUE": 11 | msg = 'cache property is not number or more than zero milliseconds. set skeleton cache parameter.'; 12 | break; 13 | case "WENT_WELL": 14 | msg = 'everything went well, data was successfully fetched in Server and was received by Client.'; 15 | break; 16 | case "FETCHED_IN_CLIENT": 17 | msg = 'fetch errored in server but data was successfully fetched in Client.'; 18 | break; 19 | case "CLIENT_ERRORED": 20 | msg = 'fetch had error in both sides.The default data is now used.'; 21 | break; 22 | case "READ_FROM_CACHE": 23 | msg = 'read data from cache.'; 24 | break; 25 | case "CACHE_EXPIRED": 26 | msg = 'delete expired cache.'; 27 | break; 28 | case "CACHING_DATA": 29 | msg = 'caching fetched data.'; 30 | break; 31 | case "FETCHING_API": 32 | msg = 'fetching data from API.'; 33 | break; 34 | case "SUCCESSFULLY_FETCH": 35 | msg = 'data was fetched successfully in server and pushed to skeleton state.'; 36 | break; 37 | case "SERVER_ERRORED": 38 | msg = 'fetch had error in server.'; 39 | break; 40 | default: 41 | msg = 'message key is invalid!' 42 | } 43 | 44 | msg = '[' + msgKey + '] ' + msg; 45 | 46 | console.info('SKELETON > ' + msg) 47 | } 48 | -------------------------------------------------------------------------------- /src/Partial/skeleton/skeleton.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {skeletonClientProvider} from "./skeletonClientProvider"; 3 | import {debugLog} from "./debugLog"; 4 | 5 | 6 | export const skeleton = function (TheComponent, fetchFn, cache) { 7 | // cache is disabled 8 | if (typeof cache !== "number" || cache <= 0) { 9 | debugLog('INVALID_CACHE_VALUE') 10 | return true; 11 | } 12 | 13 | const Skeleton = function (props) { 14 | // refetch skeleton in client when server fetch skeleton encounters an error 15 | if (typeof window !== 'undefined') 16 | skeletonClientProvider(fetchFn) 17 | 18 | return 19 | } 20 | 21 | Skeleton.skeleton = fetchFn; 22 | Skeleton.skeleton.cache = cache; 23 | 24 | return Skeleton; 25 | } 26 | -------------------------------------------------------------------------------- /src/Partial/skeleton/skeletonClientProvider.js: -------------------------------------------------------------------------------- 1 | import {getStore, setStore} from "trim-redux"; 2 | import {parse} from "querystringify"; 3 | import {matchPath} from "react-router-dom"; 4 | import {browserHistory} from "../../setup/browserHistory"; 5 | import {routeMap} from "../../setup/routeMap"; 6 | import {debugLog} from "./debugLog"; 7 | 8 | 9 | export const skeletonClientProvider = function (fetchFn) { 10 | // when server fetches data successfully 11 | const skeleton = getStore('skeleton'); 12 | if (skeleton && !skeleton.isErrorData) { 13 | debugLog('WENT_WELL') 14 | return; 15 | } 16 | 17 | // calculate fetch params 18 | const ftechParams = { 19 | match: {}, 20 | query: parse(window.location.search) 21 | }; 22 | 23 | routeMap.find(route => { 24 | // is object for matched or null for not matched 25 | const match = matchPath(browserHistory.location.pathname, route); 26 | 27 | if (match) 28 | ftechParams.match = match; 29 | 30 | return match; 31 | }); 32 | 33 | fetchFn(ftechParams) 34 | .then(function (response) { 35 | setStore('skeleton', response.data) 36 | debugLog('FETCHED_IN_CLIENT') 37 | }) 38 | .catch(function (err) { 39 | console.error(err) 40 | debugLog('CLIENT_ERRORED') 41 | }) 42 | } 43 | -------------------------------------------------------------------------------- /src/Partial/skeleton/skeletonServerProvider.js: -------------------------------------------------------------------------------- 1 | import {responseValidation} from "../../setup/utility/responseValidation"; 2 | import {errorLogger} from "../../setup/utility/errorLogger"; 3 | import App from "../../App/App"; 4 | import {debugLog} from "./debugLog"; 5 | 6 | 7 | 8 | // locking the fetch to prevent parallel requests. 9 | let fetchLock = false; 10 | 11 | 12 | 13 | /** 14 | * call skeleton fetch and handle errors 15 | */ 16 | export const skeletonServerProvider = async function (DUCT) { 17 | // skeleton is disabled 18 | if (!App.skeleton) 19 | return true; 20 | 21 | try { 22 | await skeletonFetch(DUCT); 23 | } catch (err) { 24 | errorLogger('SKELETON >', err, false, DUCT.req); 25 | } 26 | } 27 | 28 | 29 | 30 | 31 | /** 32 | * try to read data from cache or get data from API and update cache 33 | */ 34 | const skeletonFetch = async function (DUCT) { 35 | const skeleton = App.skeleton 36 | 37 | // *** reset in develop *** delete global['SKELETON-CACHED-DATA']; 38 | const data = global['SKELETON-CACHED-DATA']; 39 | 40 | // read data from cache if not expired 41 | if (data !== undefined) { 42 | const notExpired = (global['SKELETON-CACHE-EXP'] - Date.now()) > 0; 43 | if (notExpired) { 44 | debugLog('READ_FROM_CACHE') 45 | pushDataToUpdatedState.success(DUCT, data) 46 | return true; 47 | } else { 48 | debugLog('CACHE_EXPIRED') 49 | delete global['SKELETON-CACHED-DATA'] 50 | } 51 | } 52 | 53 | if (!fetchLock) { 54 | fetchLock = true; 55 | await skeletonGetDataFromApi(DUCT) 56 | .then(function (data) { 57 | debugLog('CACHING_DATA') 58 | global['SKELETON-CACHED-DATA'] = data 59 | global['SKELETON-CACHE-EXP'] = Date.now() + skeleton.cache; 60 | }) 61 | } 62 | } 63 | 64 | 65 | 66 | 67 | 68 | /** 69 | * 1) response validation 70 | * 2) push data to updatedState (redux) 71 | */ 72 | function skeletonGetDataFromApi(DUCT) { 73 | const skeleton = App.skeleton 74 | debugLog('FETCHING_API') 75 | 76 | // pass to skeleton fetch as params 77 | const ftechParams = { 78 | req: DUCT.req, // Express js request object 79 | match: DUCT.match, // match is match object of react-router-dom 80 | query: DUCT.req.query //exp: {foo:'bar'} in 'http://www.site.com/post/1?foo=bar' 81 | } 82 | 83 | return new Promise(function (resolve, reject) { 84 | skeleton(ftechParams) 85 | .then(function (response) { 86 | responseValidation(response) 87 | pushDataToUpdatedState.success(DUCT, response.data) 88 | debugLog('SUCCESSFULLY_FETCH') 89 | resolve(response.data); 90 | }) 91 | .catch(function (err) { 92 | debugLog('SERVER_ERRORED') 93 | // push error to updatedState 94 | pushDataToUpdatedState.error(DUCT) 95 | reject(err); 96 | }) 97 | .then(function () { 98 | fetchLock = false; 99 | }) 100 | }) 101 | } 102 | 103 | 104 | 105 | 106 | 107 | /** 108 | * set value of 'skeleton' OR 'skeletonErroredInServer' in 'updatedState' 109 | */ 110 | const pushDataToUpdatedState = { 111 | success: function (DUCT, data) { 112 | DUCT.updatedState['skeleton'] = data 113 | }, 114 | error: function (DUCT) { 115 | DUCT.updatedState['skeleton'] = {isErrorData: true} 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/render/Template/Error.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Helmet} from "react-helmet-async"; 3 | import {IS_DEVELOPMENT} from "../../setup/constant"; 4 | 5 | 6 | function Error(props) { 7 | return ( 8 |
9 | 10 | { 11 | !IS_DEVELOPMENT ? 12 |
13 |

Process Error

14 |

15 | Sorry, an error occurred during processing. if possible, go back to the previous page and contact support. 16 |

17 |
18 | : '' 19 | } 20 |
21 | {props.error.message} 22 |
23 | { 24 | IS_DEVELOPMENT ? 25 |
{props.error.stack}
26 | : 27 | : '' 49 | } 50 | 51 | 52 | 53 | 54 | ); 55 | } 56 | 57 | export default Index; -------------------------------------------------------------------------------- /src/render/client.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import reactDom from "react-dom"; 3 | import {Provider} from "trim-redux"; 4 | import {Router} from "react-router-dom"; 5 | import {HelmetProvider} from 'react-helmet-async'; 6 | 7 | // --- Structures ---// 8 | import {clientCreateStore} from "../setup/store"; 9 | import {browserHistory} from "../setup/browserHistory"; 10 | import {localStorageSetup} from "../setup/localStorage"; 11 | import "../setup/axiosConfig" 12 | import App from "../App/App"; 13 | 14 | //---- jQuery Plugins ----// 15 | import "../setup/utility/samplejQueryPlugin"; 16 | 17 | //---- Public styles ----// 18 | import "../setup/style/public.scss"; 19 | // react-toastify 20 | import "react-toastify/dist/ReactToastify.min.css"; 21 | // Bootstrap 22 | import "bootstrap"; 23 | import "bootstrap/dist/css/bootstrap.min.css"; 24 | 25 | 26 | 27 | if (!window.RSSR_PROCCESS_ERROR) { 28 | // define public structure and variables 29 | localStorageSetup(); 30 | 31 | // create redux store with posted value from "RSSR_UPDATED_REDUX_STATES" 32 | const store = clientCreateStore(); 33 | 34 | // root element of application 35 | const appWrap = document.getElementById('app-root'); 36 | 37 | // client app 38 | const app = ( 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | ) 47 | 48 | 49 | // render on client with hydrate() and render() when does not have Child Nodes 50 | const method = module.hot ? reactDom.render : reactDom.hydrate; 51 | method(app, appWrap); 52 | } 53 | -------------------------------------------------------------------------------- /src/render/server/fetchProvider.js: -------------------------------------------------------------------------------- 1 | // import als from "async-local-storage"; 2 | import {responseValidation} from "../../setup/utility/responseValidation"; 3 | import {convertErrorToResponse} from "../../setup/utility/convertErrorToResponse"; 4 | 5 | 6 | 7 | // fetch data of component from server 8 | export const fetchProvider = async function (DUCT) { 9 | // const fetch = als.get('fetch') 10 | const fetch = DUCT.fetch 11 | 12 | // when component does not have fetch() then fetch is undefined and fetchType is 'WITH_OUT_FETCH' 13 | if (!fetch) { 14 | debugLog('with out FETCH', DUCT) 15 | return true 16 | } 17 | 18 | debugLog('fetching data', DUCT) 19 | 20 | // pass to fetch() as params 21 | const ftechParams = { 22 | req: DUCT.req, // Express js request object 23 | // match: als.get('match'), // match is match object of react-router-dom 24 | match: DUCT.match, // match is match object of react-router-dom 25 | query: DUCT.req.query //exp: {foo:'bar'} in 'http://www.site.com/post/1?foo=bar' 26 | } 27 | 28 | // NOTICE: catch() will be handled on the server.js with failedRes() 29 | await 30 | fetch(ftechParams) 31 | .then(function (response) { 32 | debugLog('fetched SUCCESSFULLY', DUCT) 33 | fetchResponsePreparing(DUCT, response) 34 | }) 35 | .catch(function (error) { 36 | debugLog('ERROR in fetch', DUCT) 37 | const response = convertErrorToResponse(error, DUCT.req) 38 | fetchResponsePreparing(DUCT, response) 39 | }) 40 | } 41 | 42 | 43 | 44 | 45 | /** 46 | * 1) response validation 47 | * 2) set response status code 48 | * 3) push data to updatedState (redux) 49 | */ 50 | function fetchResponsePreparing(DUCT, response) { 51 | // execute 'throw new Error' if response is not valid 52 | responseValidation(response) 53 | 54 | // set response status code 55 | // als.set('status', response.status, true) 56 | DUCT.status = response.status 57 | 58 | // const stateName = als.get('stateName') 59 | // const updatedState = als.get('updatedState') 60 | const stateName = DUCT.stateName 61 | const updatedState = DUCT.updatedState 62 | updatedState[stateName] = response.data 63 | // als.set('updatedState', updatedState, true) 64 | DUCT.updatedState = updatedState 65 | 66 | // use for improving SEO 67 | if (response.schema) 68 | DUCT.schema = response.schema; 69 | // als.set('schema', response.schema, true) 70 | } 71 | 72 | 73 | 74 | 75 | 76 | // active switch for debugging logs 77 | const debug = JSON.parse(process.env.RSSR_FETCHER_DEBUG); 78 | 79 | function debugLog(msg, DUCT) { 80 | if (debug) 81 | console.info('FETCH > ' + msg + '. route: ', DUCT.req.originalUrl) 82 | } 83 | -------------------------------------------------------------------------------- /src/render/server/initialize.js: -------------------------------------------------------------------------------- 1 | import {matchPath} from "react-router-dom"; 2 | import {routeMap} from "../../setup/routeMap"; 3 | 4 | 5 | 6 | // define public structure and variables 7 | export const initialize = function (DUCT) { 8 | /** updatedState **/ 9 | // we use updatedState to set value of RSSR_UPDATED_REDUX_STATES in index template 10 | // to pass data to the client for syncing redux and merge with defaultState 11 | // of 'stateName' to create store on the server 12 | DUCT.updatedState = {} 13 | 14 | 15 | 16 | 17 | /** match **/ 18 | /* 19 | * CONSTANT {undefined || object} 20 | * 21 | * match is the match object of react-router-dom 22 | * match of "site.com/post/1" is { path: '/post/:postId', url: '/post/1', isExact: true, params: {postId: '1'} } 23 | */ 24 | const matchedRouteMapItem = routeMap.find(route => { 25 | // is object for matched or null for not matched 26 | const match = matchPath(DUCT.req.path, route); 27 | 28 | if (match) 29 | DUCT.match = match 30 | 31 | return match; 32 | }); 33 | 34 | // can not match to any route map item 35 | if (matchedRouteMapItem === undefined) 36 | throw new Error('⛔ can not match to any route map item! define "*" path for not matched routes to be able to handle e-404, page not found errors.'); 37 | 38 | 39 | 40 | 41 | 42 | const hasComponent = matchedRouteMapItem.hasOwnProperty('component') 43 | const hasFetch = hasComponent && matchedRouteMapItem.component.hasOwnProperty('fetch') 44 | const hasStateName = hasComponent && matchedRouteMapItem.component.hasOwnProperty('stateName') 45 | 46 | if (hasFetch) { 47 | if (!hasStateName) 48 | throw new Error('⛔ component does not "stateName" param. when define fetch() for component, you must define "stateName" param.'); 49 | 50 | /** fetch **/ 51 | /* 52 | * CONSTANT {function || undefinded} 53 | * 54 | * fetch() method of component of matched route item 55 | * when component has not fetch() then fetch is undefined 56 | * NOTICE: when "fetch" is undefind mean hasFetch is false 57 | */ 58 | DUCT.fetch = matchedRouteMapItem.component.fetch 59 | 60 | /** stateName **/ 61 | /* 62 | * CONSTANT {string} 63 | * 64 | * stateName is name of 'stateName' state and define when fetch type is 65 | */ 66 | DUCT.stateName = matchedRouteMapItem.component.stateName 67 | 68 | } else if (hasStateName) { 69 | throw new Error('⛔ component does not fetch() param. when define "stateName" for component, you must define fetch() param.'); 70 | } 71 | 72 | 73 | 74 | 75 | 76 | /** status **/ 77 | /* 78 | * VARIABLE {number} 79 | * 80 | * response status code like 200 81 | * status get value from sevral places 82 | * NOTICE: when occur status is 301 83 | * 84 | * SUCCESSFUL (render index template successfully) 85 | * first --> get status prop of matchedRouteMapItem if exist (item of routeMap) 86 | * second -> if matchedRouteMapItem has not status then status is 200 (default value) 87 | * third --> if hasFetch is true then get fetch status in fetchProvider() 88 | * fourth -> status is 500 when DUCT.status is undefined (defined in render() - send response place) 89 | * 90 | * ERROR (defined in failedRequest() - occur an error in proccess) 91 | * only -> status is 500 92 | */ 93 | const status = matchedRouteMapItem.status !== undefined ? matchedRouteMapItem.status : 200; 94 | 95 | if (typeof status !== 'number') 96 | throw new Error('⛔ status of routeMap is NOT number. status must be number like 404. status is ' + status); 97 | 98 | DUCT.status = status 99 | } 100 | -------------------------------------------------------------------------------- /src/render/server/render.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOMServer from "react-dom/server"; 3 | import {StaticRouter} from "react-router-dom"; 4 | import {HelmetProvider} from 'react-helmet-async'; 5 | import {Provider} from "react-redux"; 6 | import {createStore, defaultState} from "../../setup/store"; 7 | import App from "../../App/App"; 8 | import {errorLogger} from "../../setup/utility/errorLogger"; 9 | import Index from "../Template/Index"; 10 | import Error from "../Template/Error"; 11 | 12 | 13 | 14 | /** 15 | * to render view on the server and send response as HTML to client 16 | */ 17 | export const render = function (error, DUCT) { 18 | let view; 19 | const routerContext = {}; 20 | const helmetContext = {}; 21 | 22 | if (!error) { 23 | // normal views 24 | const updatedState = DUCT.updatedState 25 | const dataExist = Object.getOwnPropertyNames(updatedState).length; 26 | const states = dataExist ? {...defaultState, ...updatedState} : undefined; // when passed states is undefined then createStore use defaultState 27 | const store = createStore(states); 28 | 29 | view = ( 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | ); 38 | } else { 39 | // when error occured during fetch and process 40 | errorLogger('SERVER >', error, false, DUCT.req); 41 | view = ( 42 | 43 | 44 | 45 | ) 46 | } 47 | 48 | // render view to HTML 49 | const renderedView = ReactDOMServer.renderToString(view); 50 | 51 | if (!routerContext.url) { 52 | // get final response status code 53 | // const status = !error ? (als.get('status') || 500) : 500; 54 | const status = !error ? (DUCT.status|| 500) : 500; 55 | 56 | // make HTML response 57 | let response = ; 58 | response = ReactDOMServer.renderToString(response); 59 | response = '' + response; 60 | 61 | DUCT.res.status(status).send(response); 62 | } else { 63 | // when rendered 64 | DUCT.res.redirect(301, routerContext.url); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/render/server/server.js: -------------------------------------------------------------------------------- 1 | import {render} from "./render"; 2 | import {fetchProvider} from "./fetchProvider"; 3 | import {initialize} from "./initialize"; 4 | import {skeletonServerProvider} from "../../Partial/skeleton/skeletonServerProvider"; 5 | import "../../setup/axiosConfig"; 6 | 7 | 8 | export default function serverRenderer() { 9 | return async (req, res) => { 10 | 11 | // DUCT is a channel to access and pass data between sections 12 | // and will be completed in initialize() method 13 | const DUCT = {req, res} 14 | 15 | 16 | //------- Test of Memory usage-----------// 17 | //--------------------------------------// 18 | // const used = process.memoryUsage(); 19 | // let str = ''; 20 | // for (let key in used) { 21 | // if (key !== 'external') 22 | // str += ` ${key} ${Math.round(used[key] / 1024 / 1024 * 100) / 100} MB / ` 23 | // } 24 | // console.log(str) 25 | // 26 | // DUCT.bigArray =Array(1e6).fill("some string"); 27 | //--------------------------------------// 28 | //--------------------------------------// 29 | 30 | 31 | // make response 32 | const response = (error) => render(error, DUCT); 33 | 34 | try { 35 | // define basic parameters 36 | initialize(DUCT); 37 | 38 | // handle skeleton data 39 | await skeletonServerProvider(DUCT); 40 | 41 | // call fetch() of component and get data 42 | fetchProvider(DUCT) 43 | .then(() => response()) // get data successfully 44 | .catch((err) => response(err)); // return error if any occured in fetchProvider() or render() 45 | } catch (err) { 46 | response(err) // return error occurance in try 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/setup/api.js: -------------------------------------------------------------------------------- 1 | /** 2 | * API path of your application. 3 | * exp: 4 | * if you get home page data from 'https://api.site.com/home, you must 5 | * define 'https://api.site.com' as API_HOST_IN_CLIENT and API_HOST_IN_SERVER 6 | * in .env file in root of project, then in api object down below define 'home' property with '/home' value 7 | * and in fetchData method use 'api.home' to access to api url. 8 | */ 9 | let api = { 10 | // authentication 11 | signin: '/signin', 12 | signup: '/signup', 13 | userDetails: '/userDetails', 14 | forgetPassword : '/forgetPassword', 15 | resetPassword: { 16 | trust: '/resetPasswordTrust', 17 | submit: '/resetPasswordSubmit', 18 | }, 19 | skeleton:'/skeleton', 20 | posts: '/posts', 21 | post: id => '/posts/' + id, 22 | } 23 | 24 | export {api}; 25 | -------------------------------------------------------------------------------- /src/setup/axiosConfig.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import {API_DOMAIN} from "./constant"; 3 | 4 | // set Global axios defaults 5 | axios.defaults.baseURL = API_DOMAIN; 6 | axios.defaults.timeout = 58000; // fix uncontrolled server 502 Error 7 | 8 | /************************* AXIOS template ************************* 9 | axios({ 10 | url: api, 11 | method: 'POST', 12 | data: { 13 | test: 'test' // your data params 14 | } 15 | }) 16 | .then((response) => { 17 | // actions 18 | }) 19 | .catch(function (err) { 20 | if (err.response && err.response.status === 400) 21 | return toast.error('your custom error'); 22 | 23 | badConnectionAlert('whereOf'); 24 | }) 25 | 26 | 27 | ***************************************************************/ 28 | -------------------------------------------------------------------------------- /src/setup/browserHistory.js: -------------------------------------------------------------------------------- 1 | import {createBrowserHistory} from "history"; 2 | import {IS_BROWSER} from "./constant"; 3 | 4 | 5 | // create a browser history 6 | export const browserHistory = (IS_BROWSER) ? createBrowserHistory() : {push: () => ''}; 7 | -------------------------------------------------------------------------------- /src/setup/constant.js: -------------------------------------------------------------------------------- 1 | import serialize from "serialize-javascript"; 2 | 3 | /** 4 | * IS_BROWSER in client is 'true' and in server is 'false' and IS_SERVER is reversed 5 | */ 6 | export const IS_BROWSER = typeof window !== 'undefined'; 7 | export const IS_SERVER = !IS_BROWSER; 8 | 9 | export const IS_DEVELOPMENT = process.env.NODE_ENV === 'development'; 10 | export const IS_PRODUCTION = process.env.NODE_ENV === 'production'; 11 | 12 | 13 | 14 | 15 | 16 | /** 17 | * is IOS device 18 | */ 19 | export const isIOS = IS_BROWSER ? !!navigator.platform && /iPad|iPhone|iPod/.test(navigator.platform) : false; 20 | 21 | 22 | 23 | 24 | 25 | /** 26 | * is PWA mode 27 | */ 28 | export const isPWA = IS_BROWSER ? window.matchMedia('(display-mode: standalone)').matches : false; 29 | 30 | 31 | 32 | 33 | 34 | /** 35 | * API_HOST_IN_CLIENT (in .env file) use in browser for AJAX request and 36 | * API_HOST_IN_SERVER in server (node js) for HTTP request (fetch data) 37 | * 38 | * this routes can be equal for two side but 39 | * in server (fetch data) is better use API machine IP. 40 | * for example: 41 | * API_HOST_IN_CLIENT = https://api.site.com 42 | * API_HOST_IN_SERVER = 192.168.2.1 43 | */ 44 | export const API_DOMAIN = IS_BROWSER ? process.env.API_HOST_IN_CLIENT : process.env.API_HOST_IN_SERVER; 45 | 46 | 47 | 48 | 49 | /** 50 | * regex pattern 51 | * use for validation form 52 | */ 53 | export const regexp = { 54 | //like: m.ebrahimiaval@gmail.com 55 | email: '((([^<>()\\[\\]\\\\.,;:\\s@"]+(\\.[^<>()\\[\\]\\\\.,;:\\s@"]+)*)|(".+"))@((\\[[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\])|(([a-zA-Z\\-0-9]+\\.)+[a-zA-Z]{2,})))', 56 | 57 | //like: 09199624169 OR +989199624169 58 | mobileNumber: '(^(\\+98|0)?9\\d{9}$)', 59 | 60 | // user password (like: SignInForm) 61 | password: '(^.{6,64}$)', 62 | }; 63 | 64 | 65 | 66 | // used in for optimize SEO 67 | export const SITE_SCHEMA = serialize({ 68 | "@context": "http://schema.org", 69 | "@type": "LocalBusiness", 70 | "name": "your site title", 71 | "@id": "https://www.your-site-domain.com", 72 | "url": "https://your-site-domain.com", 73 | "image": "https://your-site-domain.com/app-logo.png", 74 | "logo": "https://your-site-domain.com/app-logo.png", 75 | "telephone": "+989199624169", 76 | "geo": { 77 | "@type": "GeoCoordinates", 78 | "latitude": 35.8439292, // your location 79 | "longitude": 50.9544032 // your location 80 | }, 81 | "contactPoint": [ 82 | { 83 | "@type": "ContactPoint", 84 | "telephone": "your phone number", 85 | "contactType": "customer service" 86 | } 87 | ], 88 | "openingHoursSpecification": { 89 | "@type": "OpeningHoursSpecification", 90 | "dayOfWeek": [ 91 | "Monday", 92 | "Tuesday", 93 | "Wednesday", 94 | "Thursday", 95 | "Saturday", 96 | "Sunday" 97 | ], 98 | "opens": "08:00", 99 | "closes": "23:00" 100 | }, 101 | "sameAs": [ 102 | "https://www.instagram.com/your-social-media-account" 103 | ] 104 | }) -------------------------------------------------------------------------------- /src/setup/localStorage.js: -------------------------------------------------------------------------------- 1 | import storage from "local-storage"; 2 | 3 | 4 | export const localStorageSetup = function () { 5 | /** 6 | * version 7 | * 8 | * we set static version for each important release to be able to remove some values in some versions. 9 | * first version is 1 and add one in each time. 10 | */ 11 | const version = 1; 12 | 13 | // define version for the first time 14 | if (storage('version') === null) 15 | storage('version', version); 16 | 17 | // available version in localstorage (sometimes meaning previous version) 18 | const nowVersion = storage('version'); 19 | 20 | 21 | 22 | 23 | 24 | /** 25 | * UPDATE 26 | * 27 | * you can access to last value by version number for removing or editing 28 | */ 29 | if (nowVersion !== version) { 30 | // UPDATE sample: 31 | // if (nowVersion < 5 && nowVersion > 2) { 32 | // localStorage.removeItem('sampleValue'); 33 | 34 | // update available vesion 35 | storage('version', version); 36 | } 37 | 38 | 39 | 40 | 41 | 42 | /** 43 | * DEFAULT 44 | * 45 | * define default value 46 | */ 47 | // DEFAULT sample: 48 | // storage('sampleValue', 'SAMPLE_DEFAULT_VALUE'); 49 | 50 | } 51 | -------------------------------------------------------------------------------- /src/setup/route.js: -------------------------------------------------------------------------------- 1 | export const route = { 2 | home: '/', 3 | post: id => '/post/' + id, 4 | resetPassword: token => '/reset-password/' + token, 5 | 6 | // --- sign in,up --- 7 | sign: type => '/sign/' + type, 8 | } 9 | 10 | 11 | // for improving DX 12 | route.signIn = route.sign('in') 13 | route.signUp = route.sign('up') 14 | -------------------------------------------------------------------------------- /src/setup/routeMap.js: -------------------------------------------------------------------------------- 1 | import Error404 from "../App/Error404/Error404"; 2 | import Home from "../App/Home/Home"; 3 | import Post from "../App/Post/Post"; 4 | import ResetPassword from "../Component/Auth/ResetPassword"; 5 | import {route} from "./route"; 6 | import Sign from "../App/Sign/Sign"; 7 | 8 | 9 | export const routeMap = [ 10 | { 11 | path: route.home, 12 | component: Home, 13 | exact: true 14 | }, 15 | { 16 | path: route.post( ':postId'), 17 | component: Post 18 | }, 19 | { 20 | path: route.resetPassword(':token'), 21 | component: ResetPassword 22 | }, 23 | 24 | { 25 | path: route.sign(':type'), 26 | component: Sign 27 | }, 28 | 29 | // ------- E404 ------- 30 | { 31 | path: "*", 32 | component: Error404, 33 | status: 404 34 | } 35 | ]; 36 | -------------------------------------------------------------------------------- /src/setup/store.js: -------------------------------------------------------------------------------- 1 | import {compose, createStore as createStoreProvider} from 'trim-redux'; 2 | import {IS_BROWSER} from "./constant"; 3 | 4 | 5 | 6 | 7 | 8 | /** 9 | * Redux states 10 | * 11 | * each item in this list is one state in redux store 12 | * and value of this is the default value 13 | */ 14 | export const defaultState = { 15 | user: {updated: false, detail: null}, 16 | post: null, 17 | homepage: {isLoading: true}, 18 | skeleton: { 19 | // when error occurs in both client and server then skeleton contains this data 20 | dailyMessage: 'opps! occur error' 21 | } 22 | } 23 | 24 | 25 | 26 | 27 | 28 | /** 29 | * Redux-DevTools 30 | * 31 | * define parameters of browser "Redux-DevTools" plugin in development mode 32 | * - chrome: https://chrome.google.com/webstore/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd?hl=en 33 | * - firefox: https://addons.mozilla.org/en-US/firefox/addon/reduxdevtools/ 34 | */ 35 | let composeEnhancer = compose; 36 | // 37 | if (JSON.parse(process.env.RSSR_REDUX_DEV_TOOLS) && IS_BROWSER) 38 | composeEnhancer = window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__() 39 | 40 | 41 | 42 | 43 | 44 | /** 45 | * create Redux Store 46 | * 47 | * create store with defaultState 48 | * in server rendering, we do not have to make the store only by defining default state. In case of any state changes, we can 49 | * replace defaultState with the changed state and rewrite them. 50 | * (exp: in home page we have a data fetch, so we replace it with defaultState. In this example, home state has real data 51 | * but other states have default data or remain empty 52 | * @param state : object of states with default value 53 | * @returns {any} : redux store object 54 | */ 55 | export const createStore = (state = {...defaultState}) => createStoreProvider(state, composeEnhancer); 56 | 57 | 58 | 59 | 60 | 61 | /** 62 | * create store by combining server feched data and default store states 63 | * @returns {any} : redux store object 64 | */ 65 | export const clientCreateStore = function () { 66 | let states; 67 | 68 | if (window.RSSR_UPDATED_REDUX_STATES !== undefined) { 69 | states = { 70 | ...defaultState, 71 | ...window.RSSR_UPDATED_REDUX_STATES 72 | }; 73 | delete window.RSSR_UPDATED_REDUX_STATES; 74 | } 75 | 76 | return createStore(states); 77 | } 78 | -------------------------------------------------------------------------------- /src/setup/style/public.scss: -------------------------------------------------------------------------------- 1 | @import "var"; 2 | 3 | #webpack-hot-middleware-clientOverlay { 4 | direction : ltr !important; 5 | } 6 | -------------------------------------------------------------------------------- /src/setup/style/var.scss: -------------------------------------------------------------------------------- 1 | // public variable and structure 2 | // definded in includePaths property of sass-loader in webpack. 3 | // can access to it with: 4 | // @import "var"; 5 | 6 | //----- custom ----- 7 | // path of font directory 8 | $path-font : "/asset/font/"; 9 | 10 | -------------------------------------------------------------------------------- /src/setup/utility/badConnectionAlert.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {toast} from "react-toastify"; 3 | 4 | 5 | 6 | export const badConnectionAlert = function (whereOf) { 7 | 8 | const message = ( 9 |
10 |
11 | {whereOf} Error 12 |
13 | Server Error, try again or tell to support! 14 |
15 | ) 16 | 17 | toast.error(message, {autoClose: false}); 18 | } -------------------------------------------------------------------------------- /src/setup/utility/convertErrorToResponse.js: -------------------------------------------------------------------------------- 1 | import {errorLogger} from "./errorLogger"; 2 | 3 | /** 4 | * convert axios error object to valid data object for SSR 5 | * see fetcher/clientFetcher and server/fetchProvider 6 | * 7 | * @param error {object} 8 | * @returns {{data: {code: *, error: boolean}, status: null}} 9 | */ 10 | export const convertErrorToResponse = function (error, req) { 11 | let response = { 12 | status: null, 13 | data: { 14 | isErrorData: true, 15 | code: error.code 16 | } 17 | }; 18 | 19 | if (error.response) { 20 | response.status = error.response.status; 21 | response.data.data = error.response.data; 22 | } 23 | // handel request time out error 24 | else if (error.code === 'ECONNABORTED') { 25 | response.status = 504; 26 | response.data.data = 'Time Out!\n' + error.message; 27 | } 28 | // handel internet not found error 29 | else if (error.code === 'ENOTFOUND') { 30 | response.status = 502; 31 | response.data.data = 'ERROR) your network connection is faulty or the hostname is just invalid or your DNS server is faulty or the DNS server that handles "mysite.com" is faulty.\n' + error.message; 32 | } 33 | 34 | if (response.status !== null) { 35 | // none-200 status (3**, 4**, 5**), request timeout and internet not found 36 | response.data.status = response.status; 37 | 38 | if (response.status === 504 || response.status === 502) 39 | errorLogger('FETCH >', error, false, req); 40 | 41 | return response; 42 | } else { 43 | // internal errors (like semantic errors) and other request errors (with out timeout) 44 | throw error; 45 | } 46 | } -------------------------------------------------------------------------------- /src/setup/utility/errorLogger.js: -------------------------------------------------------------------------------- 1 | /** 2 | * log axios errors to console 3 | */ 4 | export const errorLogger = (title, error, ignoreMessage, req) => { 5 | // get uesr IP if exists 6 | let ip = '', url = ''; 7 | if (req !== undefined) { 8 | url = req.originalUrl; 9 | 10 | ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress; 11 | if (typeof ip === 'string') 12 | ip = ip.split(',')[0]; 13 | } 14 | 15 | let errorMessage, type; 16 | if (error.response) { 17 | // errorMessage = error.response.data; // commented for clean console 18 | errorMessage = '' 19 | type = "RES";// response error - server finds error and returns a status like 402 to front 20 | } else if (error.request) { 21 | errorMessage = error.message; 22 | type = "REQ"; // request error - like error 500 or request timeouted 23 | } else if (error.message) { 24 | errorMessage = error.stack || error.message || JSON.stringify(error).slice(0, 600); 25 | type = "STP"; // setup error - have error in code like one variable is undefined 26 | } else { 27 | errorMessage = JSON.stringify(error).slice(0, 600); 28 | type = "PUB"; // public error - other errors 29 | } 30 | 31 | console.error(`${title} ${type} ${ip} ${url}`); 32 | 33 | if (errorMessage && !ignoreMessage) 34 | console.error('\t' + errorMessage); 35 | } 36 | -------------------------------------------------------------------------------- /src/setup/utility/fetching.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | // extended verion of axios (contains cancel token) 4 | export const fetching = function (options, thenHandler, catchHandler) { 5 | const {token, cancel} = axios.CancelToken.source(); 6 | options.cancelToken = token; 7 | // 8 | let request = axios(options) 9 | if (thenHandler) 10 | request = request.then(thenHandler) 11 | if (catchHandler) 12 | request = request.catch(catchHandler) 13 | // 14 | request.cancel = cancel; 15 | return request; 16 | } 17 | -------------------------------------------------------------------------------- /src/setup/utility/isErrorData.js: -------------------------------------------------------------------------------- 1 | /** 2 | * check data type if it is error or not 3 | * compatible with convertErrorToResponse() 4 | * see fetcher/clientFetcher and server/fetchProvider 5 | * 6 | * @param data {*} 7 | * @returns {boolean|*} 8 | */ 9 | export const isErrorData = (data) => data && data.isErrorData 10 | -------------------------------------------------------------------------------- /src/setup/utility/isValidUser.js: -------------------------------------------------------------------------------- 1 | import {getStore} from "trim-redux"; 2 | import Cookies from "js-cookie"; 3 | 4 | 5 | /** 6 | * check if the user has logged-in before or not and token is valid or not 7 | * 8 | * @param updateIsRequired {boolean} : if it is set to false then user is valid just when has token 9 | * @returns {boolean} 10 | */ 11 | export const isValidUser = (updateIsRequired = true) => { 12 | const user = getStore('user'); 13 | 14 | if (user === undefined) { 15 | console.error('❗ user not exist in store!') 16 | return false; 17 | } 18 | 19 | if (!Cookies.get('token')) 20 | return false; 21 | 22 | if (updateIsRequired) 23 | return user.updated; 24 | else 25 | return true; 26 | } 27 | -------------------------------------------------------------------------------- /src/setup/utility/jumpScrollToTop.js: -------------------------------------------------------------------------------- 1 | // config 2 | import {IS_BROWSER, IS_SERVER} from "../constant"; 3 | 4 | 5 | 6 | /** 7 | * for resetting the scroll and getting to the top of page when redirected 8 | */ 9 | if (IS_BROWSER) 10 | window.jumpScrollToTop_offset = 0; 11 | // 12 | export const jumpScrollToTop = () => { 13 | if (IS_SERVER) 14 | return; 15 | 16 | // time out is for making sure if the new component script has loaded 17 | setTimeout(() => $('html,body').animate({scrollTop: window.jumpScrollToTop_offset}, 'fast'), 200); 18 | } 19 | -------------------------------------------------------------------------------- /src/setup/utility/random.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param range : number (Int) 3 | * @returns {number} : random number between(1 - range) 4 | */ 5 | export const random = (range) => { 6 | return Math.floor((Math.random() * range + 1)); 7 | } 8 | -------------------------------------------------------------------------------- /src/setup/utility/responseValidation.js: -------------------------------------------------------------------------------- 1 | /** 2 | * check if data contains valid data and status property 3 | * see fetcher/clientFetcher and server/fetchProvider 4 | * 5 | * @param response 6 | */ 7 | export const responseValidation = function (response) { 8 | // response not found 9 | if (typeof response === "undefined") 10 | throw new Error("⛔ invalid fetch() response. response is undefined. response must be an object with 'status' and 'data' property."); 11 | 12 | // data existence and status 13 | if (!response.hasOwnProperty('data') || !response.hasOwnProperty('status')) 14 | throw new Error('⛔ invalid fetch() response. check axios returns, "data" and "status" is required properties in response.'); 15 | 16 | // status data type 17 | if (typeof response.status !== 'number') 18 | throw new Error('⛔ invalid status data type of fetch() response. status data type must be number like 404. status is ' + response.status + ' with type ' + typeof response.status); 19 | 20 | // status valid range 21 | if (response.status < 100 || response.status >= 600) 22 | console.warn('📌 value of "status" is not in valid range (1** to 5**). status is ' + response.status) 23 | } 24 | -------------------------------------------------------------------------------- /src/setup/utility/samplejQueryPlugin.js: -------------------------------------------------------------------------------- 1 | /* 2 | // you can define jQuery plugin like this and use in project with out import 3 | 4 | $.fn.samplePlugin = function () { 5 | console.log('selected elements: ', $(this)); 6 | } 7 | */ 8 | --------------------------------------------------------------------------------