9 Commits

16 changed files with 2570 additions and 55 deletions

14
.eslintrc.json Normal file
View File

@ -0,0 +1,14 @@
{
"env": {
"es2021": true,
"node": true
},
"extends": "google",
"overrides": [
],
"parserOptions": {
"ecmaVersion": "latest"
},
"rules": {
}
}

20
.woodpecker.yml Normal file
View File

@ -0,0 +1,20 @@
pipeline:
setup:
image: node:18
commands:
- npm install
lint:
image: node:18
commands:
- npm run lint
gitea_release:
image: plugins/gitea-release
settings:
api_key:
from_secret: gitea_api_key
base_url: https://git.metaunix.net
title: "${CI_COMMIT_TAG}"
when:
event: tag

View File

@ -1,5 +1,7 @@
$primary-color: orange; $primary-color: orangered;
$primary-color-highlight: darken($primary-color, 10%); $primary-color-highlight: darken($primary-color, 10%);
$box-shadow-1: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23);
$box-shadow-2: 0 10px 20px rgba(0,0,0,0.19), 0 6px 6px rgba(0,0,0,0.23);
$nav-bar-height: 60px; $nav-bar-height: 60px;
@ -8,6 +10,29 @@ body{
background: lightgrey; background: lightgrey;
} }
a{
color: $primary-color;
transition: all 230ms ease-in-out;
&:hover{
color: $primary-color-highlight;
}
}
.button.button-primary,
button.button-primary,
input[type="button"].button-primary,
input[type="reset"].button-primary,
input[type="submit"].button-primary{
background-color: $primary-color;
color: white;
transition: all 230ms ease-in-out;
&:hover{
background-color: $primary-color-highlight;
}
}
.container.fluid{ .container.fluid{
max-width: 100%; max-width: 100%;
} }
@ -19,7 +44,7 @@ body{
width: 100%; width: 100%;
height: $nav-bar-height; height: $nav-bar-height;
background: #212121; background: #212121;
box-shadow: 0 2px 1px rgba(0, 0, 0, .25); box-shadow: $box-shadow-1;
color: white; color: white;
.nav-bar-left{ .nav-bar-left{
@ -36,26 +61,45 @@ body{
.site-logo, .site-logo,
.nav-link a{ .nav-link a{
padding: 9px 12px; padding: 10px 12px;
font-size: 2.5rem; font-size: 2.5rem;
text-decoration: none;
} }
.site-logo{ .site-logo{
padding-left: 35px;
font-weight: bold; font-weight: bold;
} }
.nav-link a{
color: $primary-color;
transition: all 230ms ease-in-out;
&:hover{
color: $primary-color-highlight;
}
}
} }
#main-content{ #main-content{
margin-top: 25px; margin-top: 25px;
padding: 15px 25px; padding: 20px 32px;
background: white; background: white;
box-shadow: $box-shadow-2;
}
#record-actions{
p{
margin-bottom: 0;
font-size: 2rem;
}
}
#item-header{
margin-bottom: 25px;
h1,
p{
display: inline-block;
margin-bottom: 7px;
}
.item-added-date,
.item-updated-date{
margin-bottom: 5px;
color: #666;
font-size: 1.6rem;
font-style: italic;
}
} }

View File

