Add frontend support for Quill JSON strings

This commit is contained in:
Christopher C. Wells 2021-03-07 14:52:39 -08:00
parent 22173bcdcb
commit a0d842e562
11 changed files with 20298 additions and 119 deletions

View File

@ -28,7 +28,7 @@ class RecipeSchema extends SchemaProvider
return [
'slug' => $resource->slug,
'name' => $resource->name,
'description' => $resource->description,
'description' => $resource->description_html,
'time_prep' => $resource->time_prep,
'time_active' => $resource->time_active,
'time_total' => $resource->time_total,

View File

@ -71,6 +71,7 @@ use Spatie\Tags\HasTags;
* @method static \Illuminate\Database\Eloquent\Builder|Food withAnyTags($tags, ?string $type = null)
* @method static \Illuminate\Database\Eloquent\Builder|Food withAnyTagsOfAnyType($tags)
* @mixin \Eloquent
* @method static \Illuminate\Database\Eloquent\Builder|Food withUniqueSlugConstraints(\Illuminate\Database\Eloquent\Model $model, string $attribute, array $config, string $slug)
*/
final class Food extends Model
{

View File

@ -6,6 +6,7 @@ use App\Models\Traits\HasIngredients;
use App\Models\Traits\Ingredient;
use App\Models\Traits\Journalable;
use App\Models\Traits\Sluggable;
use DBlackborough\Quill\Render;
use ElasticScoutDriverPlus\QueryDsl;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
@ -55,6 +56,13 @@ use Spatie\Tags\HasTags;
* @method static \Illuminate\Database\Eloquent\Builder|Recipe withAnyTags($tags, ?string $type = null)
* @method static \Illuminate\Database\Eloquent\Builder|Recipe withAnyTagsOfAnyType($tags)
* @mixin \Eloquent
* @property int|null $time_prep
* @property int|null $time_active
* @property-read int $time_total
* @method static \Illuminate\Database\Eloquent\Builder|Recipe whereTimeActive($value)
* @method static \Illuminate\Database\Eloquent\Builder|Recipe whereTimePrep($value)
* @method static \Illuminate\Database\Eloquent\Builder|Recipe withUniqueSlugConstraints(\Illuminate\Database\Eloquent\Model $model, string $attribute, array $config, string $slug)
* @property-read string $description_html
*/
final class Recipe extends Model
{
@ -106,6 +114,7 @@ final class Recipe extends Model
* @inheritdoc
*/
protected $appends = [
'description_html',
'serving_weight',
'time_total',
];
@ -118,13 +127,33 @@ final class Recipe extends Model
return [
'name' => $this->name,
'tags' => $this->tags->pluck('name')->toArray(),
'description' => $this->description,
'description' => $this->description_html,
'source' => $this->source,
'created_at' => $this->created_at,
'updated_at' => $this->updated_at,
];
}
/**
* Get description as an HTML string.
*/
public function getDescriptionHtmlAttribute(): ?string {
$description = $this->description;
if (!empty($description)) {
try {
$quill = new Render($this->description);
$description = $quill->render();
} catch (\Exception $e) {
// TODO: Log this or something.
$description = null;
}
}
return $description;
}
/**
* Get total recipe time.
*/
public function getTimeTotalAttribute(): int {
return $this->time_prep + $this->time_active;
}

View File

@ -10,6 +10,7 @@
"babenkoivan/elastic-scout-driver-plus": "^2.0",
"cloudcreativity/laravel-json-api": "^3.2",
"cviebrock/eloquent-sluggable": "^8.0",
"deanblackborough/php-quill-renderer": "^4.00",
"fideloper/proxy": "^4.4",
"fruitcake/laravel-cors": "^2.0",
"guzzlehttp/guzzle": "^7.0.1",

67
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "7ceaed365c3962d66d301ae612a52e9f",
"content-hash": "da6d8202b7a5074a138e4b6fd87e687e",
"packages": [
{
"name": "asm89/stack-cors",
@ -711,6 +711,71 @@
},
"time": "2021-02-28T20:03:09+00:00"
},
{
"name": "deanblackborough/php-quill-renderer",
"version": "v4.00.0",
"source": {
"type": "git",
"url": "https://github.com/deanblackborough/php-quill-renderer.git",
"reference": "bcaa78799b3c29a41eeff1469c64018c37769349"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/deanblackborough/php-quill-renderer/zipball/bcaa78799b3c29a41eeff1469c64018c37769349",
"reference": "bcaa78799b3c29a41eeff1469c64018c37769349",
"shasum": ""
},
"require": {
"ext-json": "*",
"php": "^7.4|^8"
},
"require-dev": {
"php-coveralls/php-coveralls": "^v2.4.3",
"phpunit/phpunit": "^9"
},
"suggest": {
"php": "^8.0"
},
"type": "library",
"autoload": {
"psr-4": {
"DBlackborough\\Quill\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Dean Blackborough",
"email": "dean@g3d-development.com"
}
],
"description": "Render quill insert deltas to HTML, Markdown and GitHub flavoured Markdown",
"homepage": "http://www.transmute-coffee.com/php-quill-renderer.php",
"keywords": [
"delta",
"html",
"markdown",
"parse",
"php",
"quill",
"quilljs",
"renderer"
],
"support": {
"issues": "https://github.com/deanblackborough/php-quill-renderer/issues",
"source": "https://github.com/deanblackborough/php-quill-renderer/tree/v4.00.0"
},
"funding": [
{
"url": "https://github.com/deanblackborough",
"type": "github"
}
],
"time": "2021-02-16T13:57:01+00:00"
},
{
"name": "dnoegel/php-xdg-base-dir",
"version": "v0.1.1",

12178
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -11,9 +11,10 @@
},
"devDependencies": {
"@tailwindcss/forms": "^0.2.1",
"@tailwindcss/typography": "^0.4.0",
"alpinejs": "^2.7.3",
"autoprefixer": "^9.8.6",
"axios": "^0.19",
"axios": "^0.21.1",
"cross-env": "^7.0",
"laravel-mix": "^5.0.1",
"lodash": "^4.17.19",

7914
public/css/app.css vendored

File diff suppressed because it is too large Load Diff

191
public/js/app.js vendored
View File

@ -4497,6 +4497,7 @@ module.exports = __webpack_require__(/*! ./lib/axios */ "./node_modules/axios/li
var utils = __webpack_require__(/*! ./../utils */ "./node_modules/axios/lib/utils.js");
var settle = __webpack_require__(/*! ./../core/settle */ "./node_modules/axios/lib/core/settle.js");
var cookies = __webpack_require__(/*! ./../helpers/cookies */ "./node_modules/axios/lib/helpers/cookies.js");
var buildURL = __webpack_require__(/*! ./../helpers/buildURL */ "./node_modules/axios/lib/helpers/buildURL.js");
var buildFullPath = __webpack_require__(/*! ../core/buildFullPath */ "./node_modules/axios/lib/core/buildFullPath.js");
var parseHeaders = __webpack_require__(/*! ./../helpers/parseHeaders */ "./node_modules/axios/lib/helpers/parseHeaders.js");
@ -4517,7 +4518,7 @@ module.exports = function xhrAdapter(config) {
// HTTP basic authentication
if (config.auth) {
var username = config.auth.username || '';
var password = config.auth.password || '';
var password = config.auth.password ? unescape(encodeURIComponent(config.auth.password)) : '';
requestHeaders.Authorization = 'Basic ' + btoa(username + ':' + password);
}
@ -4598,8 +4599,6 @@ module.exports = function xhrAdapter(config) {
// This is only done if running in a standard browser environment.
// Specifically not if we're in a web worker, or react-native.
if (utils.isStandardBrowserEnv()) {
var cookies = __webpack_require__(/*! ./../helpers/cookies */ "./node_modules/axios/lib/helpers/cookies.js");
// Add xsrf header
var xsrfValue = (config.withCredentials || isURLSameOrigin(fullPath)) && config.xsrfCookieName ?
cookies.read(config.xsrfCookieName) :
@ -4665,7 +4664,7 @@ module.exports = function xhrAdapter(config) {
});
}
if (requestData === undefined) {
if (!requestData) {
requestData = null;
}
@ -4734,6 +4733,9 @@ axios.all = function all(promises) {
};
axios.spread = __webpack_require__(/*! ./helpers/spread */ "./node_modules/axios/lib/helpers/spread.js");
// Expose isAxiosError
axios.isAxiosError = __webpack_require__(/*! ./helpers/isAxiosError */ "./node_modules/axios/lib/helpers/isAxiosError.js");
module.exports = axios;
// Allow use of default import syntax in TypeScript
@ -4942,9 +4944,10 @@ Axios.prototype.getUri = function getUri(config) {
utils.forEach(['delete', 'get', 'head', 'options'], function forEachMethodNoData(method) {
/*eslint func-names:0*/
Axios.prototype[method] = function(url, config) {
return this.request(utils.merge(config || {}, {
return this.request(mergeConfig(config || {}, {
method: method,
url: url
url: url,
data: (config || {}).data
}));
};
});
@ -4952,7 +4955,7 @@ utils.forEach(['delete', 'get', 'head', 'options'], function forEachMethodNoData
utils.forEach(['post', 'put', 'patch'], function forEachMethodWithData(method) {
/*eslint func-names:0*/
Axios.prototype[method] = function(url, data, config) {
return this.request(utils.merge(config || {}, {
return this.request(mergeConfig(config || {}, {
method: method,
url: url,
data: data
@ -5212,7 +5215,7 @@ module.exports = function enhanceError(error, config, code, request, response) {
error.response = response;
error.isAxiosError = true;
error.toJSON = function() {
error.toJSON = function toJSON() {
return {
// Standard
message: this.message,
@ -5261,59 +5264,73 @@ module.exports = function mergeConfig(config1, config2) {
config2 = config2 || {};
var config = {};
var valueFromConfig2Keys = ['url', 'method', 'params', 'data'];
var mergeDeepPropertiesKeys = ['headers', 'auth', 'proxy'];
var valueFromConfig2Keys = ['url', 'method', 'data'];
var mergeDeepPropertiesKeys = ['headers', 'auth', 'proxy', 'params'];
var defaultToConfig2Keys = [
'baseURL', 'url', 'transformRequest', 'transformResponse', 'paramsSerializer',
'timeout', 'withCredentials', 'adapter', 'responseType', 'xsrfCookieName',
'xsrfHeaderName', 'onUploadProgress', 'onDownloadProgress',
'maxContentLength', 'validateStatus', 'maxRedirects', 'httpAgent',
'httpsAgent', 'cancelToken', 'socketPath'
'baseURL', 'transformRequest', 'transformResponse', 'paramsSerializer',
'timeout', 'timeoutMessage', 'withCredentials', 'adapter', 'responseType', 'xsrfCookieName',
'xsrfHeaderName', 'onUploadProgress', 'onDownloadProgress', 'decompress',
'maxContentLength', 'maxBodyLength', 'maxRedirects', 'transport', 'httpAgent',
'httpsAgent', 'cancelToken', 'socketPath', 'responseEncoding'
];
var directMergeKeys = ['validateStatus'];
function getMergedValue(target, source) {
if (utils.isPlainObject(target) && utils.isPlainObject(source)) {
return utils.merge(target, source);
} else if (utils.isPlainObject(source)) {
return utils.merge({}, source);
} else if (utils.isArray(source)) {
return source.slice();
}
return source;
}
function mergeDeepProperties(prop) {
if (!utils.isUndefined(config2[prop])) {
config[prop] = getMergedValue(config1[prop], config2[prop]);
} else if (!utils.isUndefined(config1[prop])) {
config[prop] = getMergedValue(undefined, config1[prop]);
}
}
utils.forEach(valueFromConfig2Keys, function valueFromConfig2(prop) {
if (typeof config2[prop] !== 'undefined') {
config[prop] = config2[prop];
if (!utils.isUndefined(config2[prop])) {
config[prop] = getMergedValue(undefined, config2[prop]);
}
});
utils.forEach(mergeDeepPropertiesKeys, function mergeDeepProperties(prop) {
if (utils.isObject(config2[prop])) {
config[prop] = utils.deepMerge(config1[prop], config2[prop]);
} else if (typeof config2[prop] !== 'undefined') {
config[prop] = config2[prop];
} else if (utils.isObject(config1[prop])) {
config[prop] = utils.deepMerge(config1[prop]);
} else if (typeof config1[prop] !== 'undefined') {
config[prop] = config1[prop];
}
});
utils.forEach(mergeDeepPropertiesKeys, mergeDeepProperties);
utils.forEach(defaultToConfig2Keys, function defaultToConfig2(prop) {
if (typeof config2[prop] !== 'undefined') {
config[prop] = config2[prop];
} else if (typeof config1[prop] !== 'undefined') {
config[prop] = config1[prop];
if (!utils.isUndefined(config2[prop])) {
config[prop] = getMergedValue(undefined, config2[prop]);
} else if (!utils.isUndefined(config1[prop])) {
config[prop] = getMergedValue(undefined, config1[prop]);
}
});
utils.forEach(directMergeKeys, function merge(prop) {
if (prop in config2) {
config[prop] = getMergedValue(config1[prop], config2[prop]);
} else if (prop in config1) {
config[prop] = getMergedValue(undefined, config1[prop]);
}
});
var axiosKeys = valueFromConfig2Keys
.concat(mergeDeepPropertiesKeys)
.concat(defaultToConfig2Keys);
.concat(defaultToConfig2Keys)
.concat(directMergeKeys);
var otherKeys = Object
.keys(config2)
.keys(config1)
.concat(Object.keys(config2))
.filter(function filterAxiosKeys(key) {
return axiosKeys.indexOf(key) === -1;
});
utils.forEach(otherKeys, function otherKeysDefaultToConfig2(prop) {
if (typeof config2[prop] !== 'undefined') {
config[prop] = config2[prop];
} else if (typeof config1[prop] !== 'undefined') {
config[prop] = config1[prop];
}
});
utils.forEach(otherKeys, mergeDeepProperties);
return config;
};
@ -5342,7 +5359,7 @@ var createError = __webpack_require__(/*! ./createError */ "./node_modules/axios
*/
module.exports = function settle(resolve, reject, response) {
var validateStatus = response.config.validateStatus;
if (!validateStatus || validateStatus(response.status)) {
if (!response.status || !validateStatus || validateStatus(response.status)) {
resolve(response);
} else {
reject(createError(
@ -5474,6 +5491,7 @@ var defaults = {
xsrfHeaderName: 'X-XSRF-TOKEN',
maxContentLength: -1,
maxBodyLength: -1,
validateStatus: function validateStatus(status) {
return status >= 200 && status < 300;
@ -5537,7 +5555,6 @@ var utils = __webpack_require__(/*! ./../utils */ "./node_modules/axios/lib/util
function encode(val) {
return encodeURIComponent(val).
replace(/%40/gi, '@').
replace(/%3A/gi, ':').
replace(/%24/g, '$').
replace(/%2C/gi, ',').
@ -5721,6 +5738,29 @@ module.exports = function isAbsoluteURL(url) {
};
/***/ }),
/***/ "./node_modules/axios/lib/helpers/isAxiosError.js":
/*!********************************************************!*\
!*** ./node_modules/axios/lib/helpers/isAxiosError.js ***!
\********************************************************/
/*! no static exports found */
/***/ (function(module, exports, __webpack_require__) {
"use strict";
/**
* Determines whether the payload is an error thrown by Axios
*
* @param {*} payload The value to test
* @returns {boolean} True if the payload is an error thrown by Axios, otherwise false
*/
module.exports = function isAxiosError(payload) {
return (typeof payload === 'object') && (payload.isAxiosError === true);
};
/***/ }),
/***/ "./node_modules/axios/lib/helpers/isURLSameOrigin.js":
@ -6046,6 +6086,21 @@ function isObject(val) {
return val !== null && typeof val === 'object';
}
/**
* Determine if a value is a plain Object
*
* @param {Object} val The value to test
* @return {boolean} True if value is a plain Object, otherwise false
*/
function isPlainObject(val) {
if (toString.call(val) !== '[object Object]') {
return false;
}
var prototype = Object.getPrototypeOf(val);
return prototype === null || prototype === Object.prototype;
}
/**
* Determine if a value is a Date
*
@ -6202,34 +6257,12 @@ function forEach(obj, fn) {
function merge(/* obj1, obj2, obj3, ... */) {
var result = {};
function assignValue(val, key) {
if (typeof result[key] === 'object' && typeof val === 'object') {
if (isPlainObject(result[key]) && isPlainObject(val)) {
result[key] = merge(result[key], val);
} else {
result[key] = val;
}
}
for (var i = 0, l = arguments.length; i < l; i++) {
forEach(arguments[i], assignValue);
}
return result;
}
/**
* Function equal to merge with the difference being that no reference
* to original objects is kept.
*
* @see merge
* @param {Object} obj1 Object to merge
* @returns {Object} Result of all merge properties
*/
function deepMerge(/* obj1, obj2, obj3, ... */) {
var result = {};
function assignValue(val, key) {
if (typeof result[key] === 'object' && typeof val === 'object') {
result[key] = deepMerge(result[key], val);
} else if (typeof val === 'object') {
result[key] = deepMerge({}, val);
} else if (isPlainObject(val)) {
result[key] = merge({}, val);
} else if (isArray(val)) {
result[key] = val.slice();
} else {
result[key] = val;
}
@ -6260,6 +6293,19 @@ function extend(a, b, thisArg) {
return a;
}
/**
* Remove byte order marker. This catches EF BB BF (the UTF-8 BOM)
*
* @param {string} content with BOM
* @return {string} content value without BOM
*/
function stripBOM(content) {
if (content.charCodeAt(0) === 0xFEFF) {
content = content.slice(1);
}
return content;
}
module.exports = {
isArray: isArray,
isArrayBuffer: isArrayBuffer,
@ -6269,6 +6315,7 @@ module.exports = {
isString: isString,
isNumber: isNumber,
isObject: isObject,
isPlainObject: isPlainObject,
isUndefined: isUndefined,
isDate: isDate,
isFile: isFile,
@ -6279,9 +6326,9 @@ module.exports = {
isStandardBrowserEnv: isStandardBrowserEnv,
forEach: forEach,
merge: merge,
deepMerge: deepMerge,
extend: extend,
trim: trim
trim: trim,
stripBOM: stripBOM
};

View File

@ -20,6 +20,11 @@
</x-slot>
<div class="flex flex-col-reverse justify-between pb-4 sm:flex-row">
<div x-data="{showNutrientsSummary: false}">
@if($recipe->description_html)
<section class="mb-2 prose prose-lg md:prose-xl">
{!! $recipe->description_html !!}
</section>
@endif
@if(!$recipe->tags->isEmpty())
<section class="mb-2 text-gray-700 text-sm">
<h1 class="font-extrabold inline">Tags:</h1>
@ -42,12 +47,6 @@
</div>
</section>
@endif
@if($recipe->description)
<section>
<h1 class="mb-2 font-bold text-2xl">Description</h1>
<p class="mb-2 text-gray-800">{{ $recipe->description }}</p>
</section>
@endif
<section x-data="{showNutrientsSummary: false}">
<h1 class="mb-2 font-bold text-2xl">
Ingredients
@ -121,12 +120,13 @@
</div>
<section>
<h1 class="mb-2 font-bold text-2xl">Steps</h1>
@foreach($recipe->steps as $step)
<div class="flex flex-row space-x-4 mb-4">
<p class="text-3xl text-gray-400 text-center">{{ $step->number }}</p>
<p class="text-2xl">{{ $step->step }}</p>
</div>
@endforeach
<div class="prose prose-xl md:prose-2xl">
<ol>
@foreach($recipe->steps as $step)
<li>{{ $step->step }}</li>
@endforeach
</ol>
</div>
</section>
@if($recipe->source)
<footer class="mb-2 text-gray-500 text-sm">

5
tailwind.config.js vendored
View File

@ -17,5 +17,8 @@ module.exports = {
},
},
plugins: [require('@tailwindcss/forms')],
plugins: [
require('@tailwindcss/typography'),
require('@tailwindcss/forms')
],
};