Initial commit

This commit is contained in:
Glenn de Haan
2018-02-26 22:24:07 +01:00
commit 91bad12f52
35 changed files with 9454 additions and 0 deletions

24
.editorconfig Normal file
View 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
View 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
View 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
View 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
View 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
View 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;

View 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;

View 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();

View 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;

View 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();

View 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
View 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
View 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
View 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');
});
});

View File

@@ -0,0 +1 @@
<h2>404 Not Found !</h2>

View File

@@ -0,0 +1,4 @@
<h2>Old Browser</h2>
<p>
Please update your browser to make sure our website works properly
</p>

View File

@@ -0,0 +1,4 @@
User-agent: *
Disallow: /fonts/
Disallow: /scss/
Sitemap: <%= baseUrl %>sitemap.xml

View 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
View 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>

View File

@@ -0,0 +1 @@
Some text here<br/>

View File

@@ -0,0 +1,4 @@
<footer>
<hr/>
<p>© Copyright The Awesome People</p>
</footer>

View 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>

View File

@@ -0,0 +1,4 @@
<header>
<h2><%= config.application.name %></h2>
<hr/>
</header>

8340
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

85
package.json Normal file
View 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
View 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);

View 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;

View File

@@ -0,0 +1,7 @@
class Index {
constructor({el}) {
this.el = el;
}
}
module.exports = Index;

14
public/js/settings.js Normal file
View 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
View 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});
}
}
}
}

View File

@@ -0,0 +1,5 @@
label.invalid {
span {
color: #F44336;
}
}

View 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;
}

View 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
View 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";

View File

@@ -0,0 +1,4 @@
// colors
$black: #000000;
$white: #ffffff;
$orange: #ff9800;