mirror of
https://github.com/glenndehaan/unifi-voucher-site.git
synced 2026-03-31 06:24:00 -04:00
Initial commit
This commit is contained in:
24
.editorconfig
Normal file
24
.editorconfig
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# Top-most EditorConfig file
|
||||||
|
root = true
|
||||||
|
|
||||||
|
# Set default charset with unix-style newlines with a newline ending every file
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
end_of_line = lf
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
# PHP PSR2 overrides
|
||||||
|
[*.php]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 4
|
||||||
|
|
||||||
|
# JS overrides
|
||||||
|
[*.js]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 4
|
||||||
|
|
||||||
|
# SCSS and JSON overrides
|
||||||
|
[*.{scss,json}]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
13
.gitignore
vendored
Normal file
13
.gitignore
vendored
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
# PhpStorm
|
||||||
|
.idea
|
||||||
|
|
||||||
|
# General files
|
||||||
|
*~
|
||||||
|
*.DS_STORE
|
||||||
|
|
||||||
|
# Dependency managers
|
||||||
|
node_modules/
|
||||||
|
npm-debug.log
|
||||||
|
|
||||||
|
# Build files
|
||||||
|
dist/
|
||||||
21
LICENCE
Normal file
21
LICENCE
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2017 Glenn de Haan
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
13
README.md
Normal file
13
README.md
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
# UniFi Voucher Site
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
- Install NodeJS 8.0 or higher.
|
||||||
|
- Run `npm install` in the root folder
|
||||||
|
- Run `npm start` or `npm run dev` in the root folder
|
||||||
|
|
||||||
|
Then open up your favorite web browser and go to: http://localhost:3001
|
||||||
|
That's it. Easy right?
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT
|
||||||
111
_scripts/webpack.config.js
Normal file
111
_scripts/webpack.config.js
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
const webpack = require('webpack');
|
||||||
|
const path = require('path');
|
||||||
|
const autoprefixer = require('autoprefixer');
|
||||||
|
const ExtractTextPlugin = require('extract-text-webpack-plugin');
|
||||||
|
const browserslist = require('../package.json').browserslist;
|
||||||
|
const ManifestPlugin = require('webpack-manifest-plugin');
|
||||||
|
|
||||||
|
//Get path so every environment works
|
||||||
|
const projectPath = path.resolve(__dirname);
|
||||||
|
|
||||||
|
//Environment check depending on call from
|
||||||
|
const env = process.env.NODE_ENV || 'development';
|
||||||
|
const isProd = env === 'production';
|
||||||
|
|
||||||
|
//Define all the global config for both production & dev
|
||||||
|
const config = {
|
||||||
|
entry: {
|
||||||
|
main: [
|
||||||
|
projectPath + '/../public/js/main.js',
|
||||||
|
projectPath + '/../public/scss/style.scss'
|
||||||
|
],
|
||||||
|
},
|
||||||
|
output: {
|
||||||
|
path: projectPath + '/../public/dist/',
|
||||||
|
filename: '[name].[hash:6].js'
|
||||||
|
},
|
||||||
|
module: {
|
||||||
|
loaders: [
|
||||||
|
{
|
||||||
|
test: /\.js$/,
|
||||||
|
exclude: /node_modules/,
|
||||||
|
loader: 'babel-loader',
|
||||||
|
query: {
|
||||||
|
presets: ['babel-preset-es2015'].map(require.resolve),
|
||||||
|
sourceMaps: 'inline'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.(css|scss)$/,
|
||||||
|
loader: ExtractTextPlugin.extract({
|
||||||
|
use: [
|
||||||
|
'raw-loader?url=false', {
|
||||||
|
loader: 'postcss-loader',
|
||||||
|
options: {
|
||||||
|
ident: 'postcss',
|
||||||
|
context: path.resolve(__dirname, '../public'),
|
||||||
|
sourceMap: 'inline',
|
||||||
|
plugins: [
|
||||||
|
autoprefixer
|
||||||
|
],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'sass-loader?sourceMap=true'
|
||||||
|
]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
new webpack.DefinePlugin({
|
||||||
|
'process.env': {
|
||||||
|
NODE_ENV: JSON.stringify(env)
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
new ExtractTextPlugin({filename: 'main.[hash:6].css', allChunks: true}),
|
||||||
|
new ManifestPlugin({
|
||||||
|
fileName: 'rev-manifest.json',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
resolve: {
|
||||||
|
extensions: ['.js'],
|
||||||
|
modules: [path.join(__dirname, '../node_modules')]
|
||||||
|
},
|
||||||
|
devServer: {
|
||||||
|
compress: true,
|
||||||
|
disableHostCheck: true,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
//Extra options depending on environment
|
||||||
|
if (isProd) {
|
||||||
|
Object.assign(config, {
|
||||||
|
plugins: config.plugins.concat([
|
||||||
|
new webpack.LoaderOptionsPlugin({
|
||||||
|
minimize: true,
|
||||||
|
}),
|
||||||
|
new webpack.optimize.UglifyJsPlugin({
|
||||||
|
drop_console: true,
|
||||||
|
output: {
|
||||||
|
comments: false,
|
||||||
|
},
|
||||||
|
mangle: {
|
||||||
|
screw_ie8: true,
|
||||||
|
},
|
||||||
|
compressor: {
|
||||||
|
screw_ie8: true,
|
||||||
|
warnings: false,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
])
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
Object.assign(config, {
|
||||||
|
devtool: 'cheap-module-eval-source-map',
|
||||||
|
plugins: config.plugins.concat([
|
||||||
|
new webpack.NoEmitOnErrorsPlugin()
|
||||||
|
])
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = config;
|
||||||
24
app/config/config.js
Normal file
24
app/config/config.js
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
/**
|
||||||
|
* General config
|
||||||
|
*/
|
||||||
|
const config = {
|
||||||
|
application: {
|
||||||
|
name: "NodeExpress",
|
||||||
|
env: " (local)",
|
||||||
|
basePath: "/",
|
||||||
|
port: 3001,
|
||||||
|
bind: "0.0.0.0",
|
||||||
|
supportedBrowsers: [
|
||||||
|
"Chrome >= 52",
|
||||||
|
"Firefox >= 47",
|
||||||
|
"Safari >= 10",
|
||||||
|
"Edge == All",
|
||||||
|
"IE == 11"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
session: {
|
||||||
|
secret: "averysecretstring"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = config;
|
||||||
21
app/controllers/Api/BaseController.js
Normal file
21
app/controllers/Api/BaseController.js
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
class BaseController {
|
||||||
|
constructor() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a response to express
|
||||||
|
*
|
||||||
|
* @param response
|
||||||
|
* @param status
|
||||||
|
* @param data
|
||||||
|
* @param contentType
|
||||||
|
*/
|
||||||
|
jsonResponse(response, status, data, contentType = 'application/json') {
|
||||||
|
response.type(contentType);
|
||||||
|
response.status(status);
|
||||||
|
response.json(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = BaseController;
|
||||||
19
app/controllers/Api/IndexController.js
Normal file
19
app/controllers/Api/IndexController.js
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
const baseController = require('./BaseController');
|
||||||
|
|
||||||
|
class IndexController extends baseController {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Action for the default api route
|
||||||
|
*
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
*/
|
||||||
|
indexAction(req, res) {
|
||||||
|
this.jsonResponse(res, 200, { 'message': 'Default API route!' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = new IndexController();
|
||||||
26
app/controllers/Web/BaseController.js
Normal file
26
app/controllers/Web/BaseController.js
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
const config = require("../../config/config");
|
||||||
|
|
||||||
|
class BaseController {
|
||||||
|
constructor() {
|
||||||
|
this.baseConfig = {
|
||||||
|
config: config,
|
||||||
|
hostname: '',
|
||||||
|
baseUrl: ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the complete config base + page specific
|
||||||
|
*
|
||||||
|
* @param request
|
||||||
|
* @param pageSpecificConfig
|
||||||
|
*/
|
||||||
|
mergePageConfig(request, pageSpecificConfig) {
|
||||||
|
this.baseConfig.hostname = request.hostname;
|
||||||
|
this.baseConfig.baseUrl = `${request.protocol}://${request.hostname}${config.application.basePath}`;
|
||||||
|
|
||||||
|
return Object.assign(this.baseConfig, pageSpecificConfig);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = BaseController;
|
||||||
74
app/controllers/Web/IndexController.js
Normal file
74
app/controllers/Web/IndexController.js
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
const baseController = require('./BaseController');
|
||||||
|
|
||||||
|
class IndexController extends baseController {
|
||||||
|
/**
|
||||||
|
* Renders the Home page
|
||||||
|
*
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
*/
|
||||||
|
indexAction(req, res) {
|
||||||
|
res.render('index', this.mergePageConfig(req, {
|
||||||
|
template: 'index/index',
|
||||||
|
pageTitle: 'Home'
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the 404 page
|
||||||
|
*
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
*/
|
||||||
|
notFoundAction(req, res) {
|
||||||
|
res.render('index', this.mergePageConfig(req, {
|
||||||
|
template: 'general/notfound',
|
||||||
|
pageTitle: 'Not Found'
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the old browser page (Fallback page)
|
||||||
|
*
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
*/
|
||||||
|
oldBrowserAction(req, res) {
|
||||||
|
res.render('index', this.mergePageConfig(req, {
|
||||||
|
template: 'general/oldbrowser',
|
||||||
|
pageTitle: 'Old Browser'
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the sitemap.xml
|
||||||
|
*
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @param routes
|
||||||
|
*/
|
||||||
|
siteMapAction(req, res, routes) {
|
||||||
|
res.type("application/xml");
|
||||||
|
res.render('general/sitemap', this.mergePageConfig(req, {
|
||||||
|
template: false,
|
||||||
|
pageTitle: false,
|
||||||
|
routes: routes
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the robots.txt
|
||||||
|
*
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
*/
|
||||||
|
robotsAction(req, res) {
|
||||||
|
res.type("text/plain");
|
||||||
|
res.render('general/robots', this.mergePageConfig(req, {
|
||||||
|
template: false,
|
||||||
|
pageTitle: false
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = new IndexController();
|
||||||
26
app/helpers/modules/Router.js
Normal file
26
app/helpers/modules/Router.js
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
class Router {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An easy to use function to add multiple routes to the Express router
|
||||||
|
*
|
||||||
|
* @param router
|
||||||
|
* @param routes
|
||||||
|
* @param type
|
||||||
|
*/
|
||||||
|
routesToRouter(router, routes, type) {
|
||||||
|
for (let item = 0; item < routes.length; item += 1) {
|
||||||
|
const route = routes[item];
|
||||||
|
const controller = route.controller.charAt(0).toUpperCase() + route.controller.slice(1);
|
||||||
|
eval(
|
||||||
|
`
|
||||||
|
const ${route.controller}Controller = require('../../controllers/${type}/${controller}Controller');
|
||||||
|
router.${route.method}('${route.route}', (req, res) => {
|
||||||
|
${route.controller}Controller.${route.action}Action(req, res);
|
||||||
|
});
|
||||||
|
`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = new Router();
|
||||||
22
app/routers/Api.js
Normal file
22
app/routers/Api.js
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
/**
|
||||||
|
* Import base packages
|
||||||
|
*/
|
||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
const routerUtils = require('../helpers/modules/Router');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define routes
|
||||||
|
*/
|
||||||
|
const routes = [
|
||||||
|
{
|
||||||
|
route: '/',
|
||||||
|
method: 'get',
|
||||||
|
controller: 'Index',
|
||||||
|
action: 'index'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
routerUtils.routesToRouter(router, routes, 'Api');
|
||||||
|
|
||||||
|
module.exports = {router, routes};
|
||||||
22
app/routers/Web.js
Normal file
22
app/routers/Web.js
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
/**
|
||||||
|
* Import base packages
|
||||||
|
*/
|
||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
const routerUtils = require('../helpers/modules/Router');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define routes
|
||||||
|
*/
|
||||||
|
const routes = [
|
||||||
|
{
|
||||||
|
route: '/',
|
||||||
|
method: 'get',
|
||||||
|
controller: 'Index',
|
||||||
|
action: 'index'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
routerUtils.routesToRouter(router, routes, 'Web');
|
||||||
|
|
||||||
|
module.exports = {router, routes};
|
||||||
110
app/server.js
Normal file
110
app/server.js
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
/**
|
||||||
|
* Import base packages
|
||||||
|
*/
|
||||||
|
const express = require('express');
|
||||||
|
const app = express();
|
||||||
|
const bodyParser = require('body-parser');
|
||||||
|
const session = require('express-session');
|
||||||
|
const browsersupport = require('express-browsersupport');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Import own packages
|
||||||
|
*/
|
||||||
|
const config = require('./config/config');
|
||||||
|
const webRouter = require('./routers/Web');
|
||||||
|
const apiRouter = require('./routers/Api');
|
||||||
|
const indexController = require('./controllers/Web/IndexController');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set template engine
|
||||||
|
*/
|
||||||
|
app.set('view engine', 'ejs');
|
||||||
|
app.set('views', `${__dirname}/views`);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serve static public dir
|
||||||
|
*/
|
||||||
|
app.use(express.static(`${__dirname}/../public`));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configure app to use bodyParser()
|
||||||
|
*/
|
||||||
|
app.use(bodyParser.urlencoded({extended: true}));
|
||||||
|
app.use(bodyParser.json());
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configure sessions in app
|
||||||
|
*/
|
||||||
|
app.use(session({secret: config.session.secret, resave: true, saveUninitialized: true}));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configure app to use Browser Support
|
||||||
|
*/
|
||||||
|
app.use(browsersupport({
|
||||||
|
redirectUrl: "/oldbrowser",
|
||||||
|
supportedBrowsers: config.application.supportedBrowsers
|
||||||
|
}));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configure routers
|
||||||
|
*/
|
||||||
|
app.use('/', webRouter.router);
|
||||||
|
app.use('/api', apiRouter.router);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render sitemap.xml and robots.txt
|
||||||
|
*/
|
||||||
|
app.get('/sitemap.xml', (req, res) => {
|
||||||
|
indexController.siteMapAction(req, res, webRouter.routes);
|
||||||
|
});
|
||||||
|
app.get('/robots.txt', (req, res) => {
|
||||||
|
indexController.robotsAction(req, res);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render old browser page
|
||||||
|
*/
|
||||||
|
app.get('/oldbrowser', (req, res) => {
|
||||||
|
indexController.oldBrowserAction(req, res);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup default 404 message
|
||||||
|
*/
|
||||||
|
app.use((req, res) => {
|
||||||
|
res.status(404);
|
||||||
|
|
||||||
|
// respond with json
|
||||||
|
if (req.originalUrl.split('/')[1] === 'api') {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API 404 not found
|
||||||
|
*/
|
||||||
|
res.send({error: 'This API route is not implemented yet'});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
indexController.notFoundAction(req, res);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disable powered by header for security reasons
|
||||||
|
*/
|
||||||
|
app.disable('x-powered-by');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start listening on port
|
||||||
|
*/
|
||||||
|
const server = app.listen(config.application.port, config.application.bind, () => {
|
||||||
|
console.log(`[NODE] App is running on: ${config.application.bind}:${config.application.port}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle nodemon shutdown
|
||||||
|
*/
|
||||||
|
process.once('SIGUSR2', () => {
|
||||||
|
server.close(() => {
|
||||||
|
console.log(`[NODE] Express exited! Port ${config.application.port} is now free!`);
|
||||||
|
process.kill(process.pid, 'SIGUSR2');
|
||||||
|
});
|
||||||
|
});
|
||||||
1
app/views/general/notfound.ejs
Normal file
1
app/views/general/notfound.ejs
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<h2>404 Not Found !</h2>
|
||||||
4
app/views/general/oldbrowser.ejs
Normal file
4
app/views/general/oldbrowser.ejs
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<h2>Old Browser</h2>
|
||||||
|
<p>
|
||||||
|
Please update your browser to make sure our website works properly
|
||||||
|
</p>
|
||||||
4
app/views/general/robots.ejs
Normal file
4
app/views/general/robots.ejs
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
User-agent: *
|
||||||
|
Disallow: /fonts/
|
||||||
|
Disallow: /scss/
|
||||||
|
Sitemap: <%= baseUrl %>sitemap.xml
|
||||||
17
app/views/general/sitemap.ejs
Normal file
17
app/views/general/sitemap.ejs
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml">
|
||||||
|
<url>
|
||||||
|
<loc><%= baseUrl %></loc>
|
||||||
|
<changefreq>daily</changefreq>
|
||||||
|
<priority>1.0</priority>
|
||||||
|
</url>
|
||||||
|
<% routes.forEach((route) => { %>
|
||||||
|
<% if(route.route !== "/") { %>
|
||||||
|
<url>
|
||||||
|
<loc><%= baseUrl %><%= route.route.substring(1) %></loc>
|
||||||
|
<changefreq>monthly</changefreq>
|
||||||
|
<priority>0.5</priority>
|
||||||
|
</url>
|
||||||
|
<% } %>
|
||||||
|
<% }); %>
|
||||||
|
</urlset>
|
||||||
15
app/views/index.ejs
Normal file
15
app/views/index.ejs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<% include ./partials/head.ejs %>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<% include ./partials/header.ejs %>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<%- include(template) %>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<% include ./partials/footer.ejs %>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
1
app/views/index/index.ejs
Normal file
1
app/views/index/index.ejs
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Some text here<br/>
|
||||||
4
app/views/partials/footer.ejs
Normal file
4
app/views/partials/footer.ejs
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<footer>
|
||||||
|
<hr/>
|
||||||
|
<p>© Copyright The Awesome People</p>
|
||||||
|
</footer>
|
||||||
24
app/views/partials/head.ejs
Normal file
24
app/views/partials/head.ejs
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<title><%= config.application.name %> <%= config.application.env %> | <%= pageTitle %></title>
|
||||||
|
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimal-ui">
|
||||||
|
|
||||||
|
<meta name="description" content="">
|
||||||
|
<meta name="author" content="">
|
||||||
|
<meta name="keywords" content="">
|
||||||
|
|
||||||
|
<meta property="og:title" content="<%= config.application.name %>"/>
|
||||||
|
<meta property="og:type" content="website"/>
|
||||||
|
<meta property="og:url" content="<%= baseUrl %>"/>
|
||||||
|
<meta property="og:description" content=""/>
|
||||||
|
|
||||||
|
<link rel="alternate" href="<%= baseUrl %>" hreflang="en" />
|
||||||
|
<link rel="canonical" href="<%= baseUrl %>" />
|
||||||
|
<script type='application/ld+json'>
|
||||||
|
{
|
||||||
|
"@context": "http://schema.org",
|
||||||
|
"@type": "WebSite",
|
||||||
|
"name": "<%= config.application.name %>",
|
||||||
|
"url": "<%= baseUrl %>"
|
||||||
|
}
|
||||||
|
</script>
|
||||||
4
app/views/partials/header.ejs
Normal file
4
app/views/partials/header.ejs
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<header>
|
||||||
|
<h2><%= config.application.name %></h2>
|
||||||
|
<hr/>
|
||||||
|
</header>
|
||||||
8340
package-lock.json
generated
Normal file
8340
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
85
package.json
Normal file
85
package.json
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
{
|
||||||
|
"name": "unifi-voucher-site",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"description": "NPM packages for unifi-voucher-site",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"start": "concurrently --kill-others 'npm run frontend' 'npm run backend:dev'",
|
||||||
|
"backend:start": "node ./app/server.js",
|
||||||
|
"backend:dev": "nodemon -L --watch ./app ./app/server.js",
|
||||||
|
"prebuild": "rimraf ./public/dist",
|
||||||
|
"build": "cross-env NODE_ENV=production webpack --config ./_scripts/webpack.config.js --progress",
|
||||||
|
"prefrontend": "rimraf ./public/dist",
|
||||||
|
"frontend": "cross-env NODE_ENV=development webpack --config ./_scripts/webpack.config.js --watch --watch-poll -d --progress",
|
||||||
|
"lint": "eslint -c ./package.json ./app ./public/js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.0.0"
|
||||||
|
},
|
||||||
|
"eslintConfig": {
|
||||||
|
"parserOptions": {
|
||||||
|
"ecmaVersion": 6,
|
||||||
|
"sourceType": "module"
|
||||||
|
},
|
||||||
|
"env": {
|
||||||
|
"browser": true,
|
||||||
|
"node": true
|
||||||
|
},
|
||||||
|
"rules": {
|
||||||
|
"no-console": 0,
|
||||||
|
"comma-dangle": [
|
||||||
|
"error",
|
||||||
|
"never"
|
||||||
|
],
|
||||||
|
"indent": [
|
||||||
|
"error",
|
||||||
|
4
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"globals": {
|
||||||
|
"site": false,
|
||||||
|
"expressConfig": false
|
||||||
|
},
|
||||||
|
"extends": [
|
||||||
|
"eslint:recommended"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"browserslist": [
|
||||||
|
"Chrome >= 52",
|
||||||
|
"Firefox >= 47",
|
||||||
|
"ie 11",
|
||||||
|
"last 2 Edge versions",
|
||||||
|
"Safari >= 8",
|
||||||
|
"ios_saf >= 9",
|
||||||
|
"last 2 and_chr versions"
|
||||||
|
],
|
||||||
|
"author": "Glenn de Haan",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"autoprefixer": "^8.0.0",
|
||||||
|
"babel-core": "^6.26.0",
|
||||||
|
"babel-loader": "^7.1.3",
|
||||||
|
"babel-preset-es2015": "^6.24.1",
|
||||||
|
"body-parser": "^1.17.2",
|
||||||
|
"cross-env": "^5.1.3",
|
||||||
|
"ejs": "^2.5.6",
|
||||||
|
"express": "^4.15.3",
|
||||||
|
"express-browsersupport": "^1.3.3",
|
||||||
|
"express-session": "^1.15.6",
|
||||||
|
"extract-text-webpack-plugin": "^3.0.2",
|
||||||
|
"node-sass": "^4.7.2",
|
||||||
|
"postcss": "^6.0.19",
|
||||||
|
"postcss-loader": "^2.1.1",
|
||||||
|
"raw-loader": "^0.5.1",
|
||||||
|
"rimraf": "^2.6.2",
|
||||||
|
"sass-loader": "^6.0.6",
|
||||||
|
"socket.io-client": "^2.0.4",
|
||||||
|
"webpack": "^3.11.0",
|
||||||
|
"webpack-manifest-plugin": "^1.3.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"concurrently": "^3.5.1",
|
||||||
|
"eslint": "^4.18.1",
|
||||||
|
"nodemon": "^1.15.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
34
public/js/main.js
Normal file
34
public/js/main.js
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import settings from './settings';
|
||||||
|
|
||||||
|
// Create global
|
||||||
|
window.site = {};
|
||||||
|
site.settings = settings;
|
||||||
|
|
||||||
|
function initialize() {
|
||||||
|
site.html = document.querySelector('html');
|
||||||
|
site.body = document.querySelector('body');
|
||||||
|
|
||||||
|
console.log('JS Start');
|
||||||
|
|
||||||
|
initializeModules();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the modules with all the components
|
||||||
|
*/
|
||||||
|
function initializeModules() {
|
||||||
|
for(let modulesItem = 0; modulesItem < settings.modules.length; modulesItem++) {
|
||||||
|
const module = settings.modules[modulesItem];
|
||||||
|
|
||||||
|
const moduleClass = require(`./modules/${module.group}/${module.module}`);
|
||||||
|
const domElements = document.querySelectorAll(module.el);
|
||||||
|
|
||||||
|
if (typeof moduleClass !== 'undefined' && domElements.length > 0) {
|
||||||
|
new moduleClass({
|
||||||
|
el: domElements
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener("DOMContentLoaded", initialize);
|
||||||
32
public/js/modules/default/Socket.js
Normal file
32
public/js/modules/default/Socket.js
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import io from 'socket.io-client';
|
||||||
|
|
||||||
|
class Socket {
|
||||||
|
constructor({el}) {
|
||||||
|
this.el = el;
|
||||||
|
this.socket = false;
|
||||||
|
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this.socket = io.connect(`http://${expressConfig.hostname}:${expressConfig.port}`);
|
||||||
|
|
||||||
|
this.socket.on('connect', this.connect);
|
||||||
|
this.socket.on('disconnect', this.disconnect);
|
||||||
|
this.socket.on('error', this.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
connect() {
|
||||||
|
console.log('[SOCKET] Connected!');
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnect() {
|
||||||
|
console.log('[SOCKET] Disconnected!');
|
||||||
|
}
|
||||||
|
|
||||||
|
error() {
|
||||||
|
console.log('[SOCKET] Error!');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Socket;
|
||||||
7
public/js/modules/index/Index.js
Normal file
7
public/js/modules/index/Index.js
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
class Index {
|
||||||
|
constructor({el}) {
|
||||||
|
this.el = el;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Index;
|
||||||
14
public/js/settings.js
Normal file
14
public/js/settings.js
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
export default {
|
||||||
|
modules: [
|
||||||
|
{
|
||||||
|
module: "Socket",
|
||||||
|
group: "default",
|
||||||
|
el: "main"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
module: "Index",
|
||||||
|
group: "index",
|
||||||
|
el: "#home"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
33
public/js/utils/Api.js
Normal file
33
public/js/utils/Api.js
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
/**
|
||||||
|
* Function to post data to the API
|
||||||
|
*
|
||||||
|
* @param method
|
||||||
|
* @param data
|
||||||
|
* @param callback
|
||||||
|
* @param auth
|
||||||
|
*/
|
||||||
|
export default (method, data, callback, auth = "") => {
|
||||||
|
const xmlhttp = new XMLHttpRequest();
|
||||||
|
let route = "/api";
|
||||||
|
|
||||||
|
xmlhttp.open("POST", route);
|
||||||
|
xmlhttp.setRequestHeader("Content-Type", "application/json");
|
||||||
|
xmlhttp.setRequestHeader("Bearer", auth);
|
||||||
|
xmlhttp.send(JSON.stringify(data));
|
||||||
|
|
||||||
|
xmlhttp.onreadystatechange = function () {
|
||||||
|
if (xmlhttp.readyState === XMLHttpRequest.DONE) {
|
||||||
|
const responseCode = xmlhttp.status;
|
||||||
|
const response = JSON.parse(xmlhttp.responseText);
|
||||||
|
console.log('response', response);
|
||||||
|
|
||||||
|
console.log('responseCode', responseCode);
|
||||||
|
|
||||||
|
if (responseCode === 200) {
|
||||||
|
callback({error: false});
|
||||||
|
} else {
|
||||||
|
callback({error: true, fields: response.fields, raw_error: response.error});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
5
public/scss/components/_form.scss
Normal file
5
public/scss/components/_form.scss
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
label.invalid {
|
||||||
|
span {
|
||||||
|
color: #F44336;
|
||||||
|
}
|
||||||
|
}
|
||||||
259
public/scss/global/_normalize.scss
Normal file
259
public/scss/global/_normalize.scss
Normal file
@@ -0,0 +1,259 @@
|
|||||||
|
/*! normalize.css v3.0.2 | MIT License | git.io/normalize */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Set default font family to sans-serif.
|
||||||
|
* 2. Prevent iOS text size adjust after orientation change, without disabling
|
||||||
|
* user zoom.
|
||||||
|
*/
|
||||||
|
|
||||||
|
html {
|
||||||
|
font-family: sans-serif; /* 1 */
|
||||||
|
-ms-text-size-adjust: 100%; /* 2 */
|
||||||
|
-webkit-text-size-adjust: 100%; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove default margin.
|
||||||
|
*/
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* HTML5 display definitions
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Correct `block` display not defined for any HTML5 element in IE 8/9.
|
||||||
|
* Correct `block` display not defined for `details` or `summary` in IE 10/11
|
||||||
|
* and Firefox.
|
||||||
|
* Correct `block` display not defined for `main` in IE 11.
|
||||||
|
*/
|
||||||
|
|
||||||
|
article,
|
||||||
|
aside,
|
||||||
|
details,
|
||||||
|
figcaption,
|
||||||
|
figure,
|
||||||
|
footer,
|
||||||
|
header,
|
||||||
|
hgroup,
|
||||||
|
main,
|
||||||
|
menu,
|
||||||
|
nav,
|
||||||
|
section,
|
||||||
|
summary {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Correct `inline-block` display not defined in IE 8/9.
|
||||||
|
* 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera.
|
||||||
|
*/
|
||||||
|
|
||||||
|
audio,
|
||||||
|
canvas,
|
||||||
|
progress,
|
||||||
|
video {
|
||||||
|
display: inline-block; /* 1 */
|
||||||
|
vertical-align: baseline; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prevent modern browsers from displaying `audio` without controls.
|
||||||
|
* Remove excess height in iOS 5 devices.
|
||||||
|
*/
|
||||||
|
|
||||||
|
audio:not([controls]) {
|
||||||
|
display: none;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Address `[hidden]` styling not present in IE 8/9/10.
|
||||||
|
* Hide the `template` element in IE 8/9/11, Safari, and Firefox < 22.
|
||||||
|
*/
|
||||||
|
|
||||||
|
[hidden],
|
||||||
|
template {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Links
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the gray background color from active links in IE 10.
|
||||||
|
*/
|
||||||
|
|
||||||
|
a {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Improve readability when focused and also mouse hovered in all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
a:active,
|
||||||
|
a:hover {
|
||||||
|
outline: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Embedded content
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove border when inside `a` element in IE 8/9/10.
|
||||||
|
*/
|
||||||
|
|
||||||
|
img {
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Correct overflow not hidden in IE 9/10/11.
|
||||||
|
*/
|
||||||
|
|
||||||
|
svg:not(:root) {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Forms
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Known limitation: by default, Chrome and Safari on OS X allow very limited
|
||||||
|
* styling of `select`, unless a `border` property is set.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Correct color not being inherited.
|
||||||
|
* Known issue: affects color of disabled elements.
|
||||||
|
* 2. Correct font properties not being inherited.
|
||||||
|
* 3. Address margins set differently in Firefox 4+, Safari, and Chrome.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button,
|
||||||
|
input,
|
||||||
|
optgroup,
|
||||||
|
select,
|
||||||
|
textarea {
|
||||||
|
color: inherit; /* 1 */
|
||||||
|
font: inherit; /* 2 */
|
||||||
|
margin: 0; /* 3 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Address `overflow` set to `hidden` in IE 8/9/10/11.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button {
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Address inconsistent `text-transform` inheritance for `button` and `select`.
|
||||||
|
* All other form control elements do not inherit `text-transform` values.
|
||||||
|
* Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera.
|
||||||
|
* Correct `select` style inheritance in Firefox.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button,
|
||||||
|
select {
|
||||||
|
text-transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio`
|
||||||
|
* and `video` controls.
|
||||||
|
* 2. Correct inability to style clickable `input` types in iOS.
|
||||||
|
* 3. Improve usability and consistency of cursor style between image-type
|
||||||
|
* `input` and others.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button,
|
||||||
|
html input[type="button"], /* 1 */
|
||||||
|
input[type="reset"],
|
||||||
|
input[type="submit"] {
|
||||||
|
appearance: none; /* 2 */
|
||||||
|
border: none;
|
||||||
|
cursor: pointer; /* 3 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Re-set default cursor for disabled elements.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button[disabled],
|
||||||
|
html input[disabled] {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove inner padding and border in Firefox 4+.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button::-moz-focus-inner,
|
||||||
|
input::-moz-focus-inner {
|
||||||
|
border: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Address Firefox 4+ setting `line-height` on `input` using `!important` in
|
||||||
|
* the UA stylesheet.
|
||||||
|
*/
|
||||||
|
|
||||||
|
input {
|
||||||
|
line-height: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Address `appearance` set to `searchfield` in Safari and Chrome.
|
||||||
|
* 2. Address `box-sizing` set to `border-box` in Safari and Chrome
|
||||||
|
* (include `-moz` to future-proof).
|
||||||
|
*/
|
||||||
|
|
||||||
|
input[type="search"] {
|
||||||
|
-webkit-appearance: textfield; /* 1 */
|
||||||
|
-moz-box-sizing: content-box;
|
||||||
|
-webkit-box-sizing: content-box; /* 2 */
|
||||||
|
box-sizing: content-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove inner padding and search cancel button in Safari and Chrome on OS X.
|
||||||
|
* Safari (but not Chrome) clips the cancel button when the search input has
|
||||||
|
* padding (and `textfield` appearance).
|
||||||
|
*/
|
||||||
|
|
||||||
|
input[type="search"]::-webkit-search-cancel-button,
|
||||||
|
input[type="search"]::-webkit-search-decoration {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove default vertical scrollbar in IE 8/9/10/11.
|
||||||
|
*/
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tables
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove most spacing between table cells.
|
||||||
|
*/
|
||||||
|
|
||||||
|
table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
border-spacing: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
td,
|
||||||
|
th {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
22
public/scss/global/_utils.scss
Normal file
22
public/scss/global/_utils.scss
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
// Visibility
|
||||||
|
.hidden {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.visible {
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Accessibility: Positioning content offscreen
|
||||||
|
* @see https://www.paciellogroup.com/blog/2012/05/html5-accessibility-chops-hidden-and-aria-hidden/
|
||||||
|
* @see http://alistapart.com/article/now-you-see-me
|
||||||
|
* @see http://accessibilitytips.com/2008/03/04/positioning-content-offscreen/
|
||||||
|
*/
|
||||||
|
.off-screen,
|
||||||
|
.visually-hidden {
|
||||||
|
position: absolute;
|
||||||
|
// never use the top property!
|
||||||
|
left: -999em;
|
||||||
|
margin: 0; // fix uncontrolled whitespace still being displayed
|
||||||
|
}
|
||||||
19
public/scss/style.scss
Normal file
19
public/scss/style.scss
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
@charset "UTF-8";
|
||||||
|
|
||||||
|
//
|
||||||
|
// General
|
||||||
|
//
|
||||||
|
|
||||||
|
// Variables
|
||||||
|
@import "variables/colors";
|
||||||
|
|
||||||
|
// Global
|
||||||
|
@import "global/normalize";
|
||||||
|
@import "global/utils";
|
||||||
|
|
||||||
|
//
|
||||||
|
// Local App
|
||||||
|
//
|
||||||
|
|
||||||
|
// Components
|
||||||
|
@import "components/form";
|
||||||
4
public/scss/variables/_colors.scss
Normal file
4
public/scss/variables/_colors.scss
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
// colors
|
||||||
|
$black: #000000;
|
||||||
|
$white: #ffffff;
|
||||||
|
$orange: #ff9800;
|
||||||
Reference in New Issue
Block a user