@ -5,14 +5,18 @@ const app = express();
const port = 3000; const port = 3000;
// initialize database connection // initialize database connection
const db = require('./src/models'); (async () => {
db.sequelize.sync({ force: true }).then(() => { const db = require('./src/models');
console.log("Drop and re-sync db."); await db.sequelize.sync({
}); alter: true,
});
})();
// set up body POST parameters // set up body POST parameters
app.use(express.json()); app.use(express.json());
app.use(express.urlencoded({ extended: true })); app.use(express.urlencoded({
extended: true,
}));
// load the template engine // load the template engine
app.set('view engine', 'twig'); app.set('view engine', 'twig');
@ -28,6 +32,9 @@ const itemRoutes = require('./src/routes/item');
app.get('/', homeRoutes.getIndex); app.get('/', homeRoutes.getIndex);
app.get('/item/add', itemRoutes.getAdd); app.get('/item/add', itemRoutes.getAdd);
app.post('/item/add', itemRoutes.postAdd); app.post('/item/add', itemRoutes.postAdd);
app.get('/item/:id', itemRoutes.getItem);
app.get('/item/:id/edit', itemRoutes.getItemEdit);
app.post('/item/:id/edit', itemRoutes.postItemEdit);
// start app // start app
app.listen(port, () => { app.listen(port, () => {

9
nodemon.json Normal file
View File

@ -0,0 +1,9 @@
{
"verbose": true,
"ext": "js,json,twig",
"ignore": [
"*.test.js",
"assets/",
"data/"
]
}

2168
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -6,6 +6,8 @@
"scripts": { "scripts": {
"start": "node index.js", "start": "node index.js",
"gulp": "gulp", "gulp": "gulp",
"nodemon": "nodemon index.js",
"lint": "eslint index.js src/**/*.js",
"test": "echo \"Error: no test specified\" && exit 1" "test": "echo \"Error: no test specified\" && exit 1"
}, },
"repository": { "repository": {
@ -19,8 +21,11 @@
"author": "Gregory Ballanine <gballantine@bitgoblin.tech>", "author": "Gregory Ballanine <gballantine@bitgoblin.tech>",
"license": "BSD-2-Clause", "license": "BSD-2-Clause",
"devDependencies": { "devDependencies": {
"eslint": "^8.26.0",
"eslint-config-google": "^0.14.0",
"gulp": "^4.0.2", "gulp": "^4.0.2",
"gulp-sass": "^5.1.0", "gulp-sass": "^5.1.0",
"nodemon": "^2.0.20",
"sass": "^1.55.0" "sass": "^1.55.0"
}, },
"dependencies": { "dependencies": {

View File

@ -1,9 +1,9 @@
const dbConfig = require('config').get('database'); const dbConfig = require('config').get('database');
const Sequelize = require("sequelize"); const Sequelize = require('sequelize');
const sequelize = new Sequelize({ const sequelize = new Sequelize({
dialect: dbConfig.get('driver'), dialect: dbConfig.get('driver'),
storage: dbConfig.get('connection_string') storage: dbConfig.get('connection_string'),
}); });
const db = {}; const db = {};
@ -11,6 +11,6 @@ const db = {};
db.Sequelize = Sequelize; db.Sequelize = Sequelize;
db.sequelize = sequelize; db.sequelize = sequelize;
db.items = require("./item.js")(sequelize, Sequelize); db.items = require('./item.js')(sequelize, Sequelize);
module.exports = db; module.exports = db;

View File

@ -1,21 +1,35 @@
module.exports = (sequelize, Sequelize) => { module.exports = (sequelize, Sequelize) => {
const Item = sequelize.define('item', {
const Item = sequelize.define("item", {
name: { name: {
type: Sequelize.STRING type: Sequelize.STRING,
}, },
manufacturer: { manufacturer: {
type: Sequelize.STRING type: Sequelize.STRING,
},
serialNumber: {
type: Sequelize.STRING,
},
skuNumber: {
type: Sequelize.STRING,
}, },
type: { type: {
type: Sequelize.STRING type: Sequelize.STRING,
} },
purchasedFrom: {
type: Sequelize.STRING,
},
purchasedAt: {
type: Sequelize.DATE,
},
}); });
return Item; return Item;
}; };

View File

@ -2,8 +2,15 @@ const db = require('../models');
const Item = db.items; const Item = db.items;
// GET - / // GET - /
exports.getIndex = async function (req, res) { exports.getIndex = async function(req, res) {
let items = await Item.findAll({}); const items = await Item.findAll({
console.log(items); limit: 10,
res.render('index.twig'); order: [
['updatedAt', 'DESC'],
],
});
res.render('index.twig', {
inventory: items,
});
}; };

View File

@ -2,14 +2,18 @@ const db = require('../models');
const Item = db.items; const Item = db.items;
// GET - /item/add // GET - /item/add
exports.getAdd = async function (req, res) { exports.getAdd = async function(req, res) {
res.render('item/add.twig'); res.render('item/add.twig');
}; };
// POST - /item/add // POST - /item/add
exports.postAdd = async function (req, res) { exports.postAdd = async function(req, res) {
const item = await Item.create({ const item = await Item.create({
name: req.body.item_name, name: req.body.item_name,
serialNumber: req.body.item_serial,
skuNumber: req.body.item_sku,
purchasedFrom: req.body.item_purchase_from,
purchasedAt: req.body.item_purchase_date,
manufacturer: req.body.item_manufacturer, manufacturer: req.body.item_manufacturer,
type: req.body.item_type, type: req.body.item_type,
}); });
@ -18,3 +22,57 @@ exports.postAdd = async function (req, res) {
res.redirect('/'); res.redirect('/');
}; };
// GET - /item/{id}
exports.getItem = async function(req, res) {
const item = await Item.findAll({
where: {
id: req.params.id,
},
});
res.render('item/view.twig', {
item: item[0],
});
};
// GET - /item/{id}/edit
exports.getItemEdit = async function(req, res) {
const item = await Item.findAll({
where: {
id: req.params.id,
},
});
res.render('item/edit.twig', {
item: item[0],
});
};
// POST - /item/{id}/edit
exports.postItemEdit = async function(req, res) {
// fetch item from DB
const itemSearch = await Item.findAll({
where: {
id: req.params.id,
},
});
// retrieve the item record from the array for ease of use
const item = itemSearch[0];
// update item attributes
item.name = req.body.item_name;
item.serialNumber = req.body.item_serial;
item.skuNumber = req.body.item_sku;
item.purchasedFrom = req.body.item_purchase_from;
item.purchasedAt = req.body.item_purchase_date;
item.manufacturer = req.body.item_manufacturer;
item.type = req.body.item_type;
// save attribute changes
await item.save();
// redirect user to item page
res.redirect('/item/' + item.id);
};

View File

@ -11,20 +11,49 @@
</div> </div>
</header> </header>
<section class="row"> <section id="record-actions" class="row">
<div class="columns six"> <div class="columns six">
<a href="/item/add"> <a href="/item/add">
<i class="fa-solid fa-plus"></i> <p><i class="fa-solid fa-plus"></i> Add Item</p>
<p>Add Item</p>
</a> </a>
</div> </div>
<div class="columns six"> <div class="columns six">
<a href="/item/search"> <a href="/item/search">
<i class="fa-solid fa-search"></i> <p><i class="fa-solid fa-search"></i> Search</p>
<p>Search</p>
</a> </a>
</div> </div>
</section> </section>
<hr>
<section class="row">
<div class="columns twelve">
<h3>Recently updated records:</h3>
</div>
</section>
<section class="row">
<table class="columns twelve">
<thead>
<tr>
<th>Name</th>
<th>Manufacturer</th>
<th>Type</th>
<th>Updated at</th>
</tr>
</thead>
<tbody>
{% for item in inventory %}
<tr>
<td><a href="/item/{{ item.id }}">{{ item.name }}</a></td>
<td>{{ item.manufacturer }}</td>
<td>{{ item.type }}</td>
<td>{{ item.updatedAt | date("m/d/Y h:i:s A") }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</section>
{% endblock %} {% endblock %}

View File

@ -1,6 +1,6 @@
{% extends 'layout.twig' %} {% extends 'layout.twig' %}
{% block title %}Create Item{% endblock %} {% block title %}Add New Item{% endblock %}
{% block content %} {% block content %}
@ -17,19 +17,43 @@
<div class="row"> <div class="row">
<div class="columns twelve"> <div class="columns twelve">
<label for="item_name">Item name:</label> <label for="item_name">Item name:</label>
<input class="u-full-width" type="text" placeholder="My new item" id="item_name"> <input class="u-full-width" type="text" placeholder="My new item" id="item_name" name="item_name" required>
</div>
</div>
<div class="row">
<div class="six columns">
<label for="item_serial">Serial number:</label>
<input class="u-full-width" type="text" placeholder="0123456789" id="item_serial" name="item_serial">
</div>
<div class="six columns">
<label for="item_sku">SKU number:</label>
<input class="u-full-width" type="text" placeholder="ABC12345678" id="item_sku" name="item_sku">
</div>
</div>
<div class="row">
<div class="six columns">
<label for="item_purchase_from">Purchased from:</label>
<input class="u-full-width" type="text" placeholder="Newegg" id="item_purchase_from" name="item_purchase_from">
</div>
<div class="six columns">
<label for="item_purchase_date">Purchased at:</label>
<input class="u-full-width" type="datetime-local" id="item_purchase_date" name="item_purchase_date">
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<div class="six columns"> <div class="six columns">
<label for="item_manufacturer">Manufacturer:</label> <label for="item_manufacturer">Manufacturer:</label>
<input class="u-full-width" type="text" placeholder="Manufacturer" id="item_manufacturer"> <input class="u-full-width" type="text" placeholder="Manufacturer" id="item_manufacturer" name="item_manufacturer">
</div> </div>
<div class="six columns"> <div class="six columns">
<label for="item_type">Item type</label> <label for="item_type">Item type</label>
<select class="u-full-width" id="item_type"> <select class="u-full-width" id="item_type" name="item_type">
<option value="cpu">Processor</option> <option value="cpu">Processor</option>
<option value="motherboard">Motherboard</option> <option value="motherboard">Motherboard</option>
<option value="memory">Memory (RAM)</option> <option value="memory">Memory (RAM)</option>
@ -41,7 +65,7 @@
</div> </div>
</div> </div>
<input class="button-primary" type="submit" value="Submit"> <input class="button-primary u-full-width" type="submit" value="Submit">
</form> </form>
</div> </div>
</section> </section>

73
views/item/edit.twig Normal file
View File

@ -0,0 +1,73 @@
{% extends 'layout.twig' %}
{% block title %}Edit Item{% endblock %}
{% block content %}
<!-- page header -->
<header class="row">
<div class="columns twelve">
<h1>Editing "{{ item.name }}"</h1>
</div>
</header>
<section class="row">
<div class="columns twelve">
<form action="/item/{{ item.id }}/edit" method="POST">
<div class="row">
<div class="columns twelve">
<label for="item_name">Item name:</label>
<input class="u-full-width" type="text" placeholder="My new item" id="item_name" name="item_name" value="{{ item.name }}" required>
</div>
</div>
<div class="row">
<div class="six columns">
<label for="item_serial">Serial number:</label>
<input class="u-full-width" type="text" placeholder="0123456789" id="item_serial" name="item_serial" value="{{ item.serialNumber }}">
</div>
<div class="six columns">
<label for="item_sku">SKU number:</label>
<input class="u-full-width" type="text" placeholder="ABC12345678" id="item_sku" name="item_sku" value="{{ item.skuNumber }}">
</div>
</div>
<div class="row">
<div class="six columns">
<label for="item_purchase_from">Purchased from:</label>
<input class="u-full-width" type="text" placeholder="Newegg" id="item_purchase_from" name="item_purchase_from" value="{{ item.purchasedFrom }}">
</div>
<div class="six columns">
<label for="item_purchase_date">Purchased at:</label>
<input class="u-full-width" type="datetime-local" id="item_purchase_date" name="item_purchase_date" value="{{ item.purchasedAt }}">
</div>
</div>
<div class="row">
<div class="six columns">
<label for="item_manufacturer">Manufacturer:</label>
<input class="u-full-width" type="text" placeholder="Manufacturer" id="item_manufacturer" name="item_manufacturer" value="{{ item.manufacturer }}">
</div>
<div class="six columns">
<label for="item_type">Item type</label>
<select class="u-full-width" id="item_type" name="item_type" value="{{ item.type }}">
<option value="cpu">Processor</option>
<option value="motherboard">Motherboard</option>
<option value="memory">Memory (RAM)</option>
<option value="psu">Power Supply</option>
<option value="case">Case</option>
<option value="storage">Storage Device</option>
<option value="gpu">Graphics Card</option>
</select>
</div>
</div>
<input class="button-primary u-full-width" type="submit" value="Submit">
</form>
</div>
</section>
{% endblock %}

47
views/item/view.twig Normal file
View File

@ -0,0 +1,47 @@
{% extends 'layout.twig' %}
{% block title %}{{ item.name }}{% endblock %}
{% block content %}
<!-- page header -->
<header id="item-header" class="row">
<div class="columns twelve">
<span>
<h1>{{ item.name }}</h1>
<p><a href="/item/{{ item.id }}/edit"><i class="fa-solid fa-pen-to-square"></i> Edit</a></p>
</span>
<h4 class="item-added-date">Item added at: {{ item.createdAt }}</h4>
<h4 class="item-updated-date">Last updated at: {{ item.updatedAt }}</h4>
</div>
</header>
<!-- item information -->
<section class="row">
<table class="columns twelve">
<thead>
<tr>
<th>Product name</th>
<th>Serial number</th>
<th>SKU Number</th>
<th>Manufacturer</th>
<th>Type</th>
<th>Seller</th>
<th>Purchase date</th>
</tr>
</thead>
<tbody>
<tr>
<td>{{ item.name }}</td>
<td>{{ item.serialNumber ? item.serialNumber : 'N/a' }}</td>
<td>{{ item.skuNumber ? item.skuNumber : 'N/a' }}</td>
<td>{{ item.manufacturer ? item.manufacturer : 'N/a' }}</td>
<td>{{ item.type }}</td>
<td>{{ item.purchasedFrom ? item.purchasedFrom : 'N/a' }}</td>
<td>{{ item.purchasedAt | date("m/d/Y h:i:s A") }}</td>
</tr>
</tbody>
</table>
</section>
{% endblock %}

View File

@ -25,7 +25,7 @@
</nav> </nav>
<!-- main content --> <!-- main content -->
<div id="main-content" class="container"> <div id="main-content" class="container fluid">
{% block content %}{% endblock %} {% block content %}{% endblock %}
</div> </div>
</body> </body>