🔨 Add base projet
This commit is contained in:
parent
162a7c6d5b
commit
457ffc366f
94
.gitignore
vendored
94
.gitignore
vendored
@ -1,3 +1,5 @@
|
||||
# Created by .ignore support plugin (hsz.mobi)
|
||||
### Node template
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
@ -41,8 +43,8 @@ build/Release
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# TypeScript v1 declaration files
|
||||
typings/
|
||||
# Snowpack dependency directory (https://snowpack.dev/)
|
||||
web_modules/
|
||||
|
||||
# TypeScript cache
|
||||
*.tsbuildinfo
|
||||
@ -74,9 +76,11 @@ typings/
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
.parcel-cache
|
||||
|
||||
# Next.js build output
|
||||
.next
|
||||
out
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
.nuxt
|
||||
@ -84,7 +88,7 @@ dist
|
||||
|
||||
# Gatsby files
|
||||
.cache/
|
||||
# Comment in the public line in if your project uses Gatsby and *not* Next.js
|
||||
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||
# public
|
||||
|
||||
@ -102,3 +106,87 @@ dist
|
||||
|
||||
# TernJS port file
|
||||
.tern-port
|
||||
|
||||
# Stores VSCode versions used for testing VSCode extensions
|
||||
.vscode-test
|
||||
|
||||
# yarn v2
|
||||
.yarn/cache
|
||||
.yarn/unplugged
|
||||
.yarn/build-state.yml
|
||||
.yarn/install-state.gz
|
||||
.pnp.*
|
||||
|
||||
### JetBrains template
|
||||
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
|
||||
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
||||
|
||||
# User-specific stuff
|
||||
.idea/**/workspace.xml
|
||||
.idea/**/tasks.xml
|
||||
.idea/**/usage.statistics.xml
|
||||
.idea/**/dictionaries
|
||||
.idea/**/shelf
|
||||
|
||||
# Generated files
|
||||
.idea/**/contentModel.xml
|
||||
|
||||
# Sensitive or high-churn files
|
||||
.idea/**/dataSources/
|
||||
.idea/**/dataSources.ids
|
||||
.idea/**/dataSources.local.xml
|
||||
.idea/**/sqlDataSources.xml
|
||||
.idea/**/dynamic.xml
|
||||
.idea/**/uiDesigner.xml
|
||||
.idea/**/dbnavigator.xml
|
||||
|
||||
# Gradle
|
||||
.idea/**/gradle.xml
|
||||
.idea/**/libraries
|
||||
|
||||
# Gradle and Maven with auto-import
|
||||
# When using Gradle or Maven with auto-import, you should exclude module files,
|
||||
# since they will be recreated, and may cause churn. Uncomment if using
|
||||
# auto-import.
|
||||
# .idea/artifacts
|
||||
# .idea/compiler.xml
|
||||
# .idea/jarRepositories.xml
|
||||
# .idea/modules.xml
|
||||
# .idea/*.iml
|
||||
# .idea/modules
|
||||
# *.iml
|
||||
# *.ipr
|
||||
|
||||
# CMake
|
||||
cmake-build-*/
|
||||
|
||||
# Mongo Explorer plugin
|
||||
.idea/**/mongoSettings.xml
|
||||
|
||||
# File-based project format
|
||||
*.iws
|
||||
|
||||
# IntelliJ
|
||||
out/
|
||||
|
||||
# mpeltonen/sbt-idea plugin
|
||||
.idea_modules/
|
||||
|
||||
# JIRA plugin
|
||||
atlassian-ide-plugin.xml
|
||||
|
||||
# Cursive Clojure plugin
|
||||
.idea/replstate.xml
|
||||
|
||||
# Crashlytics plugin (for Android Studio and IntelliJ)
|
||||
com_crashlytics_export_strings.xml
|
||||
crashlytics.properties
|
||||
crashlytics-build.properties
|
||||
fabric.properties
|
||||
|
||||
# Editor-based Rest Client
|
||||
.idea/httpRequests
|
||||
|
||||
# Android studio 3.1+ serialized cache file
|
||||
.idea/caches/build_file_checksums.ser
|
||||
|
||||
|
5
.idea/.gitignore
vendored
Normal file
5
.idea/.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
12
.idea/cnc-speed-calculator.iml
Normal file
12
.idea/cnc-speed-calculator.iml
Normal file
@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="WEB_MODULE" version="4">
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<excludeFolder url="file://$MODULE_DIR$/temp" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/tmp" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
6
.idea/jsLibraryMappings.xml
Normal file
6
.idea/jsLibraryMappings.xml
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="JavaScriptLibraryMappings">
|
||||
<includedPredefinedLibrary name="Node.js Core" />
|
||||
</component>
|
||||
</project>
|
8
.idea/modules.xml
Normal file
8
.idea/modules.xml
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/cnc-speed-calculator.iml" filepath="$PROJECT_DIR$/.idea/cnc-speed-calculator.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
6
.idea/vcs.xml
Normal file
6
.idea/vcs.xml
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
25
.idea/watcherTasks.xml
Normal file
25
.idea/watcherTasks.xml
Normal file
@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectTasksOptions">
|
||||
<TaskOptions isEnabled="true">
|
||||
<option name="arguments" value="$FileName$:$FileNameWithoutExtension$.css" />
|
||||
<option name="checkSyntaxErrors" value="true" />
|
||||
<option name="description" />
|
||||
<option name="exitCodeBehavior" value="ERROR" />
|
||||
<option name="fileExtension" value="scss" />
|
||||
<option name="immediateSync" value="true" />
|
||||
<option name="name" value="SCSS" />
|
||||
<option name="output" value="$FileNameWithoutExtension$.css:$FileNameWithoutExtension$.css.map" />
|
||||
<option name="outputFilters">
|
||||
<array />
|
||||
</option>
|
||||
<option name="outputFromStdout" value="false" />
|
||||
<option name="program" value="sass" />
|
||||
<option name="runOnExternalChanges" value="true" />
|
||||
<option name="scopeName" value="Unnamed" />
|
||||
<option name="trackOnlyRoot" value="true" />
|
||||
<option name="workingDir" value="$FileDir$" />
|
||||
<envs />
|
||||
</TaskOptions>
|
||||
</component>
|
||||
</project>
|
75
app.js
Normal file
75
app.js
Normal file
@ -0,0 +1,75 @@
|
||||
const createError = require('http-errors');
|
||||
const express = require('express');
|
||||
const path = require('path');
|
||||
const cookieParser = require('cookie-parser');
|
||||
const logger = require('./config/winston');
|
||||
const sassMiddleware = require('node-sass-middleware');
|
||||
const expressWinston = require('express-winston');
|
||||
const i18n = require('i18n');
|
||||
|
||||
const indexRouter = require('./routes/index');
|
||||
|
||||
const app = express();
|
||||
|
||||
// view engine setup
|
||||
app.set('views', path.join(__dirname, 'views'));
|
||||
app.set('view engine', 'ejs');
|
||||
|
||||
i18n.configure({
|
||||
// setup some locales - other locales default to en silently
|
||||
locales: ['en', 'fr'],
|
||||
// where to store json files - defaults to './locales'
|
||||
directory: __dirname + '/locales'
|
||||
});
|
||||
|
||||
// Logger
|
||||
app.use(expressWinston.logger({
|
||||
winstonInstance: logger,
|
||||
meta: true, // optional: control whether you want to log the meta data about the request (default to true)
|
||||
expressFormat: true, // Use the default Express/morgan request formatting. Enabling this will override any msg if true. Will only output colors with colorize set to true
|
||||
colorize: true, // Color the text and status code, using the Express/morgan color palette (text: gray, status: default green, 3XX cyan, 4XX yellow, 5XX red).
|
||||
level: function (req, res) {
|
||||
if (res.statusCode < 500)
|
||||
return "debug";
|
||||
return "warn";
|
||||
|
||||
}
|
||||
}));
|
||||
|
||||
|
||||
app.use(express.json());
|
||||
app.use(express.urlencoded({ extended: false }));
|
||||
app.use(cookieParser());
|
||||
app.use(sassMiddleware({
|
||||
src: path.join(__dirname, 'public'),
|
||||
dest: path.join(__dirname, 'public'),
|
||||
indentedSyntax: true, // true = .sass and false = .scss
|
||||
sourceMap: true
|
||||
}));
|
||||
app.use(express.static(path.join(__dirname, 'public')));
|
||||
app.use(i18n.init)
|
||||
|
||||
app.use('/', indexRouter);
|
||||
|
||||
// Boootstrap JS Files
|
||||
app.use('/js/bootstrap.min.js', express.static(path.join(__dirname, '/node_modules/bootstrap/dist/js/bootstrap.min.js')))
|
||||
|
||||
// catch 404 and forward to error handler
|
||||
app.use(function (req, res, next) {
|
||||
next(createError(404));
|
||||
});
|
||||
|
||||
// error handler
|
||||
app.use(function (err, req, res, next) {
|
||||
// set locals, only providing error in development
|
||||
res.locals.message = err.message;
|
||||
res.locals.error = req.app.get('env') === 'development' ? err : {};
|
||||
|
||||
// render the error page
|
||||
res.status(err.status || 500);
|
||||
res.render('error');
|
||||
});
|
||||
|
||||
|
||||
|
||||
module.exports = app;
|
110
bin/www
Executable file
110
bin/www
Executable file
@ -0,0 +1,110 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Module dependencies.
|
||||
*/
|
||||
|
||||
const app = require('../app');
|
||||
const debug = require('debug')('cnc-speed-calculator:server');
|
||||
const http = require('http');
|
||||
const sequelize = require("../sequelize");
|
||||
const init_db = require("../sequelize/init-db")
|
||||
const logger = require("../config/winston")
|
||||
|
||||
/**
|
||||
* Get port from environment and store in Express.
|
||||
*/
|
||||
|
||||
const port = normalizePort(process.env.PORT || '3000');
|
||||
app.set('port', port);
|
||||
|
||||
/**
|
||||
* Create HTTP server.
|
||||
*/
|
||||
|
||||
const server = http.createServer(app);
|
||||
|
||||
|
||||
/**
|
||||
* Normalize a port into a number, string, or false.
|
||||
*/
|
||||
|
||||
function normalizePort(val) {
|
||||
let port = parseInt(val, 10);
|
||||
|
||||
if (isNaN(port)) {
|
||||
// named pipe
|
||||
return val;
|
||||
}
|
||||
|
||||
if (port >= 0) {
|
||||
// port number
|
||||
return port;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Event listener for HTTP server "error" event.
|
||||
*/
|
||||
|
||||
function onError(error) {
|
||||
if (error.syscall !== 'listen') {
|
||||
throw error;
|
||||
}
|
||||
|
||||
let bind = typeof port === 'string'
|
||||
? 'Pipe ' + port
|
||||
: 'Port ' + port;
|
||||
|
||||
// handle specific listen errors with friendly messages
|
||||
switch (error.code) {
|
||||
case 'EACCES':
|
||||
console.error(bind + ' requires elevated privileges');
|
||||
process.exit(1);
|
||||
break;
|
||||
case 'EADDRINUSE':
|
||||
console.error(bind + ' is already in use');
|
||||
process.exit(1);
|
||||
break;
|
||||
default:
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Event listener for HTTP server "listening" event.
|
||||
*/
|
||||
|
||||
function onListening() {
|
||||
let addr = server.address();
|
||||
let bind = typeof addr === 'string'
|
||||
? 'pipe ' + addr
|
||||
: 'port ' + addr.port;
|
||||
logger.info('Listening on ' + bind);
|
||||
}
|
||||
|
||||
async function init() {
|
||||
console.log(`Checking database connection...`);
|
||||
try {
|
||||
await sequelize.authenticate();
|
||||
logger.info('Database connection OK!');
|
||||
await init_db.check_database()
|
||||
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Unable to connect to the database:');
|
||||
logger.error(error);
|
||||
process.exit(1);
|
||||
}
|
||||
/**
|
||||
* Listen on provided port, on all network interfaces.
|
||||
*/
|
||||
server.listen(port);
|
||||
server.on('error', onError);
|
||||
server.on('listening', onListening);
|
||||
}
|
||||
|
||||
init();
|
||||
|
27
config/winston.js
Normal file
27
config/winston.js
Normal file
@ -0,0 +1,27 @@
|
||||
const winston = require("winston");
|
||||
|
||||
const logger = winston.createLogger({
|
||||
level: process.env.LOG_LEVEL || 'info',
|
||||
format: winston.format.combine(
|
||||
winston.format.timestamp({
|
||||
format: "YYYY-MM-DD HH:mm:ss",
|
||||
}),
|
||||
winston.format.errors({ stack: true }),
|
||||
// winston.format.splat(),
|
||||
winston.format.colorize(),
|
||||
winston.format.align(),
|
||||
winston.format.printf(({ level, message, timestamp }) => {
|
||||
return `[${timestamp}] [${level}]: ${message}`;
|
||||
})
|
||||
),
|
||||
transports: [
|
||||
//
|
||||
// - Write to all logs with level `info` and below to `quick-start-combined.log`.
|
||||
// - Write all logs error (and below) to `quick-start-error.log`.
|
||||
//
|
||||
new winston.transports.Console({ handleExceptions: true }),
|
||||
// new winston.transports.File({filename: '/data/NCB.log', handleExceptions: true})
|
||||
],
|
||||
});
|
||||
|
||||
module.exports = logger;
|
1
locales/en.json
Normal file
1
locales/en.json
Normal file
@ -0,0 +1 @@
|
||||
{}
|
19
locales/fr.json
Normal file
19
locales/fr.json
Normal file
@ -0,0 +1,19 @@
|
||||
{
|
||||
"Material": "Matière",
|
||||
"Step down factor": "Facteur de profondeur de passe",
|
||||
"Tool Diameter": "Tool Diameter",
|
||||
"Number of tooth": "Number of tooth",
|
||||
"Settings": "Settings",
|
||||
"Calculated Values": "Calculated Values",
|
||||
"Spindle Speed": "Spindle Speed",
|
||||
"Feed Rate": "Feed Rate",
|
||||
"Step Down": "Step Down",
|
||||
"Feed Rate (mm/sec": "Feed Rate (mm/sec",
|
||||
"Feed Rate (mm/sec)": "Feed Rate (mm/sec)",
|
||||
"Feed Rate (mm/min)": "Feed Rate (mm/min)",
|
||||
"Constants": "Constants",
|
||||
"Cutting Speed": "Cutting Speed",
|
||||
"Feed By Tooth": "Feed By Tooth",
|
||||
"Step Down factor": "Step Down factor",
|
||||
"Spindle Max Speed": "Spindle Max Speed"
|
||||
}
|
2414
package-lock.json
generated
Normal file
2414
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
23
package.json
Normal file
23
package.json
Normal file
@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "cnc-speed-calculator",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "node ./bin/www"
|
||||
},
|
||||
"dependencies": {
|
||||
"bootstrap": "^5.0.0-beta1",
|
||||
"cookie-parser": "~1.4.4",
|
||||
"debug": "~2.6.9",
|
||||
"ejs": "~2.6.1",
|
||||
"express": "~4.16.1",
|
||||
"express-winston": "^4.0.5",
|
||||
"http-errors": "~1.6.3",
|
||||
"i18n": "^0.13.2",
|
||||
"morgan": "~1.9.1",
|
||||
"node-sass-middleware": "0.11.0",
|
||||
"sequelize": "^6.5.0",
|
||||
"sqlite3": "^5.0.1",
|
||||
"winston": "^3.3.3"
|
||||
}
|
||||
}
|
37
public/css/bootstrap_imports.scss
vendored
Normal file
37
public/css/bootstrap_imports.scss
vendored
Normal file
@ -0,0 +1,37 @@
|
||||
// Layout & components
|
||||
@import "../../node_modules/bootstrap/scss/root";
|
||||
@import "../../node_modules/bootstrap/scss/reboot";
|
||||
@import "../../node_modules/bootstrap/scss/type";
|
||||
@import "../../node_modules/bootstrap/scss/images";
|
||||
@import "../../node_modules/bootstrap/scss/containers";
|
||||
@import "../../node_modules/bootstrap/scss/grid";
|
||||
//@import "tables";
|
||||
@import "../../node_modules/bootstrap/scss/forms";
|
||||
@import "../../node_modules/bootstrap/scss/buttons";
|
||||
@import "../../node_modules/bootstrap/scss/transitions";
|
||||
@import "../../node_modules/bootstrap/scss/dropdown";
|
||||
//@import "button-group";
|
||||
//@import "nav";
|
||||
@import "../../node_modules/bootstrap/scss/navbar";
|
||||
@import "../../node_modules/bootstrap/scss/card";
|
||||
//@import "accordion";
|
||||
//@import "breadcrumb";
|
||||
//@import "pagination";
|
||||
@import "../../node_modules/bootstrap/scss/badge";
|
||||
@import "../../node_modules/bootstrap/scss/alert";
|
||||
@import "../../node_modules/bootstrap/scss/progress";
|
||||
@import "../../node_modules/bootstrap/scss/list-group";
|
||||
@import "../../node_modules/bootstrap/scss/close";
|
||||
@import "../../node_modules/bootstrap/scss/toasts";
|
||||
@import "../../node_modules/bootstrap/scss/modal";
|
||||
//@import "tooltip";
|
||||
//@import "popover";
|
||||
//@import "carousel";
|
||||
@import "../../node_modules/bootstrap/scss/spinners";
|
||||
|
||||
// Helpers
|
||||
@import "../../node_modules/bootstrap/scss/helpers";
|
||||
|
||||
// Utilities
|
||||
@import "../../node_modules/bootstrap/scss/utilities/api";
|
||||
// scss-docs-end import-stack
|
9731
public/css/custom_bootstrap.css
Normal file
9731
public/css/custom_bootstrap.css
Normal file
File diff suppressed because it is too large
Load Diff
1
public/css/custom_bootstrap.css.map
Normal file
1
public/css/custom_bootstrap.css.map
Normal file
File diff suppressed because one or more lines are too long
71
public/css/custom_bootstrap.scss
Normal file
71
public/css/custom_bootstrap.scss
Normal file
@ -0,0 +1,71 @@
|
||||
$body-bg: #222222;
|
||||
$dark: #292929;
|
||||
$secondary: #343a40;
|
||||
$accent: #b58e51;
|
||||
|
||||
$disabled: #455a64;
|
||||
|
||||
$custom-colors:(
|
||||
"accent": $accent
|
||||
);
|
||||
|
||||
|
||||
|
||||
$enable-shadows: true;
|
||||
$btn-box-shadow: none;
|
||||
|
||||
$component-active-bg: $accent;
|
||||
|
||||
$input-color: $accent;
|
||||
$input-bg: $secondary;
|
||||
$input-border-color: $secondary;
|
||||
$input-group-addon-bg: $disabled;
|
||||
//$input-group-addon-color: $secondary;
|
||||
$input-group-addon-border-color: $disabled;
|
||||
|
||||
|
||||
$form-select-indicator-color: $accent;
|
||||
|
||||
$form-switch-color: $accent;
|
||||
$form-switch-bg-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'><circle r='3' fill='#{$form-switch-color}'/></svg>");
|
||||
$form-check-input-border: 1px solid $accent !default;
|
||||
|
||||
$list-group-action-color: $accent;
|
||||
$list-group-bg: $secondary;
|
||||
$list-group-hover-bg: $secondary;
|
||||
$list-group-action-hover-color: #adb5bd;
|
||||
|
||||
$alert-bg-scale: 0%;
|
||||
$alert-border-scale: -10%;
|
||||
$alert-color-scale: -100%;
|
||||
|
||||
// Configuration
|
||||
@import "../../node_modules/bootstrap/scss/functions";
|
||||
@import "../../node_modules/bootstrap/scss/variables";
|
||||
|
||||
$theme-colors: map-merge($theme-colors, $custom-colors);
|
||||
|
||||
@import "../../node_modules/bootstrap/scss/mixins";
|
||||
@import "../../node_modules/bootstrap/scss/utilities";
|
||||
|
||||
//All other bootstrap imports
|
||||
@import "bootstrap_imports.scss";
|
||||
|
||||
.modal-dialog-scrollable .modal-body {
|
||||
&::-webkit-scrollbar-track {
|
||||
-webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
|
||||
border-radius: 10px;
|
||||
background-color: $secondary;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 12px;
|
||||
background-color: $dark;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
border-radius: 10px;
|
||||
-webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, .3);
|
||||
background-color: $accent;
|
||||
}
|
||||
}
|
5
public/css/style.css
Normal file
5
public/css/style.css
Normal file
@ -0,0 +1,5 @@
|
||||
.navbar {
|
||||
background-color: #0091ea;
|
||||
}
|
||||
|
||||
/*# sourceMappingURL=style.css.map */
|
1
public/css/style.css.map
Normal file
1
public/css/style.css.map
Normal file
@ -0,0 +1 @@
|
||||
{"version":3,"sourceRoot":"","sources":["style.scss"],"names":[],"mappings":"AAAA;EACE","file":"style.css"}
|
3
public/css/style.scss
Normal file
3
public/css/style.scss
Normal file
@ -0,0 +1,3 @@
|
||||
.navbar {
|
||||
background-color: #0091ea;
|
||||
}
|
15
routes/index.js
Normal file
15
routes/index.js
Normal file
@ -0,0 +1,15 @@
|
||||
var express = require('express');
|
||||
var router = express.Router();
|
||||
const sequelize = require('../sequelize')
|
||||
|
||||
/* GET home page. */
|
||||
router.get('/', function(req, res, next) {
|
||||
sequelize.models.preset_cut.findAll({order: ['name']}).then((preset_cut)=>{
|
||||
sequelize.models.preset_step_down_factor.findAll({order: ['name']}).then((step_down_factor)=>{
|
||||
res.render('index', { preset_cut: preset_cut, step_down_factor: step_down_factor });
|
||||
})
|
||||
})
|
||||
|
||||
});
|
||||
|
||||
module.exports = router;
|
21
sequelize/index.js
Normal file
21
sequelize/index.js
Normal file
@ -0,0 +1,21 @@
|
||||
const { Sequelize } = require('sequelize');
|
||||
const logger = require('../config/winston')
|
||||
const sequelize = new Sequelize({
|
||||
dialect: 'sqlite',
|
||||
storage: 'db.sqlite',
|
||||
logQueryParameters: true,
|
||||
benchmark: true,
|
||||
logging: msg => logger.debug.bind(msg)
|
||||
});
|
||||
|
||||
const modelDefiners = [
|
||||
require('./models/preset-cut.model'),
|
||||
require('./models/preset-step-down-factor.model'),
|
||||
require('./models/need-init.model')
|
||||
];
|
||||
|
||||
for (const modelDefiner of modelDefiners) {
|
||||
modelDefiner(sequelize);
|
||||
}
|
||||
|
||||
module.exports = sequelize;
|
121
sequelize/init-db.js
Normal file
121
sequelize/init-db.js
Normal file
@ -0,0 +1,121 @@
|
||||
const logger = require('../config/winston')
|
||||
const sequelize = require('./index')
|
||||
|
||||
async function reset() {
|
||||
logger.warn('Reset database...');
|
||||
await sequelize.sync({ force: true });
|
||||
logger.info('...Done');
|
||||
logger.info('Populating with default value...');
|
||||
await sequelize.models.preset_cut.bulkCreate([
|
||||
{
|
||||
name: "Wood, Plywood",
|
||||
cut_speed: 500,
|
||||
feed_by_tooth_more_1: 0.025,
|
||||
feed_by_tooth_more_2: 0.03,
|
||||
feed_by_tooth_more_3: 0.035,
|
||||
feed_by_tooth_more_4: 0.06,
|
||||
feed_by_tooth_more_5: 0.07,
|
||||
feed_by_tooth_more_6: 0.09,
|
||||
feed_by_tooth_more_8: 0.1,
|
||||
},
|
||||
{
|
||||
name: "Hard Wood",
|
||||
cut_speed: 450,
|
||||
feed_by_tooth_more_1: 0.02,
|
||||
feed_by_tooth_more_2: 0.025,
|
||||
feed_by_tooth_more_3: 0.030,
|
||||
feed_by_tooth_more_4: 0.055,
|
||||
feed_by_tooth_more_5: 0.065,
|
||||
feed_by_tooth_more_6: 0.085,
|
||||
feed_by_tooth_more_8: 0.095,
|
||||
},
|
||||
{
|
||||
name: "MDF",
|
||||
cut_speed: 450,
|
||||
feed_by_tooth_more_1: 0.050,
|
||||
feed_by_tooth_more_2: 0.070,
|
||||
feed_by_tooth_more_3: 0.100,
|
||||
feed_by_tooth_more_4: 0.150,
|
||||
feed_by_tooth_more_5: 0.200,
|
||||
feed_by_tooth_more_6: 0.300,
|
||||
feed_by_tooth_more_8: 0.400,
|
||||
},
|
||||
{
|
||||
name: "Expanded PVC",
|
||||
cut_speed: 300,
|
||||
feed_by_tooth_more_1: 0.040,
|
||||
feed_by_tooth_more_2: 0.060,
|
||||
feed_by_tooth_more_3: 0.150,
|
||||
feed_by_tooth_more_4: 0.200,
|
||||
feed_by_tooth_more_5: 0.250,
|
||||
feed_by_tooth_more_6: 0.350,
|
||||
feed_by_tooth_more_8: 0.400,
|
||||
},
|
||||
{
|
||||
name: "PMMA, PC, POM, ...",
|
||||
cut_speed: 250,
|
||||
feed_by_tooth_more_1: 0.015,
|
||||
feed_by_tooth_more_2: 0.020,
|
||||
feed_by_tooth_more_3: 0.025,
|
||||
feed_by_tooth_more_4: 0.050,
|
||||
feed_by_tooth_more_5: 0.060,
|
||||
feed_by_tooth_more_6: 0.080,
|
||||
feed_by_tooth_more_8: 0.090,
|
||||
},
|
||||
{
|
||||
name: "Aluminum (2017A, 5083, ...) ",
|
||||
cut_speed: 125,
|
||||
feed_by_tooth_more_1: 0.010,
|
||||
feed_by_tooth_more_2: 0.010,
|
||||
feed_by_tooth_more_3: 0.010,
|
||||
feed_by_tooth_more_4: 0.015,
|
||||
feed_by_tooth_more_5: 0.015,
|
||||
feed_by_tooth_more_6: 0.020,
|
||||
feed_by_tooth_more_8: 0.030,
|
||||
}
|
||||
]);
|
||||
await sequelize.models.preset_step_down_factor.bulkCreate([
|
||||
{
|
||||
name: "Soft",
|
||||
k_less_2: 0.5,
|
||||
k_more_2: 0.5,
|
||||
k_more_3: 0.8,
|
||||
k_more_4: 1,
|
||||
k_more_5: 1,
|
||||
k_more_6: 1
|
||||
},
|
||||
{
|
||||
name: "Hard (Nonferrous) ",
|
||||
k_less_2: 0.2,
|
||||
k_more_2: 0.2,
|
||||
k_more_3: 0.4,
|
||||
k_more_4: 0.5,
|
||||
k_more_5: 0.6,
|
||||
k_more_6: 1
|
||||
},
|
||||
{
|
||||
name: "Aluminium",
|
||||
k_less_2: 0.1,
|
||||
k_more_2: 0.1,
|
||||
k_more_3: 0.2,
|
||||
k_more_4: 0.25,
|
||||
k_more_5: 0.35,
|
||||
k_more_6: 0.35
|
||||
},
|
||||
])
|
||||
await sequelize.models.need_init.create({name: 'init'})
|
||||
logger.info('...Done')
|
||||
}
|
||||
|
||||
async function check_database() {
|
||||
await sequelize.sync();
|
||||
let val = await sequelize.models.need_init.findAll({where: {name: 'init'}});
|
||||
if(val.length === 0){
|
||||
logger.info('Need init !');
|
||||
await reset();
|
||||
logger.info('Done')
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
exports.check_database = check_database;
|
16
sequelize/models/need-init.model.js
Normal file
16
sequelize/models/need-init.model.js
Normal file
@ -0,0 +1,16 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
|
||||
module.exports = (sequelize) => {
|
||||
sequelize.define('need_init', {
|
||||
id: {
|
||||
allowNull: false,
|
||||
autoIncrement: true,
|
||||
primaryKey: true,
|
||||
type: DataTypes.INTEGER
|
||||
},
|
||||
name: {
|
||||
allowNull: false,
|
||||
type: DataTypes.STRING
|
||||
}
|
||||
})
|
||||
}
|
48
sequelize/models/preset-cut.model.js
Normal file
48
sequelize/models/preset-cut.model.js
Normal file
@ -0,0 +1,48 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
|
||||
module.exports = (sequelize) => {
|
||||
sequelize.define('preset_cut', {
|
||||
id: {
|
||||
allowNull: false,
|
||||
autoIncrement: true,
|
||||
primaryKey: true,
|
||||
type: DataTypes.INTEGER
|
||||
},
|
||||
name: {
|
||||
allowNull: false,
|
||||
type: DataTypes.STRING
|
||||
},
|
||||
cut_speed: {
|
||||
allowNull: false,
|
||||
type: DataTypes.FLOAT,
|
||||
},
|
||||
feed_by_tooth_more_1: {
|
||||
allowNull: false,
|
||||
type: DataTypes.FLOAT,
|
||||
},
|
||||
feed_by_tooth_more_2: {
|
||||
allowNull: false,
|
||||
type: DataTypes.FLOAT,
|
||||
},
|
||||
feed_by_tooth_more_3: {
|
||||
allowNull: false,
|
||||
type: DataTypes.FLOAT,
|
||||
},
|
||||
feed_by_tooth_more_4: {
|
||||
allowNull: false,
|
||||
type: DataTypes.FLOAT,
|
||||
},
|
||||
feed_by_tooth_more_5: {
|
||||
allowNull: false,
|
||||
type: DataTypes.FLOAT,
|
||||
},
|
||||
feed_by_tooth_more_6: {
|
||||
allowNull: false,
|
||||
type: DataTypes.FLOAT,
|
||||
},
|
||||
feed_by_tooth_more_8: {
|
||||
allowNull: false,
|
||||
type: DataTypes.FLOAT,
|
||||
},
|
||||
})
|
||||
}
|
40
sequelize/models/preset-step-down-factor.model.js
Normal file
40
sequelize/models/preset-step-down-factor.model.js
Normal file
@ -0,0 +1,40 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
|
||||
module.exports = (sequelize) =>{
|
||||
sequelize.define('preset_step_down_factor',{
|
||||
id: {
|
||||
allowNull: false,
|
||||
autoIncrement: true,
|
||||
primaryKey: true,
|
||||
type: DataTypes.INTEGER
|
||||
},
|
||||
name: {
|
||||
allowNull: false,
|
||||
type: DataTypes.STRING
|
||||
},
|
||||
k_less_2: {
|
||||
allowNull: false,
|
||||
type: DataTypes.FLOAT,
|
||||
},
|
||||
k_more_2: {
|
||||
allowNull: false,
|
||||
type: DataTypes.FLOAT,
|
||||
},
|
||||
k_more_3: {
|
||||
allowNull: false,
|
||||
type: DataTypes.FLOAT,
|
||||
},
|
||||
k_more_4: {
|
||||
allowNull: false,
|
||||
type: DataTypes.FLOAT,
|
||||
},
|
||||
k_more_5: {
|
||||
allowNull: false,
|
||||
type: DataTypes.FLOAT,
|
||||
},
|
||||
k_more_6: {
|
||||
allowNull: false,
|
||||
type: DataTypes.FLOAT,
|
||||
}
|
||||
})
|
||||
}
|
45
views/calculated.ejs
Normal file
45
views/calculated.ejs
Normal file
@ -0,0 +1,45 @@
|
||||
<div class="card bg-dark shadow-sm border-secondary">
|
||||
<div class="card-header border-secondary border-bottom h3 text-center">
|
||||
<%= __('Calculated Values') %>
|
||||
</div>
|
||||
<div class="card-body ">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-12 col-md-5">
|
||||
<label for="rpm" class="form-label"><%= __('Spindle Speed') %></label>
|
||||
<div class="input-group">
|
||||
<input type="number" min="0" step="0.1" class="form-control bg-secondary" id="rpm" disabled
|
||||
value="0">
|
||||
<span class="input-group-text">RPM</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-md-5 mt-3 mt-md-0">
|
||||
<label for="feed_rate" class="form-label"><%= __('Feed Rate (mm/min)') %></label>
|
||||
<div class="input-group">
|
||||
<input type="number" min="0" step="0.1" class="form-control bg-secondary" id="feed_rate" disabled
|
||||
value="0">
|
||||
<span class="input-group-text">mm/min</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt-3 justify-content-center">
|
||||
<div class="col-12 col-md-5">
|
||||
<label for="step_down" class="form-label"><%= __('Step Down') %></label>
|
||||
<div class="input-group">
|
||||
<input type="number" min="0" step="0.1" class="form-control bg-secondary" id="step_down" disabled
|
||||
value="0">
|
||||
<span class="input-group-text">mm</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-md-5 mt-3 mt-md-0">
|
||||
<label for="feed_rate_sec" class="form-label"><%= __('Feed Rate (mm/sec)') %></label>
|
||||
<div class="input-group">
|
||||
<input type="number" min="0" step="0.1" class="form-control bg-secondary" id="feed_rate_sec"
|
||||
disabled
|
||||
value="0">
|
||||
<span class="input-group-text">mm/sec</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
33
views/constants.ejs
Normal file
33
views/constants.ejs
Normal file
@ -0,0 +1,33 @@
|
||||
<div class="card bg-dark shadow-sm border-secondary">
|
||||
<div class="card-header border-secondary border-bottom h3 text-center">
|
||||
<%= __('Constants') %>
|
||||
</div>
|
||||
<div class="card-body ">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-12 col-md-5">
|
||||
<label for="cutting_speed" class="form-label"><%= __('Cutting Speed') %></label>
|
||||
<div class="input-group">
|
||||
<input type="number" class="form-control bg-secondary" id="cutting_speed"
|
||||
disabled value="0">
|
||||
<span class="input-group-text">m/min</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-md-5 mt-3 mt-md-0">
|
||||
<label for="feed_tooth" class="form-label"><%= __('Feed By Tooth') %></label>
|
||||
<div class="input-group">
|
||||
<input type="number" class="form-control bg-secondary" id="feed_tooth" disabled
|
||||
value="0">
|
||||
<span class="input-group-text">mm</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt-3 justify-content-center">
|
||||
<div class="col-12 col-md-5">
|
||||
<label for="step_down_factor" class="form-label"><%= __('Step Down factor') %></label>
|
||||
<input type="number" min="0" step="0.1" class="form-control bg-secondary" id="step_down_factor" disabled
|
||||
value="0">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
3
views/error.ejs
Normal file
3
views/error.ejs
Normal file
@ -0,0 +1,3 @@
|
||||
<h1><%= message %></h1>
|
||||
<h2><%= error.status %></h2>
|
||||
<pre><%= error.stack %></pre>
|
159
views/index.ejs
Normal file
159
views/index.ejs
Normal file
@ -0,0 +1,159 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>CNC Speed Calculator</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel='stylesheet' href='/css/style.css'/>
|
||||
<link rel="stylesheet" href="/css/custom_bootstrap.css">
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar navbar-dark navbar-expand">
|
||||
<div class="container">
|
||||
<a href="#" class="navbar-brand">
|
||||
CNC Speed Calculator
|
||||
</a>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="container mt-4 text-white">
|
||||
<div class="row">
|
||||
<div class="col-12 col-lg-8 offset-lg-2">
|
||||
<%- include('settings'); -%>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-4 mt-lg-2">
|
||||
<div class="col-12 col-lg-6 mt-3">
|
||||
<%- include('calculated'); -%>
|
||||
</div>
|
||||
<div class="col-12 col-lg-6 mt-3">
|
||||
<%- include('constants'); -%>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</body>
|
||||
<script src="/js/bootstrap.min.js"></script>
|
||||
<script>
|
||||
let preset_cut = '<%- JSON.stringify(preset_cut) %>';
|
||||
let preset_step_down_factor = '<%- JSON.stringify(step_down_factor) %>';
|
||||
preset_cut = JSON.parse(preset_cut);
|
||||
preset_step_down_factor = JSON.parse(preset_step_down_factor);
|
||||
|
||||
let preset_cut_elem = document.querySelector('#preset_cut');
|
||||
let preset_step_down_factor_elem = document.querySelector('#preset_step_down_factor');
|
||||
let tooth_nbr_elem = document.querySelector('#tooth_nbr');
|
||||
let tool_diameter_elem = document.querySelector('#tool_diameter');
|
||||
let max_rpm_elem = document.querySelector('#max_rpm')
|
||||
|
||||
let rpm_elem = document.querySelector('#rpm');
|
||||
let feed_rate_elem = document.querySelector('#feed_rate');
|
||||
let feed_rate_sec_elem = document.querySelector('#feed_rate_sec');
|
||||
let step_down_elem = document.querySelector('#step_down');
|
||||
|
||||
let cutting_speed_elem = document.querySelector('#cutting_speed');
|
||||
let feed_tooth_elem = document.querySelector('#feed_tooth');
|
||||
let step_down_factor_elem = document.querySelector('#step_down_factor');
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function (event) {
|
||||
load();
|
||||
onChange();
|
||||
});
|
||||
|
||||
|
||||
function onChange() {
|
||||
if (preset_cut_elem.value === "" || preset_step_down_factor_elem.value === "" || tool_diameter_elem.value === "0" || tooth_nbr_elem.value === "0" || max_rpm_elem.value === "0") {
|
||||
return;
|
||||
}
|
||||
save();
|
||||
let new_preset_cut = getPresetValue(preset_cut, parseInt(preset_cut_elem.value, 10));
|
||||
let new_preset_steep_down = getPresetValue(preset_step_down_factor, parseInt(preset_step_down_factor_elem.value, 10))
|
||||
let new_tool_diameter = parseFloat(tool_diameter_elem.value);
|
||||
let new_tooth_nbr = parseInt(tooth_nbr_elem.value);
|
||||
let max_rpm = parseInt(max_rpm_elem.value);
|
||||
|
||||
let rpm = (1000 * new_preset_cut.cut_speed) / (3.14 * new_tool_diameter);
|
||||
if (rpm > max_rpm)
|
||||
rpm = max_rpm;
|
||||
let feed_rate = rpm * getFeedByTooth(new_tool_diameter, new_preset_cut) * new_tooth_nbr;
|
||||
let feed_rate_sec = feed_rate / 60;
|
||||
let step_down = new_tool_diameter * getStepDownFactor(new_tool_diameter, new_preset_steep_down);
|
||||
|
||||
rpm = Math.round(rpm);
|
||||
feed_rate = Math.round(feed_rate);
|
||||
feed_rate_sec = feed_rate_sec.toFixed(2)
|
||||
step_down = step_down.toFixed(2);
|
||||
|
||||
rpm_elem.value = rpm;
|
||||
feed_rate_elem.value = feed_rate;
|
||||
feed_rate_sec_elem.value = feed_rate_sec;
|
||||
step_down_elem.value = step_down;
|
||||
|
||||
cutting_speed_elem.value = new_preset_cut.cut_speed;
|
||||
feed_tooth_elem.value = getFeedByTooth(new_tool_diameter, new_preset_cut);
|
||||
step_down_factor_elem.value = getStepDownFactor(new_tool_diameter, new_preset_steep_down);
|
||||
}
|
||||
|
||||
function getPresetValue(list, id) {
|
||||
for (let elem of list) {
|
||||
if (elem.id === id) {
|
||||
return elem;
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function getFeedByTooth(tool_diam, preset) {
|
||||
if (tool_diam >= 8)
|
||||
return preset.feed_by_tooth_more_8;
|
||||
if (tool_diam >= 6)
|
||||
return preset.feed_by_tooth_more_6;
|
||||
if (tool_diam >= 5)
|
||||
return preset.feed_by_tooth_more_5;
|
||||
if (tool_diam >= 4)
|
||||
return preset.feed_by_tooth_more_4;
|
||||
if (tool_diam >= 3)
|
||||
return preset.feed_by_tooth_more_3;
|
||||
if (tool_diam >= 2)
|
||||
return preset.feed_by_tooth_more_2;
|
||||
return preset.feed_by_tooth_more_1;
|
||||
}
|
||||
|
||||
function getStepDownFactor(tool_diam, preset) {
|
||||
if (tool_diam >= 6)
|
||||
return preset.k_more_6;
|
||||
if (tool_diam >= 5)
|
||||
return preset.k_more_5;
|
||||
if (tool_diam >= 4)
|
||||
return preset.k_more_4;
|
||||
if (tool_diam >= 3)
|
||||
return preset.k_more_3;
|
||||
if (tool_diam >= 2)
|
||||
return preset.k_more_2;
|
||||
return preset.k_less_2;
|
||||
}
|
||||
|
||||
function save(){
|
||||
let values = {
|
||||
preset_cut: preset_cut_elem.value,
|
||||
preset_step_down_factor: preset_step_down_factor_elem.value,
|
||||
tool_diameter: tool_diameter_elem.value,
|
||||
tooth_nbr: tooth_nbr_elem.value,
|
||||
max_rpm: max_rpm_elem.value
|
||||
}
|
||||
localStorage.setItem('previous_data', JSON.stringify(values));
|
||||
}
|
||||
|
||||
function load(){
|
||||
let previous_data = localStorage.getItem('previous_data')
|
||||
if(previous_data === null)
|
||||
return;
|
||||
previous_data = JSON.parse(previous_data);
|
||||
preset_cut_elem.value = previous_data.preset_cut;
|
||||
preset_step_down_factor_elem.value = previous_data.preset_step_down_factor;
|
||||
tool_diameter_elem.value = previous_data.tool_diameter;
|
||||
tooth_nbr_elem.value = previous_data.tooth_nbr;
|
||||
max_rpm_elem.value = previous_data.max_rpm;
|
||||
}
|
||||
|
||||
</script>
|
||||
</html>
|
49
views/settings.ejs
Normal file
49
views/settings.ejs
Normal file
@ -0,0 +1,49 @@
|
||||
<div class="card bg-dark shadow-sm border-secondary">
|
||||
<div class="card-header border-secondary border-bottom h3 text-center">
|
||||
<%= __('Settings') %>
|
||||
</div>
|
||||
<div class="card-body ">
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-6">
|
||||
<label for="preset_cut" class="form-label"><%= __("Material") %></label>
|
||||
<select class="form-select border-accent" id="preset_cut" onchange="onChange()">
|
||||
<% preset_cut.forEach(function (preset){ %>
|
||||
<option value="<%= preset.id %>"><%= preset.name %></option>
|
||||
<% }) %>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-12 col-md-6 mt-md-0 mt-3">
|
||||
<label for="preset_step_down_factor" class="form-label"><%= __('Step down factor') %></label>
|
||||
<select class="form-select border-accent" id="preset_step_down_factor" onchange="onChange()">
|
||||
<% step_down_factor.forEach(function (preset){ %>
|
||||
<option value="<%= preset.id %>"><%= preset.name %></option>
|
||||
<% }) %>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt-3 justify-content-center">
|
||||
<div class="col-6 col-md-3">
|
||||
<label for="tooth_nbr" class="form-label"><%= __('Number of tooth') %></label>
|
||||
<input type="number" min="1" step="1" class="form-control border-accent" id="tooth_nbr" onchange="onChange()"
|
||||
onkeyup="onChange()" value="1">
|
||||
|
||||
</div>
|
||||
<div class="col-6 col-md-3">
|
||||
<label for="tool_diameter" class="form-label"><%= __('Tool Diameter') %></label>
|
||||
<div class="input-group">
|
||||
<input type="number" min="1" step="0.1" class="form-control border-accent border-end-0" id="tool_diameter" onkeyup="onChange()"
|
||||
onchange="onChange()" value="3.0">
|
||||
<span class="input-group-text border-accent">mm</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-md-3 mt-3 mt-md-0">
|
||||
<label for="max_rpm" class="form-label"><%= __('Spindle Max Speed') %></label>
|
||||
<div class="input-group">
|
||||
<input type="number" min="1000" step="1" class="form-control border-accent border-end-0" id="max_rpm" onkeyup="onChange()"
|
||||
onchange="onChange()" value="30000">
|
||||
<span class="input-group-text border-accent">RPM</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
Loading…
Reference in New Issue
Block a user