feature/partials #59
|
@ -2,4 +2,4 @@ LIFEPEX_DB=sqlite://sqlite.db
|
||||||
LIFEPEX_BIND=127.0.0.1
|
LIFEPEX_BIND=127.0.0.1
|
||||||
LIFEPEX_BASE_URL=
|
LIFEPEX_BASE_URL=
|
||||||
LIFEPEX_SECRET=
|
LIFEPEX_SECRET=
|
||||||
LIFEPEX_ENV=
|
PORT=
|
15
README.md
15
README.md
|
@ -81,4 +81,17 @@ xdg-open ./public/doc/index.html
|
||||||
|
|
||||||
### Testing
|
### Testing
|
||||||
|
|
||||||
If you want to run the test, simply type `rake test` (you will need the startup env variable to be set first).
|
Generate first a specific configuration file
|
||||||
|
|
||||||
|
```
|
||||||
|
cp .env.local .env.test.local
|
||||||
|
editor .env.test.local # don't forget to set a new database !!!
|
||||||
|
```
|
||||||
|
|
||||||
|
Then init the database
|
||||||
|
|
||||||
|
```
|
||||||
|
LIFEPEX_ENV=test ./init/database.rb
|
||||||
|
```
|
||||||
|
|
||||||
|
Then if you want to run the test, simply type `rake test` (you will need the startup env variable to be set first).
|
||||||
|
|
34
public/js/ajax.js
Normal file
34
public/js/ajax.js
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
async function ajax({
|
||||||
|
method = "GET",
|
||||||
|
url = ".",
|
||||||
|
body = undefined,
|
||||||
|
headers = {},
|
||||||
|
on_success = () => {},
|
||||||
|
on_failure = () => {},
|
||||||
|
}) {
|
||||||
|
const xhttp = new XMLHttpRequest();
|
||||||
|
const return_on_sent = new Promise((resolve, reject) => {
|
||||||
|
try {
|
||||||
|
xhttp.onreadystatechange = () => {
|
||||||
|
if (xhttp.readyState == 4) {
|
||||||
|
const response_status = String(xhttp.status);
|
||||||
|
if (/2\d\d/.test(response_status)) {
|
||||||
|
resolve(on_success(xhttp.responseText, xhttp));
|
||||||
|
} else {
|
||||||
|
resolve(on_failure(xhttp.responseText, xhttp));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
reject(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
xhttp.open(method, url, true);
|
||||||
|
Object.keys(headers).forEach((header_key) => {
|
||||||
|
xhttp.setRequestHeader(header_key, headers[header_key]);
|
||||||
|
});
|
||||||
|
xhttp.send(body);
|
||||||
|
|
||||||
|
return return_on_sent;
|
||||||
|
}
|
11
public/js/error.js
Normal file
11
public/js/error.js
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
function flashError(message) {
|
||||||
|
const new_flash_error = document.createElement('p');
|
||||||
|
new_flash_error.classList.add("alert");
|
||||||
|
new_flash_error.classList.add("alert-danger");
|
||||||
|
new_flash_error.classList.add("alert-dismissible");
|
||||||
|
new_flash_error.classList.add("fade");
|
||||||
|
new_flash_error.classList.add("show");
|
||||||
|
new_flash_error.setAttribute("role", "alert");
|
||||||
|
new_flash_error.innerHTML = `${message} <button class=\"btn-close\" type=\"button\" data-bs-dismiss=\"alert\" aria-label=\"Close\" />`;
|
||||||
|
const flash = document.querySelectorAll('.flash')[0].appendChild(new_flash_error);
|
||||||
|
}
|
|
@ -17,10 +17,70 @@ function toggle(node) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", (_event) => {
|
Array.toObject = function (arr) {
|
||||||
const togglers = document.querySelectorAll('.pex-editor-toggler');
|
return arr.reduce((base, current) => {
|
||||||
|
base[current[0]] = current[1];
|
||||||
|
return base;
|
||||||
|
}, {});
|
||||||
|
};
|
||||||
|
|
||||||
togglers.forEach((t) => {
|
function __map__(cb = (e) => e) {
|
||||||
|
const arr = [];
|
||||||
|
for (i = 0; i < this.length; i++) {
|
||||||
|
arr.push(cb(this[i]));
|
||||||
|
}
|
||||||
|
return arr;
|
||||||
|
}
|
||||||
|
|
||||||
|
HTMLCollection.prototype.map = __map__;
|
||||||
|
NodeList.prototype.map = __map__;
|
||||||
|
NamedNodeMap.prototype.map = __map__;
|
||||||
|
HTMLInputElement.prototype.attributesObject = function () {
|
||||||
|
return Array.toObject(this.attributes.map((attr) => [attr.name, attr.value]));
|
||||||
|
};
|
||||||
|
HTMLFormElement.prototype.attributesObject = function () {
|
||||||
|
return Array.toObject(this.attributes.map((attr) => [attr.name, attr.value]));
|
||||||
|
};
|
||||||
|
|
||||||
|
function parseCookie(str) {
|
||||||
|
return str
|
||||||
|
.split(";")
|
||||||
|
.map((v) => v.split("="))
|
||||||
|
.reduce((acc, v) => {
|
||||||
|
acc[decodeURIComponent(v[0].trim())] = decodeURIComponent(v[1].trim());
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
function userpexValidation(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
const attributes = event.target.attributesObject();
|
||||||
|
|
||||||
|
const fields = event.target.childNodes.map().filter(e => e.nodeName == 'INPUT');
|
||||||
|
const fields_attributes = Array.toObject(fields.map(attr => [attr.name, attr.value]));
|
||||||
|
|
||||||
|
ajax({
|
||||||
|
method: fields_attributes["type"] == "-" ? "DELETE" : "POST",
|
||||||
|
url: `/api/user-pex/v1/pexs/${fields_attributes["id"]}/validation`,
|
||||||
|
body: JSON.stringify({ date: parseCookie(document.cookie).date, force_count_total: true }),
|
||||||
|
headers: { Accept: "application/json", "Content-Type": "application/json" },
|
||||||
|
on_success: (body) => {
|
||||||
|
const json_body = JSON.parse(body);
|
||||||
|
const text_target = event.target.parentNode.parentElement.childNodes[3];
|
||||||
|
text_target.textContent = String(json_body.count_total);
|
||||||
|
const should_hide_decrease_button = json_body.count_total == 0;
|
||||||
|
event.target.parentNode.parentNode.childNodes[2].childNodes[0].hidden = should_hide_decrease_button;
|
||||||
|
},
|
||||||
|
on_failure: (body, req) => {
|
||||||
|
flashError("Error JS#0001 while validating...");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener("DOMContentLoaded", (_event) => {
|
||||||
|
const editor_togglers = document.querySelectorAll('.pex-editor-toggler');
|
||||||
|
editor_togglers.forEach((t) => {
|
||||||
const name = t.attributes.name.value;
|
const name = t.attributes.name.value;
|
||||||
const pex_editor = document.querySelector(`.pex-editor[name="${name}"]`);
|
const pex_editor = document.querySelector(`.pex-editor[name="${name}"]`);
|
||||||
hide(pex_editor);
|
hide(pex_editor);
|
||||||
|
@ -29,4 +89,9 @@ document.addEventListener("DOMContentLoaded", (_event) => {
|
||||||
toggle(pex_editor);
|
toggle(pex_editor);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const userpexvalidation0 = document.querySelectorAll('.userpexvalidationvalue').map().filter(tag => tag.textContent == "0");
|
||||||
|
userpexvalidation0.forEach((tag) => {
|
||||||
|
tag.parentNode.parentNode.childNodes[2].childNodes[0].hidden = true;
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -15,16 +15,15 @@ function setupChart(...cumuls) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
document.addEventListener('DOMContentLoaded', async function () {
|
||||||
const xhttp = new XMLHttpRequest();
|
ajax({
|
||||||
xhttp.onreadystatechange = () => {
|
method: "GET",
|
||||||
if (xhttp.readyState == 4 && xhttp.status == 200) {
|
url: "/api/pex/v1/recap",
|
||||||
const json_output = JSON.parse(xhttp.responseText);
|
body: null,
|
||||||
|
headers: { Accept: "application/json" },
|
||||||
|
on_success: (body) => {
|
||||||
|
const json_output = JSON.parse(body);
|
||||||
setupChart(...json_output.pex_tables);
|
setupChart(...json_output.pex_tables);
|
||||||
}
|
},
|
||||||
};
|
});
|
||||||
|
|
||||||
xhttp.open("GET", "/api/pex/v1/recap", true);
|
|
||||||
xhttp.setRequestHeader("Accept", "application/json");
|
|
||||||
xhttp.send();
|
|
||||||
});
|
});
|
||||||
|
|
10
src/app.rb
10
src/app.rb
|
@ -18,8 +18,12 @@ require "active_support"
|
||||||
require "active_support/core_ext"
|
require "active_support/core_ext"
|
||||||
require "pry" # debug
|
require "pry" # debug
|
||||||
|
|
||||||
|
module LifePex
|
||||||
|
APP_ENV = ENV.fetch("LIFEPEX_ENV") { "development" }
|
||||||
|
end
|
||||||
|
|
||||||
require "dotenv"
|
require "dotenv"
|
||||||
Dotenv.load(".env.local", ".env")
|
Dotenv.load(".env.#{LifePex::APP_ENV}.local", ".env.local", ".env")
|
||||||
|
|
||||||
require_relative "./utils/url.rb"
|
require_relative "./utils/url.rb"
|
||||||
require_relative "./utils/boot_framework"
|
require_relative "./utils/boot_framework"
|
||||||
|
@ -42,7 +46,6 @@ module LifePex
|
||||||
SECRET = ENV["LIFEPEX_SECRET"]
|
SECRET = ENV["LIFEPEX_SECRET"]
|
||||||
CODE_VERSION = DB[:meta].first[:code_version]
|
CODE_VERSION = DB[:meta].first[:code_version]
|
||||||
CODE_DATE = DB[:meta].first[:code_date]
|
CODE_DATE = DB[:meta].first[:code_date]
|
||||||
APP_ENV = ENV.fetch("LIFEPEX_ENV") { "development" }
|
|
||||||
|
|
||||||
include LifePex::Utils::Url
|
include LifePex::Utils::Url
|
||||||
|
|
||||||
|
@ -71,7 +74,8 @@ class LifePex::App < Sinatra::Base
|
||||||
set :session_secret, LifePex::SECRET
|
set :session_secret, LifePex::SECRET
|
||||||
enable :sessions
|
enable :sessions
|
||||||
|
|
||||||
use Rack::Csrf, skip: ["*:/api*"] if ENV["LIFEPEX_ENV"] != "test"
|
# use Rack::Csrf, skip: ["POST:/api/*"], :raise => (LifePex::APP_ENV != "production") if LifePex::APP_ENV != "test"
|
||||||
|
set :protection, :except => :json_csrf if LifePex::APP_ENV != "test"
|
||||||
|
|
||||||
LifePex::Systems.constants
|
LifePex::Systems.constants
|
||||||
.filter { |system| system.to_s =~ /System$/ }
|
.filter { |system| system.to_s =~ /System$/ }
|
||||||
|
|
|
@ -4,11 +4,12 @@ module LifePex::Systems::ApiResponse
|
||||||
any.to_json
|
any.to_json
|
||||||
end
|
end
|
||||||
|
|
||||||
def api_response_entity(message = nil, entity_type = nil, entity = nil)
|
def api_response_entity(message = nil, entity_type = nil, entity = nil, **more)
|
||||||
api_response({
|
api_response({
|
||||||
"message" => message,
|
"message" => message,
|
||||||
"entity_type" => entity_type,
|
"entity_type" => entity_type,
|
||||||
entity_type => entity,
|
entity_type => entity,
|
||||||
|
**more,
|
||||||
}.compact)
|
}.compact)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -16,6 +17,6 @@ module LifePex::Systems::ApiResponse
|
||||||
halt(status, {
|
halt(status, {
|
||||||
message => message,
|
message => message,
|
||||||
**more,
|
**more,
|
||||||
})
|
}.to_json)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -26,12 +26,12 @@ class LifePex::Systems::PexSystem < LifePex::Systems::AuthSystem
|
||||||
end
|
end
|
||||||
|
|
||||||
get "/today", auth: [] do
|
get "/today", auth: [] do
|
||||||
cookies["date"] = "today"
|
cookies.set "date", { value: "today", httponly: false }
|
||||||
redirect "/"
|
redirect "/"
|
||||||
end
|
end
|
||||||
|
|
||||||
get "/yesterday", auth: [] do
|
get "/yesterday", auth: [] do
|
||||||
cookies["date"] = "yesterday"
|
cookies.set "date", { value: "yesterday", httponly: false }
|
||||||
redirect "/"
|
redirect "/"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -4,17 +4,33 @@ require_relative "./api_response"
|
||||||
class LifePex::Systems::UserPexSystem < LifePex::Systems::AuthSystem
|
class LifePex::Systems::UserPexSystem < LifePex::Systems::AuthSystem
|
||||||
include JSON::API
|
include JSON::API
|
||||||
include LifePex::Systems::ApiResponse
|
include LifePex::Systems::ApiResponse
|
||||||
|
set :protection, :except => [:frame_options, :json_csrf]
|
||||||
|
|
||||||
|
def date_input_convertor(date)
|
||||||
|
if date && Date.respond_to?(date)
|
||||||
|
Date.send date
|
||||||
|
elsif date == "yesterday"
|
||||||
|
Date.today - 1
|
||||||
|
else
|
||||||
|
begin
|
||||||
|
Date.parse date
|
||||||
|
rescue => _
|
||||||
|
Date.today
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
extend DocMyRoutes::Annotatable # included by PexSystem
|
extend DocMyRoutes::Annotatable # included by PexSystem
|
||||||
register Sinatra::Namespace # included by PexSystem
|
register Sinatra::Namespace # included by PexSystem
|
||||||
namespace '/api/user-pex/v1' do
|
namespace "/api/user-pex/v1" do
|
||||||
|
namespace "/pexs" do
|
||||||
|
|
||||||
namespace '/pexs' do
|
summary "Get the amount of user_pex for a given day"
|
||||||
|
produces "application/json"
|
||||||
summary 'Get the amount of user_pex for a given day'
|
|
||||||
produces 'application/json'
|
|
||||||
status_codes [200]
|
status_codes [200]
|
||||||
get "/:pex_id/amount/by-date/:date", auth: [], provides: 'json' do
|
parameter :id, required: true, type: "integer", in: "path"
|
||||||
|
parameter :date, required: true, type: "string", in: "path"
|
||||||
|
get "/:pex_id/amount/by-date/:date", auth: [], provides: "json" do
|
||||||
pex_id = params["pex_id"]
|
pex_id = params["pex_id"]
|
||||||
date = params["date"]
|
date = params["date"]
|
||||||
count = LifePex::UserPex.where(
|
count = LifePex::UserPex.where(
|
||||||
|
@ -22,47 +38,66 @@ class LifePex::Systems::UserPexSystem < LifePex::Systems::AuthSystem
|
||||||
pex_id: pex_id,
|
pex_id: pex_id,
|
||||||
created_at: date,
|
created_at: date,
|
||||||
).count
|
).count
|
||||||
api_response({ count: count, entity_type: 'user_pex' })
|
api_response({ count: count, entity_type: "user_pex" })
|
||||||
end
|
end
|
||||||
|
|
||||||
summary 'Create a new user_pex for a given day'
|
summary "Create a new user_pex for a given day"
|
||||||
produces 'application/json'
|
produces "application/json"
|
||||||
status_codes [200]
|
status_codes [200]
|
||||||
post "/:pex_id/validation", auth: [], provides: 'json' do
|
parameter :id, required: true, type: "integer", in: "path"
|
||||||
|
parameter :date, required: false, type: "string", in: "body"
|
||||||
|
parameter :force_count_total, required: false, type: "bool", in: "body", description: "if true, force the server to computes the total amount of validation compatible after the operation"
|
||||||
|
post "/:pex_id/validation", auth: [], provides: "json" do
|
||||||
pex_id = params["pex_id"]
|
pex_id = params["pex_id"]
|
||||||
date = json_params["date"] || Date.today.to_s
|
date = date_input_convertor(json_params["date"])
|
||||||
|
api_error(403, "You may not have created or access to this pex id") if LifePex::Pex.where(id: pex_id).select(:user_id).first&.user_id != current_user_id
|
||||||
user_pex = LifePex::UserPex.new(
|
user_pex = LifePex::UserPex.new(
|
||||||
user_id: current_user_id,
|
user_id: current_user_id,
|
||||||
pex_id: pex_id,
|
pex_id: pex_id,
|
||||||
created_at: date,
|
created_at: date,
|
||||||
).save
|
).save
|
||||||
|
count_total = LifePex::UserPex.where(
|
||||||
|
user_id: current_user_id,
|
||||||
|
pex_id: pex_id,
|
||||||
|
created_at: date,
|
||||||
|
).count if json_params["force_count_total"] == true
|
||||||
api_response_entity(
|
api_response_entity(
|
||||||
"Successfuly added one user_pex",
|
"Successfuly added one user_pex",
|
||||||
"user_pex",
|
"user_pex",
|
||||||
user_pex,
|
user_pex,
|
||||||
|
count_total: count_total,
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
summary 'Remove an existing user_pex for a given day'
|
summary "Remove an existing user_pex for a given day"
|
||||||
produces 'application/json'
|
produces "application/json"
|
||||||
status_codes [200]
|
status_codes [200]
|
||||||
delete "/:pex_id/validation", auth: [], provides: 'json' do
|
parameter :id, required: true, type: "integer", in: "path"
|
||||||
|
parameter :date, required: false, type: "string", in: "body"
|
||||||
|
parameter :force_count_total, required: false, type: "bool", in: "body", description: "if true, force the server to computes the total amount of validation compatible after the operation"
|
||||||
|
delete "/:pex_id/validation", auth: [], provides: "json" do
|
||||||
pex_id = params["pex_id"]
|
pex_id = params["pex_id"]
|
||||||
date = json_params["date"] || Date.today.to_s
|
date = date_input_convertor(json_params["date"])
|
||||||
user_pex = LifePex::UserPex.find(
|
user_pex = LifePex::UserPex.where(
|
||||||
user_id: current_user_id,
|
user_id: current_user_id,
|
||||||
pex_id: pex_id,
|
pex_id: pex_id,
|
||||||
created_at: date,
|
created_at: date,
|
||||||
)
|
).select(:id).first
|
||||||
if user_pex
|
if user_pex
|
||||||
user_pex.destroy
|
user_pex.destroy
|
||||||
|
count_total = LifePex::UserPex.where(
|
||||||
|
user_id: current_user_id,
|
||||||
|
pex_id: pex_id,
|
||||||
|
created_at: date,
|
||||||
|
).count if json_params["force_count_total"] == true
|
||||||
api_response({
|
api_response({
|
||||||
message: "Successfuly destroyed 1 user_pex",
|
message: "Successfuly destroyed 1 user_pex",
|
||||||
entity_type: "user_pex",
|
entity_type: "user_pex",
|
||||||
count: 1,
|
count_destroyed: 1,
|
||||||
|
count_total: count_total,
|
||||||
})
|
})
|
||||||
else
|
else
|
||||||
api_error(400, "Nothing to destroy")
|
api_error(404, "Nothing to destroy")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -47,17 +47,17 @@
|
||||||
| u
|
| u
|
||||||
td.col-8=pex_by_model[:name]
|
td.col-8=pex_by_model[:name]
|
||||||
td.col-1.center
|
td.col-1.center
|
||||||
- if pex_by_model[:user_pexs][:amount] > 0
|
form.userpexvalidation.userpexvalidationdecrease method="POST" action="/" onsubmit="return userpexValidation(event)"
|
||||||
form method="POST" action="/"
|
== csrf_tag
|
||||||
== csrf_tag
|
input type="hidden" name="id" value=pex_by_model[:id]
|
||||||
input type="hidden" name="id" value=pex_by_model[:id]
|
input type="hidden" name="type" value="-"
|
||||||
input type="hidden" name="type" value="-"
|
button.btn.force-1-col type="submit" style="display: block;"
|
||||||
button.btn.force-1-col type="submit" style="display: block;"
|
| -
|
||||||
| -
|
|
||||||
td.col-1.center
|
td.col-1.center
|
||||||
=pex_by_model[:user_pexs][:amount]
|
.userpexvalidation.userpexvalidationvalue
|
||||||
|
=pex_by_model[:user_pexs][:amount]
|
||||||
td.col-1.center
|
td.col-1.center
|
||||||
form method="POST" action="/"
|
form.userpexvalidation.userpexvalidationincrease method="POST" action="/" onsubmit="return userpexValidation(event)"
|
||||||
== csrf_tag
|
== csrf_tag
|
||||||
input type="hidden" name="id" value=pex_by_model[:id]
|
input type="hidden" name="id" value=pex_by_model[:id]
|
||||||
input type="hidden" name="type" value="+"
|
input type="hidden" name="type" value="+"
|
||||||
|
|
|
@ -2,7 +2,8 @@ doctype html
|
||||||
html lang="en"
|
html lang="en"
|
||||||
head
|
head
|
||||||
/! Required meta tags
|
/! Required meta tags
|
||||||
title Life Pex
|
title
|
||||||
|
| Life Pex
|
||||||
meta charset="utf-8" /
|
meta charset="utf-8" /
|
||||||
meta content="width=device-width, initial-scale=1" name="viewport" /
|
meta content="width=device-width, initial-scale=1" name="viewport" /
|
||||||
/! Bootstrap CSS
|
/! Bootstrap CSS
|
||||||
|
@ -46,16 +47,6 @@ html lang="en"
|
||||||
li.nav-item
|
li.nav-item
|
||||||
a.btn.btn-lg.btn-dark href="/logout" Logout
|
a.btn.btn-lg.btn-dark href="/logout" Logout
|
||||||
|
|
||||||
/ li.nav-item.dropdown
|
|
||||||
/ a.btn.btn-lg.btn-dark.nav-link.dropdown-toggle#nav-drop-more href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false"
|
|
||||||
/ | More
|
|
||||||
/ ul.dropdown-menu aria-labelledby="nav-drop-more"
|
|
||||||
/ li
|
|
||||||
/ a.btn.btn-lg.btn-dark href="/password" Change password
|
|
||||||
/ li
|
|
||||||
/ a.btn.btn-lg.btn-dark href="/about" About lifepex
|
|
||||||
/ li
|
|
||||||
/ a.btn.btn-lg.btn-dark href="/logout" Logout
|
|
||||||
- else
|
- else
|
||||||
li.nav-item
|
li.nav-item
|
||||||
a.btn.btn-lg.btn-dark href="/login" Login
|
a.btn.btn-lg.btn-dark href="/login" Login
|
||||||
|
@ -65,7 +56,7 @@ html lang="en"
|
||||||
a.btn.btn-lg.btn-dark href="/about" About lifepex
|
a.btn.btn-lg.btn-dark href="/about" About lifepex
|
||||||
|
|
||||||
|
|
||||||
.flash
|
#flash.flash
|
||||||
- if defined? flash
|
- if defined? flash
|
||||||
- flash.each do |flash_name, flash_message|
|
- flash.each do |flash_name, flash_message|
|
||||||
.alert.alert-dismissible.fade.show role="alert" class="alert-#{flash_name}"
|
.alert.alert-dismissible.fade.show role="alert" class="alert-#{flash_name}"
|
||||||
|
@ -81,3 +72,5 @@ html lang="en"
|
||||||
/ script src="https://code.jquery.com/jquery-3.4.1.slim.min.js" integrity="sha384-J6qa4849blE2+poT4WnyKhv5vZF5SrPo0iEjwBvKU7imGFAV0wwj1yYfoRSJoZ+n" crossorigin="anonymous"
|
/ script src="https://code.jquery.com/jquery-3.4.1.slim.min.js" integrity="sha384-J6qa4849blE2+poT4WnyKhv5vZF5SrPo0iEjwBvKU7imGFAV0wwj1yYfoRSJoZ+n" crossorigin="anonymous"
|
||||||
script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"
|
script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"
|
||||||
script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta3/dist/js/bootstrap.bundle.min.js" integrity="sha384-JEW9xMcG8R+pH31jmWH6WWP0WintQrMb4s7ZOdauHnUtxwoG2vI5DkLtS3qm9Ekf" crossorigin="anonymous"
|
script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta3/dist/js/bootstrap.bundle.min.js" integrity="sha384-JEW9xMcG8R+pH31jmWH6WWP0WintQrMb4s7ZOdauHnUtxwoG2vI5DkLtS3qm9Ekf" crossorigin="anonymous"
|
||||||
|
script src="/js/error.js"
|
||||||
|
script src="/js/ajax.js"
|
||||||
|
|
Loading…
Reference in New Issue
Block a user