folder names

This commit is contained in:
Jen Looper
2020-11-09 22:51:04 -05:00
parent 33e5d5f777
commit 1d81829ac1
367 changed files with 0 additions and 0 deletions

View File

@@ -0,0 +1,13 @@
# Bank app
> Example solution for the bank app project, built with vanilla HTML5, CSS and JavaScript (no frameworks or libraries used).
## Running the app
First make sure you have the [API server](../api/README.md) running.
Any web server can be used to run the app, but since you should have [Node.js](https://nodejs.org) installed anyway to run the API, you can:
1. Git clone this repo.
2. Open a terminal, then run `npx lite-server solution`. It will start a development web server on port `3000`
3. Open `http://localhost:3000` in a browser to run the app.

View File

@@ -0,0 +1,271 @@
// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------
const serverUrl = 'http://localhost:5000/api';
const storageKey = 'savedState';
// ---------------------------------------------------------------------------
// Router
// ---------------------------------------------------------------------------
const routes = {
'/dashboard': { title: 'My Account', templateId: 'dashboard', init: refresh },
'/login': { title: 'Login', templateId: 'login' }
};
function navigate(path) {
window.history.pushState({}, path, window.location.origin + path);
updateRoute();
}
function updateRoute() {
const path = window.location.pathname;
const route = routes[path];
if (!route) {
return navigate('/dashboard');
}
const template = document.getElementById(route.templateId);
const view = template.content.cloneNode(true);
const app = document.getElementById('app');
app.innerHTML = '';
app.appendChild(view);
if (typeof route.init === 'function') {
route.init();
}
document.title = route.title;
}
// ---------------------------------------------------------------------------
// API interactions
// ---------------------------------------------------------------------------
async function sendRequest(api, method, body) {
try {
const response = await fetch(serverUrl + api, {
method: method || 'GET',
headers: body ? { 'Content-Type': 'application/json' } : undefined,
body
});
return await response.json();
} catch (error) {
return { error: error.message || 'Unknown error' };
}
}
async function getAccount(user) {
return sendRequest('/accounts/' + encodeURIComponent(user));
}
async function createAccount(account) {
return sendRequest('/accounts', 'POST', account);
}
async function createTransaction(user, transaction) {
return sendRequest('/accounts/' + user + '/transactions', 'POST', transaction);
}
// ---------------------------------------------------------------------------
// Global state
// ---------------------------------------------------------------------------
let state = {
user: null,
account: null
};
function updateState(newState) {
state = newState;
localStorage.setItem(storageKey, JSON.stringify(state));
}
// ---------------------------------------------------------------------------
// Login/register
// ---------------------------------------------------------------------------
async function login() {
const loginForm = document.getElementById('loginForm')
const user = loginForm.user.value;
const data = await getAccount(user);
if (data.error) {
return updateElement('loginError', data.error);
}
const newState = {
...state,
user: data.user
};
updateState(newState);
navigate('/dashboard');
}
async function register() {
const registerForm = document.getElementById('registerForm');
const formData = new FormData(registerForm);
const data = Object.fromEntries(formData);
const jsonData = JSON.stringify(data);
const result = await createAccount(jsonData);
if (result.error) {
return updateElement('registerError', result.error);
}
const newState = {
...state,
user: result.user
};
updateState(newState);
navigate('/dashboard');
}
// ---------------------------------------------------------------------------
// Dashboard
// ---------------------------------------------------------------------------
async function updateAccountData() {
const user = state.user;
if (!user) {
return logout();
}
const data = await getAccount(user);
if (data.error) {
if (data.error === 'User does not exist') {
return logout();
}
return updateElement('dashboardError', data.error);
}
const newState = {
...state,
account: data
};
updateState(newState);
}
async function refresh() {
await updateAccountData();
updateDashboard();
}
function updateDashboard() {
const account = state.account;
if (!account) {
return logout();
}
updateElement('description', account.description);
updateElement('balance', account.balance.toFixed(2));
updateElement('currency', account.currency);
// Update transactions
const transactionsRows = document.createDocumentFragment();
for (const transaction of account.transactions) {
const transactionRow = createTransactionRow(transaction);
transactionsRows.appendChild(transactionRow);
}
updateElement('transactions', transactionsRows);
}
function createTransactionRow(transaction) {
const template = document.getElementById('transaction');
const transactionRow = template.content.cloneNode(true);
const tr = transactionRow.querySelector('tr');
tr.children[0].textContent = transaction.date;
tr.children[1].textContent = transaction.object;
tr.children[2].textContent = transaction.amount.toFixed(2);
return transactionRow;
}
function addTransaction() {
const dialog = document.getElementById('transactionDialog');
dialog.classList.add('show');
// Reset form
const transactionForm = document.getElementById('transactionForm');
transactionForm.reset();
// Set date to today
transactionForm.date.valueAsDate = new Date();
}
async function confirmTransaction() {
const dialog = document.getElementById('transactionDialog');
dialog.classList.remove('show');
const transactionForm = document.getElementById('transactionForm');
const formData = new FormData(transactionForm);
const jsonData = JSON.stringify(Object.fromEntries(formData));
const data = await createTransaction(state.user, jsonData);
if (data.error) {
return updateElement('transactionError', data.error);
}
// Update local state with new transaction
const newState = {
...state,
account: {
...state.account,
balance: state.account.balance + data.amount,
transactions: [...state.account.transactions, data]
}
}
updateState(newState);
// Update display
updateDashboard();
}
async function cancelTransaction() {
const dialog = document.getElementById('transactionDialog');
dialog.classList.remove('show');
}
function logout() {
const newState = {
user: null,
account: null
};
updateState(newState);
navigate('/login');
}
// ---------------------------------------------------------------------------
// Utils
// ---------------------------------------------------------------------------
function updateElement(id, textOrNode) {
const element = document.getElementById(id);
element.textContent = ''; // Removes all children
element.append(textOrNode);
}
// ---------------------------------------------------------------------------
// Init
// ---------------------------------------------------------------------------
function init() {
// Restore state
const savedState = localStorage.getItem(storageKey);
if (savedState) {
updateState(JSON.parse(savedState));
}
// Update route for browser back/next buttons
window.onpopstate = () => updateRoute();
updateRoute();
}
init();

View File

@@ -0,0 +1,110 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Squirrel Banking</title>
<link rel="stylesheet" href="styles.css">
<script src="app.js" defer></script>
</head>
<body>
<!-- Placeholder where we will insert our app HTML based on route -->
<div id="app">Loading...</div>
<!-- Login page template -->
<template id="login">
<section class="login-page">
<div class="login-container">
<div class="login-title text-center">
<span class="hide-xs">Squirrel</span>
<img class="login-logo" src="logo.svg" alt="Squirrel Banking Logo">
<span class="hide-xs">Banking</span>
</div>
<div class="login-content">
<h2 class="text-center">Login</h2>
<form id="loginForm" action="javascript:login()">
<label for="user">Username</label>
<input name="user" type="text" maxlength="20" required>
<div id="loginError" class="error"></div>
<button>Login</button>
</form>
<p class="login-separator text-center"><span>OR</span></p>
<h2 class="text-center">Register</h2>
<form id="registerForm" action="javascript:register()">
<label for="user">Username</label>
<input name="user" type="text" maxlength="20" required>
<label for="currency">Currency</label>
<input name="currency" type="text" maxlength="5" value="$" required>
<label for="description">Description</label>
<input name="description" type="text" maxlength="100">
<label for="balance">Current balance</label>
<input name="balance" type="number" value="0">
<div id="registerError" class="error"></div>
<button>Register</button>
</form>
</div>
</div>
</section>
</template>
<!-- Dashboard page template -->
<template id="dashboard">
<section class="dashboard-page">
<header class="dashboard-header">
<img class="dashboard-logo" src="logo.svg" alt="Squirrel Banking Logo">
<span class="dashboard-title hide-xs">Squirrel Banking</span>
<button onclick="logout()">Logout</button>
</header>
<div class="balance">
<div>Balance</div>
<span id="balance"></span>
<span id="currency"></span>
</div>
<div class="dashboard-content">
<div class="transactions-title">
<div id="description" aria-label="Account description"></div>
<button onclick="addTransaction()">Add transaction</button>
</div>
<table class="transactions-table" aria-label="Transactions">
<thead>
<tr>
<th>Date</th>
<th>Object</th>
<th>Amount</th>
</tr>
</thead>
<tbody id="transactions"></tbody>
</table>
<p id="dashboardError" class="error"></p>
</div>
</section>
<section id="transactionDialog" class="dialog">
<div class="dialog-content">
<h2 class="text-center">Add transaction</h2>
<form id="transactionForm" action="javascript:void(0)">
<label for="date">Date</label>
<input name="date" type="date" required>
<label for="object">Object</label>
<input name="object" type="text" maxlength="50" required>
<label for="amount">Amount (use negative value for debit)</label>
<input name="amount" type="number" value="0" step="any" required>
<div id="transactionError" class="error"></div>
<div class="dialog-buttons">
<button class="button-alt" formaction="javascript:cancelTransaction()" formnovalidate>Cancel</button>
<button formaction="javascript:confirmTransaction()">OK</button>
</div>
</form>
</div>
</section>
</template>
<!-- Transaction row template -->
<template id="transaction">
<tr>
<td></td>
<td></td>
<td></td>
</tr>
</template>
</body>
</html>

View File

@@ -0,0 +1 @@
<svg id="squirrel" data-name="squirrel" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 296.2 479.27"><defs><style>.cls-1{fill: #fff;}</style></defs><path class="cls-1" d="M216.54,17c-30.06,93.06,63.25,82.67,127,133.73,48.38,38.78,92.87,118,29,234.83-11.12-29-24.49-75.4-37.9-138.2-6.71-31.52-11.1-92.35-12.16-91.7-6.88,4.25-21,37-21,37s-2.43-.6-13.36-2.83S256,187,256,187s-21.09.6-32,2.83-13.36,2.83-13.36,2.83-14.16-32.78-21-37c-1.06-.65-5.45,60.18-12.17,91.7-6.93,32.5-15.7,65.2-23.28,91.19C79.26,248.12,92.5,102.73,216.54,17Zm85.82,254.1c3.84,0,7-6.14,7-13.71s-3.12-13.71-7-13.71-6.95,6.14-6.95,13.71,3.12,13.71,6.95,13.71Zm-85.71-13.71c0-7.57-3.12-13.71-7-13.71s-6.95,6.14-6.95,13.71,3.11,13.71,6.95,13.71,7-6.14,7-13.71Zm59,23.45c0-6.37-8.78-11.53-19.62-11.53s-19.62,5.16-19.62,11.53c0,5.88,7.52,10.74,17.24,11.43v8a2.39,2.39,0,0,0,2.38,2.39h0a2.39,2.39,0,0,0,2.38-2.39v-8c9.71-.69,17.24-5.55,17.24-11.43Zm-7.72,34.51c-2.6-.18-6.08,4.76-6.93,8.07s-4,26.07-4,26.07-22.8-5.74-38.06-6-23,5.77-28.09,13.74-2.22,18.75-.44,23a21.08,21.08,0,0,0,3.63,6.14,88.82,88.82,0,0,0-5,22.13c7.8,10.18,33.56,26,33.56,26s6.54,3.47,5.38,9.55-18,13.91-30.55,11.74c-.53-.09-1.09-.21-1.68-.35C206.45,478.28,227,496.27,227,496.27s40.86-2.08,64-21.89a100.57,100.57,0,0,0,8.1-7.77c-7-2.86-13.59-5.9-17.28-8.48-10.47-7.29-16.92-24.69-13.48-29.82s10.54-3,10.54-3,29.43,6.88,42.13,5.11a21.1,21.1,0,0,0,6.65-2.57c4-2.22,13-8.87,13.88-18.31S340.1,390.61,328,381.35s-33.59-18.88-33.59-18.88,11.6-19.83,13-23,1.7-9.17-.44-10.63-19.05-8.05-19.05-8.05S270.52,315.53,267.93,315.35Z" transform="translate(-108.03 -17)"/></svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -0,0 +1,351 @@
:root {
/* Colors */
--primary: #0091ea;
--primary-light: #7bbde6;
--accent: #546e7a;
--grey: #445;
--error: #f52;
--background: #f5f5f6;
--background-accent: #cfe5f2;
--white: #fff;
--border: #99a;
/* Sizes */
--radius: 10px;
--space-xs: 5px;
--space-sm: 10px;
--space-md: 20px;
--space-xl: 40px;
}
/* ------------------------------------------------------------------------ */
/* Micro reset */
/* ------------------------------------------------------------------------ */
* {
box-sizing: border-box;
}
html, body, #app {
margin: 0;
padding: 0;
height: 100%;
}
/* ------------------------------------------------------------------------ */
/* General styles */
/* ------------------------------------------------------------------------ */
body {
font-family: Arial, Helvetica, sans-serif;
font-size: 14px;
}
h2 {
color: var(--primary);
text-transform: uppercase;
font-weight: bold;
font-size: 1.5rem;
margin: var(--space-md) 0;
}
form {
display: flex;
flex-direction: column;
margin: var(--space-sm) var(--space-md);
}
input {
margin-top: var(--space-xs);
margin-bottom: var(--space-sm);
height: 40px;
padding: var(--space-xs) var(--space-sm);
border: 1px solid var(--border);
border-radius: var(--radius);
}
input:focus {
border-color: var(--primary);
outline: 0;
}
label {
color: var(--grey);
text-transform: uppercase;
font-size: 80%;
}
button {
font-weight: bold;
background-color: var(--primary);
color: var(--white);
height: 40px;
padding: var(--space-xs);
border: 0;
border-radius: var(--radius);
text-transform: uppercase;
min-width: 100px;
margin: var(--space-sm) 0;
}
.button-alt {
background-color: transparent;
color: var(--primary);
}
button:hover {
filter: brightness(115%);
cursor: pointer;
}
button:focus {
outline: none;
border: 2px solid var(--grey);
}
.error {
color: var(--error);
margin: var(--space-xs) 0;
}
.error:empty {
display: none;
}
/* ------------------------------------------------------------------------ */
/* Login page */
/* ------------------------------------------------------------------------ */
.login-title {
font-size: 2rem;
font-weight: bold;
color: var(--white);
margin: var(--space-md);
}
.login-logo {
height: 80px;
vertical-align: middle;
}
.login-page {
display: flex;
height: 100%;
align-items: center;
justify-content: center;
background: linear-gradient(var(--primary), var(--primary-light));
}
.login-container {
flex: auto;
max-width: 480px;
max-height: 100%;
overflow: auto;
}
.login-content {
background-color: var(--background);
padding: var(--space-sm);
}
.login-separator {
position: relative;
top: 0.5em;
border-top: 1px solid var(--border);
opacity: 0.6;
margin: var(--space-md) 0;
}
.login-separator > span {
position: relative;
top: -0.5em;
background-color: var(--background);
padding: var(--space-sm);
}
/* ------------------------------------------------------------------------ */
/* Dashboard page */
/* ------------------------------------------------------------------------ */
.dashboard-page {
display: flex;
height: 100%;
flex-direction: column;
}
.dashboard-header {
background-color: var(--grey);
padding: 0 var(--space-sm)
}
.dashboard-header button {
float: right;
border: 1px solid;
background-color: transparent;
}
.dashboard-title {
font-size: 1.5rem;
font-weight: bold;
color: var(--white);
vertical-align: middle;
margin: 0 var(--space-sm)
}
.dashboard-logo {
height: 60px;
vertical-align: middle;
padding: var(--space-xs);
}
.balance {
background: radial-gradient(circle at center, var(--primary), var(--primary-light));
text-align: center;
}
.balance > div {
color: var(--white);
padding-top: var(--space-xs);
text-transform: uppercase;
}
.balance > span {
color: var(--white);
font-size: 3rem;
}
.transactions-title {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 var(--space-sm);
color: var(--accent);
font-weight: bold;
font-size: 1.5rem;
}
.transactions-title > div {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.transactions-table {
width: 100%;
font-size: 1.2rem;
padding: var(--space-sm);
margin: 0;
border-spacing: 0;
background-color: var(--background);
}
.transactions-table thead th {
border-bottom: 1px solid var(--border);
}
.transactions-table tr:nth-child(even) {
background-color: var(--background-accent);
}
.transactions-table td,
.transactions-table th {
padding: var(--space-xs) var(--space-sm);
text-align: left;
}
.transactions-table td:first-child,
.transactions-table th:first-child {
/* Make first column use the minimum width */
width: 1%;
white-space: nowrap;
}
.transactions-table td:last-child,
.transactions-table th:last-child {
text-align: right;
}
.dialog {
display: none;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
left: 0;
top: 0;
overflow: auto;
background-color: rgba(0,0,0,0.4);
animation: slideFromTop 0.3s ease-in-out;
justify-content: center;
align-items: flex-start;
}
.dialog.show {
display: flex;
}
@keyframes slideFromTop {
from {
top: -300px;
opacity: 0;
}
to {
top: 0;
opacity: 1;
}
}
.dialog-content {
flex: auto;
background-color: var(--white);
max-width: 480px;
max-height: 100%;
padding: var(--space-sm);
}
.dialog-buttons {
text-align: right;
}
/* ------------------------------------------------------------------------ */
/* Utilities */
/* ------------------------------------------------------------------------ */
.text-center {
text-align: center;
}
.hide-xs {
display: none;
}
/* ------------------------------------------------------------------------ */
/* Responsive adaptations */
/* ------------------------------------------------------------------------ */
@media only screen and (min-width: 480px) {
.hide-xs {
display: initial;
}
.login-content,
.dialog-content {
border-radius: var(--radius);
}
.dialog-content {
margin-top: var(--space-xl);
}
}
@media only screen and (min-width: 768px) {
.transactions-table {
border-radius: var(--radius);
}
.dashboard-content {
width: 100%;
max-width: 768px;
align-self: center;
}
}

View File

@@ -0,0 +1,13 @@
# Aplicación bancaria
> Solución de ejemplo para el proyecto de la aplicación bancaria, construida con HTML5 vanilla, CSS y JavaScript (sin marcos ni bibliotecas).
## Ejecutando la aplicación
Primero asegúrese de tener el [servidor API](../api/README.md) en ejecución.
Se puede usar cualquier servidor web para ejecutar la aplicación, pero dado que debe tener [Node.js]https://nodejs.org) instalado de todos modos para ejecutar la API, puede:
1. Git clone este repositorio.
2. Abra una terminal, luego ejecute `npx lite-server solution`. Iniciará un servidor web de desarrollo en el puerto `3000`
3. Abra `http://localhost: 3000` en un navegador para ejecutar la aplicación.