WIP on vanilla PHP framework
This commit is contained in:
parent
24c503385e
commit
41853a7645
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -256,3 +256,6 @@ paket-files/
|
|||
.ionide
|
||||
|
||||
src/environment.txt
|
||||
|
||||
# PHP ignore files
|
||||
src/vendor
|
||||
|
|
|
@ -8,9 +8,4 @@ myPrayerJournal was borne of out of a personal desire [Daniel](https://github.co
|
|||
|
||||
## Further Reading
|
||||
|
||||
The documentation for the site is at <https://bit-badger.github.io/myPrayerJournal/>.
|
||||
|
||||
---
|
||||
_Thanks to [JetBrains](https://jb.gg/OpenSource) for licensing their awesome toolset to this project._
|
||||
|
||||
[<img src="https://resources.jetbrains.com/storage/products/company/brand/logos/jb_beam.png" alt="JetBrains Logo (Main) logo" width="100" height="100">](https://jb.gg/OpenSource)
|
||||
The documentation for the site is at <https://prayerjournal.me/docs>.
|
||||
|
|
16
src/composer.json
Normal file
16
src/composer.json
Normal file
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"name": "bit-badger/my-prayer-journal",
|
||||
"minimum-stability": "dev",
|
||||
"require": {
|
||||
"php": ">=8.2",
|
||||
"ext-pdo": "*",
|
||||
"ext-sqlite3": "*",
|
||||
"bit-badger/pdo-document": "^1",
|
||||
"visus/cuid2": "^4"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"MyPrayerJournal\\": "lib/"
|
||||
}
|
||||
}
|
||||
}
|
182
src/composer.lock
generated
Normal file
182
src/composer.lock
generated
Normal file
|
@ -0,0 +1,182 @@
|
|||
{
|
||||
"_readme": [
|
||||
"This file locks the dependencies of your project to a known state",
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "aa94d32d1f21c70c88434971a9dba5a8",
|
||||
"packages": [
|
||||
{
|
||||
"name": "bit-badger/pdo-document",
|
||||
"version": "v1.0.0-alpha2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://git.bitbadger.solutions/bit-badger/pdo-document",
|
||||
"reference": "330e27218756df8b93081a17dead8aaec789b071"
|
||||
},
|
||||
"require": {
|
||||
"ext-pdo": "*",
|
||||
"netresearch/jsonmapper": "^4",
|
||||
"php": ">=8.2"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^11"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"BitBadger\\PDODocument\\": "./src",
|
||||
"BitBadger\\PDODocument\\Query\\": "./src/Query",
|
||||
"BitBadger\\PDODocument\\Mapper\\": "./src/Mapper"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Daniel J. Summers",
|
||||
"email": "daniel@bitbadger.solutions",
|
||||
"homepage": "https://bitbadger.solutions",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"description": "Treat SQLite (and soon PostgreSQL) as a document store",
|
||||
"keywords": [
|
||||
"database",
|
||||
"document",
|
||||
"pdo",
|
||||
"sqlite"
|
||||
],
|
||||
"support": {
|
||||
"email": "daniel@bitbadger.solutions",
|
||||
"rss": "https://git.bitbadger.solutions/bit-badger/pdo-document.rss",
|
||||
"source": "https://git.bitbadger.solutions/bit-badger/pdo-document"
|
||||
},
|
||||
"time": "2024-06-11T11:07:56+00:00"
|
||||
},
|
||||
{
|
||||
"name": "netresearch/jsonmapper",
|
||||
"version": "v4.4.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/cweiske/jsonmapper.git",
|
||||
"reference": "132c75c7dd83e45353ebb9c6c9f591952995bbf0"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/cweiske/jsonmapper/zipball/132c75c7dd83e45353ebb9c6c9f591952995bbf0",
|
||||
"reference": "132c75c7dd83e45353ebb9c6c9f591952995bbf0",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-json": "*",
|
||||
"ext-pcre": "*",
|
||||
"ext-reflection": "*",
|
||||
"ext-spl": "*",
|
||||
"php": ">=7.1"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "~7.5 || ~8.0 || ~9.0 || ~10.0",
|
||||
"squizlabs/php_codesniffer": "~3.5"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-0": {
|
||||
"JsonMapper": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"OSL-3.0"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Christian Weiske",
|
||||
"email": "cweiske@cweiske.de",
|
||||
"homepage": "http://github.com/cweiske/jsonmapper/",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"description": "Map nested JSON structures onto PHP classes",
|
||||
"support": {
|
||||
"email": "cweiske@cweiske.de",
|
||||
"issues": "https://github.com/cweiske/jsonmapper/issues",
|
||||
"source": "https://github.com/cweiske/jsonmapper/tree/v4.4.1"
|
||||
},
|
||||
"time": "2024-01-31T06:18:54+00:00"
|
||||
},
|
||||
{
|
||||
"name": "visus/cuid2",
|
||||
"version": "4.1.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/visus-io/php-cuid2.git",
|
||||
"reference": "17c9b3098d556bb2556a084c948211333cc19c79"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/visus-io/php-cuid2/zipball/17c9b3098d556bb2556a084c948211333cc19c79",
|
||||
"reference": "17c9b3098d556bb2556a084c948211333cc19c79",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^8.1"
|
||||
},
|
||||
"require-dev": {
|
||||
"ergebnis/composer-normalize": "^2.29",
|
||||
"ext-ctype": "*",
|
||||
"phpstan/phpstan": "^1.9",
|
||||
"phpunit/phpunit": "^10.0",
|
||||
"squizlabs/php_codesniffer": "^3.7",
|
||||
"vimeo/psalm": "^5.4"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-gmp": "*"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"files": [
|
||||
"src/compat.php"
|
||||
],
|
||||
"psr-4": {
|
||||
"Visus\\Cuid2\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Alan Brault",
|
||||
"email": "alan.brault@visus.io"
|
||||
}
|
||||
],
|
||||
"description": "A PHP library for generating collision-resistant ids (CUIDs).",
|
||||
"keywords": [
|
||||
"cuid",
|
||||
"identifier"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/visus-io/php-cuid2/issues",
|
||||
"source": "https://github.com/visus-io/php-cuid2/tree/4.1.0"
|
||||
},
|
||||
"time": "2024-05-14T13:23:35+00:00"
|
||||
}
|
||||
],
|
||||
"packages-dev": [],
|
||||
"aliases": [],
|
||||
"minimum-stability": "dev",
|
||||
"stability-flags": [],
|
||||
"prefer-stable": false,
|
||||
"prefer-lowest": false,
|
||||
"platform": {
|
||||
"php": ">=8.2",
|
||||
"ext-pdo": "*",
|
||||
"ext-sqlite3": "*"
|
||||
},
|
||||
"platform-dev": [],
|
||||
"plugin-api-version": "2.6.0"
|
||||
}
|
16
src/lib/History.php
Normal file
16
src/lib/History.php
Normal file
|
@ -0,0 +1,16 @@
|
|||
<?php declare(strict_types=1);
|
||||
|
||||
namespace MyPrayerJournal;
|
||||
|
||||
/**
|
||||
* A record of an action taken on a request
|
||||
*/
|
||||
class History
|
||||
{
|
||||
/**
|
||||
* @param string $asOf The date/time this entry was made
|
||||
* @param RequestAction $action The action taken for this history entry
|
||||
* @param string|null $text The text for this history entry (optional)
|
||||
*/
|
||||
public function __construct(public string $asOf, public RequestAction $action, public ?string $text = null) { }
|
||||
}
|
15
src/lib/Note.php
Normal file
15
src/lib/Note.php
Normal file
|
@ -0,0 +1,15 @@
|
|||
<?php declare(strict_types=1);
|
||||
|
||||
namespace MyPrayerJournal;
|
||||
|
||||
/**
|
||||
* A note entered on a prayer request
|
||||
*/
|
||||
class Note
|
||||
{
|
||||
/**
|
||||
* @param string $asOf The date/time this note was recorded
|
||||
* @param string $text The text of the note
|
||||
*/
|
||||
public function __construct(public string $asOf, public string $text) { }
|
||||
}
|
15
src/lib/Recurrence.php
Normal file
15
src/lib/Recurrence.php
Normal file
|
@ -0,0 +1,15 @@
|
|||
<?php declare(strict_types=1);
|
||||
|
||||
namespace MyPrayerJournal;
|
||||
|
||||
/**
|
||||
* The recurrence for a prayer request
|
||||
*/
|
||||
class Recurrence
|
||||
{
|
||||
/**
|
||||
* @param RecurrencePeriod $period The recurrence period
|
||||
* @param int|null $interval How many of the periods will pass before the request is visible again
|
||||
*/
|
||||
public function __construct(public RecurrencePeriod $period, public ?int $interval = null) { }
|
||||
}
|
21
src/lib/RecurrencePeriod.php
Normal file
21
src/lib/RecurrencePeriod.php
Normal file
|
@ -0,0 +1,21 @@
|
|||
<?php declare(strict_types=1);
|
||||
|
||||
namespace MyPrayerJournal;
|
||||
|
||||
/**
|
||||
* The type of recurrence a request can have
|
||||
*/
|
||||
enum RecurrencePeriod
|
||||
{
|
||||
/** Requests, once prayed, are available again immediately */
|
||||
case Immediate;
|
||||
|
||||
/** Requests, once prayed, appear again in a number of hours */
|
||||
case Hours;
|
||||
|
||||
/** Requests, once prayed, appear again in a number of days */
|
||||
case Days;
|
||||
|
||||
/** Requests, once prayed, appear again in a number of weeks */
|
||||
case Weeks;
|
||||
}
|
33
src/lib/Request.php
Normal file
33
src/lib/Request.php
Normal file
|
@ -0,0 +1,33 @@
|
|||
<?php declare(strict_types=1);
|
||||
|
||||
namespace MyPrayerJournal;
|
||||
|
||||
use Exception;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
|
||||
/**
|
||||
* A prayer request
|
||||
*/
|
||||
class Request
|
||||
{
|
||||
/**
|
||||
* @param string $id The ID for the request
|
||||
* @param string $enteredOn The date/time this request was originally entered
|
||||
* @param string $userId The ID of the user to whom this request belongs
|
||||
* @param string|null $snoozedUntil The date/time the snooze expires for this request (null = not snoozed)
|
||||
* @param string|null $showAfter The date/time the current recurrence period is over (null = immediate)
|
||||
* @param Recurrence $recurrence The recurrence for this request
|
||||
* @param array|History[] $history The history of this request
|
||||
* @param array|Note[] $notes Notes regarding this request
|
||||
* @throws Exception If the ID generation fails
|
||||
*/
|
||||
public function __construct(public string $id = '', public string $enteredOn = '', public string $userId = '',
|
||||
public ?string $snoozedUntil = null, public ?string $showAfter = null,
|
||||
public Recurrence $recurrence = new Recurrence(RecurrencePeriod::Immediate),
|
||||
public array $history = [], public array $notes = [])
|
||||
{
|
||||
if ($id == '') {
|
||||
$this->id = (new Cuid2())->toString();
|
||||
}
|
||||
}
|
||||
}
|
21
src/lib/RequestAction.php
Normal file
21
src/lib/RequestAction.php
Normal file
|
@ -0,0 +1,21 @@
|
|||
<?php declare(strict_types=1);
|
||||
|
||||
namespace MyPrayerJournal;
|
||||
|
||||
/**
|
||||
* An action taken on a prayer request
|
||||
*/
|
||||
enum RequestAction
|
||||
{
|
||||
/** The request was created */
|
||||
case Created;
|
||||
|
||||
/** The request was marked as having been prayed for */
|
||||
case Prayed;
|
||||
|
||||
/** The request was updated */
|
||||
case Updated;
|
||||
|
||||
/** The request was marked as answered */
|
||||
case Answered;
|
||||
}
|
15
src/public/index.php
Normal file
15
src/public/index.php
Normal file
|
@ -0,0 +1,15 @@
|
|||
<?php declare(strict_types=1);
|
||||
|
||||
require '../start.php';
|
||||
|
||||
html_head('Welcome'); ?>
|
||||
<article class="container mt-3">
|
||||
<p>
|
||||
<p>myPrayerJournal is a place where individuals can record their prayer requests, record that they prayed for them,
|
||||
update them as God moves in the situation, and record a final answer received on that request. It also allows
|
||||
individuals to review their answered prayers.
|
||||
<p>This site is open and available for anyone who wants to use it. To get started, simply click the “Log
|
||||
On” link above, and log on with either a Microsoft or Google account. You can also learn more about the
|
||||
site at the “Docs” link, also above.
|
||||
</article><?php
|
||||
html_foot();
|
7
src/public/script/bootstrap.bundle.min.js
vendored
Normal file
7
src/public/script/bootstrap.bundle.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
src/public/script/htmx.min.js
vendored
Normal file
1
src/public/script/htmx.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
104
src/public/script/mpj.js
Normal file
104
src/public/script/mpj.js
Normal file
|
@ -0,0 +1,104 @@
|
|||
"use strict"
|
||||
|
||||
/** myPrayerJournal script */
|
||||
this.mpj = {
|
||||
/**
|
||||
* Show a message via toast
|
||||
* @param {string} message The message to show
|
||||
*/
|
||||
showToast (message) {
|
||||
const [level, msg] = message.split("|||")
|
||||
|
||||
let header
|
||||
if (level !== "success") {
|
||||
const heading = typ => `<span class="me-auto"><strong>${typ.toUpperCase()}</strong></span>`
|
||||
|
||||
header = document.createElement("div")
|
||||
header.className = "toast-header"
|
||||
header.innerHTML = heading(level === "warning" ? level : "error")
|
||||
|
||||
const close = document.createElement("button")
|
||||
close.type = "button"
|
||||
close.className = "btn-close"
|
||||
close.setAttribute("data-bs-dismiss", "toast")
|
||||
close.setAttribute("aria-label", "Close")
|
||||
header.appendChild(close)
|
||||
}
|
||||
|
||||
const body = document.createElement("div")
|
||||
body.className = "toast-body"
|
||||
body.innerText = msg
|
||||
|
||||
const toastEl = document.createElement("div")
|
||||
toastEl.className = `toast bg-${level === "error" ? "danger" : level} text-white`
|
||||
toastEl.setAttribute("role", "alert")
|
||||
toastEl.setAttribute("aria-live", "assertlive")
|
||||
toastEl.setAttribute("aria-atomic", "true")
|
||||
toastEl.addEventListener("hidden.bs.toast", e => e.target.remove())
|
||||
if (header) toastEl.appendChild(header)
|
||||
|
||||
toastEl.appendChild(body)
|
||||
document.getElementById("toasts").appendChild(toastEl)
|
||||
new bootstrap.Toast(toastEl, { autohide: level === "success" }).show()
|
||||
},
|
||||
/**
|
||||
* Load local version of Bootstrap CSS if the CDN load failed
|
||||
*/
|
||||
ensureCss () {
|
||||
let loaded = false
|
||||
for (let i = 0; !loaded && i < document.styleSheets.length; i++) {
|
||||
loaded = document.styleSheets[i].href.endsWith("bootstrap.min.css")
|
||||
}
|
||||
if (!loaded) {
|
||||
const css = document.createElement("link")
|
||||
css.rel = "stylesheet"
|
||||
css.href = "/style/bootstrap.min.css"
|
||||
document.getElementsByTagName("head")[0].appendChild(css)
|
||||
}
|
||||
},
|
||||
/** Script for the request edit component */
|
||||
edit: {
|
||||
/**
|
||||
* Toggle the recurrence input fields
|
||||
* @param {Event} e The click event
|
||||
*/
|
||||
toggleRecurrence ({ target }) {
|
||||
const isDisabled = target.value === "Immediate"
|
||||
;["recurCount", "recurInterval"].forEach(it => document.getElementById(it).disabled = isDisabled)
|
||||
}
|
||||
},
|
||||
/**
|
||||
* The time zone of the current browser
|
||||
* @type {string}
|
||||
**/
|
||||
timeZone: undefined,
|
||||
/**
|
||||
* Derive the time zone from the current browser
|
||||
*/
|
||||
deriveTimeZone () {
|
||||
try {
|
||||
this.timeZone = (new Intl.DateTimeFormat()).resolvedOptions().timeZone
|
||||
} catch (_) { }
|
||||
}
|
||||
}
|
||||
|
||||
htmx.on("htmx:afterOnLoad", function (evt) {
|
||||
const hdrs = evt.detail.xhr.getAllResponseHeaders()
|
||||
// Show a message if there was one in the response
|
||||
if (hdrs.indexOf("x-toast") >= 0) {
|
||||
mpj.showToast(evt.detail.xhr.getResponseHeader("x-toast"))
|
||||
}
|
||||
// Hide a modal window if requested
|
||||
if (hdrs.indexOf("x-hide-modal") >= 0) {
|
||||
document.getElementById(evt.detail.xhr.getResponseHeader("x-hide-modal") + "Dismiss").click()
|
||||
}
|
||||
})
|
||||
|
||||
htmx.on("htmx:configRequest", function (evt) {
|
||||
// Send the user's current time zone so that we can display local time
|
||||
if (mpj.timeZone) {
|
||||
evt.detail.headers["X-Time-Zone"] = mpj.timeZone
|
||||
}
|
||||
})
|
||||
|
||||
mpj.deriveTimeZone()
|
57
src/public/style/style.css
Normal file
57
src/public/style/style.css
Normal file
|
@ -0,0 +1,57 @@
|
|||
|
||||
nav {
|
||||
background-color: green;
|
||||
}
|
||||
nav .m {
|
||||
font-weight: 100;
|
||||
}
|
||||
nav .p {
|
||||
font-weight: 400;
|
||||
}
|
||||
nav .j {
|
||||
font-weight: 700;
|
||||
}
|
||||
.nav-item a:link,
|
||||
.nav-item a:visited {
|
||||
padding: .5rem 1rem;
|
||||
margin: 0 .5rem;
|
||||
border-radius: .5rem;
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
}
|
||||
.nav-item a:hover {
|
||||
cursor: pointer;
|
||||
background-color: rgba(255, 255, 255, .2);
|
||||
}
|
||||
.nav-item a.is-active-route {
|
||||
border-top-left-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
border-top: solid 4px rgba(255, 255, 255, .3);
|
||||
}
|
||||
|
||||
form {
|
||||
max-width: 60rem;
|
||||
margin: auto;
|
||||
}
|
||||
.action-cell .material-icons {
|
||||
font-size: 1.1rem ;
|
||||
}
|
||||
.material-icons {
|
||||
vertical-align: bottom;
|
||||
}
|
||||
#toastHost {
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
}
|
||||
.request-text {
|
||||
white-space: pre-line
|
||||
}
|
||||
|
||||
footer {
|
||||
border-top: solid 1px lightgray;
|
||||
margin: 1rem -1rem 0;
|
||||
padding: 0 1rem;
|
||||
}
|
||||
footer p {
|
||||
margin: 0;
|
||||
}
|
55
src/start.php
Normal file
55
src/start.php
Normal file
|
@ -0,0 +1,55 @@
|
|||
<?php declare(strict_types=1);
|
||||
|
||||
const MPJ_VERSION = '4.0.0';
|
||||
|
||||
function html_head(string $title): void
|
||||
{ ?>
|
||||
<!DOCTYPE html>
|
||||
<head lang=en>
|
||||
<meta name=viewport content="width=device-width, initial-scale=1">
|
||||
<meta name=description content="Online prayer journal - free w/Google or Microsoft account">
|
||||
<title><?=$title?> « myPrayerJournal</title>
|
||||
<link href=https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css rel=stylesheet
|
||||
integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin=anonymous>
|
||||
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel=stylesheet>
|
||||
<link href=/style/style.css rel=stylesheet>
|
||||
</head><?php
|
||||
}
|
||||
|
||||
function page_link(string $href, string $text, array $attrs = []): void
|
||||
{ ?>
|
||||
<a href="<?=$href?>" hx-get="<?=$href?>" hx-target="#top" hx-swap=innerHTML hx-push-url=true<?php
|
||||
foreach ($attrs as $key => $value) echo " $key=\"" . htmlspecialchars($value) . "\""; ?>><?=$text?></a><?php
|
||||
}
|
||||
|
||||
function html_foot(): void
|
||||
{
|
||||
$version = MPJ_VERSION;
|
||||
while (!str_ends_with($version, '.0')) $version = substr($version, 0, strlen($version) - 2); ?>
|
||||
<footer class=container-fluid>
|
||||
<p class="text-muted text-end">
|
||||
myPrayerJournal <?=$version?><br>
|
||||
<em>
|
||||
<small><?php
|
||||
page_link('/legal/privacy-policy', 'Privacy Policy');
|
||||
echo ' • ';
|
||||
page_link('/legal/terms-of-service', 'Terms of Service');
|
||||
echo ' • '; ?>
|
||||
<a href=https://git.bitbadger.solutions/bit-badger/myPrayerJournal target=_blank
|
||||
rel=noopener>Developed</a> and hosted by
|
||||
<a href=https://bitbadger.solutions target=_blank rel=noopener>Bit Badger Solutions</a>
|
||||
</small>
|
||||
</em>
|
||||
Htmx.Script.minified
|
||||
<script>if (!htmx) document.write('<script src=\"/script/htmx.min.js\"><\/script>')</script>
|
||||
<script async src=https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js
|
||||
integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL"
|
||||
crossorigin=anonymous></script>
|
||||
<script>
|
||||
setTimeout(function () {
|
||||
if (!bootstrap) document.write('<script src=\"/script/bootstrap.bundle.min.js\"><\/script>')
|
||||
}, 2000)
|
||||
</script>
|
||||
<script src=/script/mpj.js></script>
|
||||
</footer><?php
|
||||
}
|
Loading…
Reference in New Issue
Block a user