diff --git a/system/typemill/Controllers/ControllerApiSystemThemes.php b/system/typemill/Controllers/ControllerApiSystemThemes.php index d4d3a33..e8bc5bf 100644 --- a/system/typemill/Controllers/ControllerApiSystemThemes.php +++ b/system/typemill/Controllers/ControllerApiSystemThemes.php @@ -8,6 +8,7 @@ use Typemill\Models\Validation; use Typemill\Models\Extension; use Typemill\Models\Settings; use Typemill\Static\Translations; +use Typemill\Static\Slug; class ControllerApiSystemThemes extends Controller { @@ -69,4 +70,124 @@ class ControllerApiSystemThemes extends Controller return $response->withHeader('Content-Type', 'application/json')->withStatus(200); } + + public function updateReadymade(Request $request, Response $response) + { + $params = $request->getParsedBody(); + $themename = $params['theme'] ?? false; + $themeinput = $params['settings'] ?? false; + $readymadetitle = $params['readymadetitle'] ?? false; + $readymadedesc = $params['readymadedesc'] ?? false; + + $extension = new Extension(); + $formdefinitions = $extension->getThemeDefinition($themename); + $formdefinitions = $this->addDatasets($formdefinitions['forms']['fields']); + $themedata = []; + + # validate input + $validator = new Validation(); + $validatedOutput = $validator->recursiveValidation($formdefinitions, $themeinput); + if(!empty($validator->errors)) + { + $response->getBody()->write(json_encode([ + 'message' => Translations::translate('Please correct your input.'), + 'errors' => $validator->errors + ])); + + return $response->withHeader('Content-Type', 'application/json')->withStatus(400); + } + + $validator = $validator->returnValidator(['themename' => $themename, 'title' => $readymadetitle, 'description' => $readymadedesc]); + $validator->rule('required', ['themename', 'title', 'description']); + $validator->rule('lengthBetween', 'description', 3, 100)->message("Length between 3 - 100"); + $validator->rule('noHTML', 'description')->message(" contains HTML"); + $validator->rule('regex', 'themename', '/^[a-zA-Z0-9\-]{3,40}$/')->message("only a-zA-Z0-9 with 3 - 40 chars allowed"); + $validator->rule('regex', 'title', '/^[a-zA-Z0-9\-\_ ]{3,20}$/')->message("only a-zA-Z0-9 with 3 - 20 chars allowed"); + + if(!$validator->validate()) + { + $message = 'There was an error, please try again'; + $errors = $validator->errors(); + $firstKey = array_key_first($errors); + if(isset($errors[$firstKey][0])) + { + $message = $firstKey . ': ' . $errors[$firstKey][0]; + } + + $response->getBody()->write(json_encode([ + 'message' => $message + ])); + + return $response->withHeader('Content-Type', 'application/json')->withStatus(400); + } + + $readymadeSlug = Slug::createSlug($readymadetitle); + + $readymade = [ + $readymadeSlug => [ + 'name' => $readymadetitle, + 'description' => $readymadedesc, + 'delete' => true, + 'settings' => $validatedOutput + ] + ]; + + $extension->storeThemeReadymade($themename, $readymadeSlug, $readymade); + + $response->getBody()->write(json_encode([ + 'message' => Translations::translate('Readymade has been saved'), + 'readymade' => $readymade, + ])); + + return $response->withHeader('Content-Type', 'application/json')->withStatus(200); + } + + public function deleteReadymade(Request $request, Response $response) + { + $params = $request->getParsedBody(); + $themename = $params['theme'] ?? false; + $readymadeslug = $params['readymadeslug'] ?? false; + + $validation = new Validation(); + $validator = $validation->returnValidator($params); + $validator->rule('required', ['theme', 'readymadeslug']); + $validator->rule('regex', 'theme', '/^[a-zA-Z0-9\-]{3,40}$/')->message("only a-zA-Z0-9 with 3 - 40 chars allowed"); + $validator->rule('regex', 'readymadeslug', '/^[a-zA-Z0-9\-\_ ]{3,20}$/')->message("only a-zA-Z0-9 with 3 - 20 chars allowed"); + + if(!$validator->validate()) + { + $message = 'There was an error, please try again'; + $errors = $validator->errors(); + $firstKey = array_key_first($errors); + if(isset($errors[$firstKey][0])) + { + $message = $firstKey . ': ' . $errors[$firstKey][0]; + } + + $response->getBody()->write(json_encode([ + 'message' => $message + ])); + + return $response->withHeader('Content-Type', 'application/json')->withStatus(400); + } + + $extension = new Extension(); + $result = $extension->deleteThemeReadymade($themename, $readymadeslug); + + if($result !== true) + { + $response->getBody()->write(json_encode([ + 'message' => Translations::translate('We could not delete the readymade.'), + 'errors' => $result + ])); + + return $response->withHeader('Content-Type', 'application/json')->withStatus(400); + } + + $response->getBody()->write(json_encode([ + 'message' => Translations::translate('readymade has been deleted') + ])); + + return $response->withHeader('Content-Type', 'application/json')->withStatus(200); + } } diff --git a/system/typemill/Controllers/ControllerWebSystem.php b/system/typemill/Controllers/ControllerWebSystem.php index e182a65..767387c 100644 --- a/system/typemill/Controllers/ControllerWebSystem.php +++ b/system/typemill/Controllers/ControllerWebSystem.php @@ -74,7 +74,6 @@ class ControllerWebSystem extends Controller $extension = new Extension(); $themeDefinitions = $extension->getThemeDetails($this->settings['theme']); $themeSettings = $extension->getThemeSettings($this->settings['themes']); - $readymades = []; # add userroles and other datasets foreach($themeDefinitions as $name => $definitions) @@ -95,10 +94,10 @@ class ControllerWebSystem extends Controller } } - if(isset($definitions['readymades'])) - { - $readymades[$name] = $definitions['readymades']; - } + # get stored indvidual readymades + $builtinReadymades = $definitions['readymades'] ?? []; + $individualReadymades = $extension->getThemeReadymades($name); + $themeDefinitions[$name]['readymades'] = $builtinReadymades + $individualReadymades; } $license = []; @@ -115,7 +114,6 @@ class ControllerWebSystem extends Controller 'systemnavi' => $systemNavigation, 'settings' => $themeSettings, 'definitions' => $themeDefinitions, - 'readymades' => $readymades, 'theme' => $this->settings['theme'], 'license' => $license, 'labels' => $this->c->get('translations'), diff --git a/system/typemill/Models/Extension.php b/system/typemill/Models/Extension.php index 6cfd08d..6557be1 100644 --- a/system/typemill/Models/Extension.php +++ b/system/typemill/Models/Extension.php @@ -99,6 +99,50 @@ class Extension return $themeSettings; } + public function getThemeReadymades($themeName) + { + $folder = 'readymades' . DIRECTORY_SEPARATOR . $themeName; + $readymadeFolder = $this->storage->getFolderPath('dataFolder', $folder); + + $readymadeFiles = scandir($readymadeFolder); + + $readymades = []; + foreach($readymadeFiles as $readymadeFile) + { + if (!in_array($readymadeFile, array(".","..")) && substr($readymadeFile, 0, 1) != '.') + { + $readymadeData = $this->storage->getYaml('dataFolder', $folder, $readymadeFile); + if($readymadeData && !empty($readymadeData)) + { + $readymades = $readymades + $readymadeData; + } + } + } + + return $readymades; + } + + public function storeThemeReadymade($themeName, $readymadeName, $data) + { + $folder = 'readymades' . DIRECTORY_SEPARATOR . $themeName; + + $result = $this->storage->updateYaml('dataFolder', $folder, $readymadeName . '.yaml', $data); + } + + public function deleteThemeReadymade($themeName, $readymadeName) + { + $folder = 'readymades' . DIRECTORY_SEPARATOR . $themeName; + + $result = $this->storage->deleteFile('dataFolder', $folder, $readymadeName . '.yaml'); + + if(!$result) + { + return $this->storage->getError(); + } + + return true; + } + public function getPluginDetails($userSettings = NULL) { $plugins = $this->getPlugins(); diff --git a/system/typemill/author/css/custom.css b/system/typemill/author/css/custom.css index 32e2755..3eddfb6 100644 --- a/system/typemill/author/css/custom.css +++ b/system/typemill/author/css/custom.css @@ -32,7 +32,6 @@ opacity: 0; } - .fade-enter-active { transition: opacity 0.2s ease; } @@ -46,6 +45,19 @@ opacity: 0; } +.fade-item { + transition: opacity 0.5s ease-in-out; +} + +.fade-enter-active, .fade-leave-active { + transition: opacity 0.5s ease-in-out; +} + +.fade-enter, .fade-leave-to { + opacity: 0; +} + + .editableinput { border: none; outline: none; diff --git a/system/typemill/author/css/output.css b/system/typemill/author/css/output.css index 3fe3ce1..701d534 100644 --- a/system/typemill/author/css/output.css +++ b/system/typemill/author/css/output.css @@ -718,6 +718,18 @@ video { top: 2.5rem; } +.left-1 { + left: 0.25rem; +} + +.right-1 { + right: 0.25rem; +} + +.bottom-1 { + bottom: 0.25rem; +} + .z-10 { z-index: 10; } @@ -746,6 +758,10 @@ video { margin: 0.25rem; } +.m-2 { + margin: 0.5rem; +} + .my-2 { margin-top: 0.5rem; margin-bottom: 0.5rem; @@ -826,6 +842,10 @@ video { margin-top: 0.5rem; } +.ml-2 { + margin-left: 0.5rem; +} + .mb-16 { margin-bottom: 4rem; } @@ -850,10 +870,6 @@ video { margin-bottom: 1.25rem; } -.ml-2 { - margin-left: 0.5rem; -} - .mb-3 { margin-bottom: 0.75rem; } @@ -886,6 +902,10 @@ video { margin-top: 1.75rem; } +.mt-auto { + margin-top: auto; +} + .block { display: block; } @@ -962,6 +982,14 @@ video { height: 12rem; } +.h-64 { + height: 16rem; +} + +.h-40 { + height: 10rem; +} + .max-h-80 { max-height: 20rem; } @@ -1038,14 +1066,14 @@ video { width: 83.333333%; } -.w-10 { - width: 2.5rem; -} - .w-3\/5 { width: 60%; } +.w-7 { + width: 1.75rem; +} + .w-3\/4 { width: 75%; } @@ -1066,8 +1094,8 @@ video { width: 91.666667%; } -.w-7 { - width: 1.75rem; +.w-10 { + width: 2.5rem; } .max-w-md { @@ -1102,6 +1130,10 @@ video { flex-grow: 1; } +.grow { + flex-grow: 1; +} + .border-collapse { border-collapse: collapse; } @@ -1216,10 +1248,6 @@ video { white-space: nowrap; } -.rounded { - border-radius: 0.25rem; -} - .border { border-width: 1px; } @@ -1662,10 +1690,6 @@ video { padding-top: 0.75rem; } -.pb-1 { - padding-bottom: 0.25rem; -} - .text-left { text-align: left; } @@ -1798,16 +1822,16 @@ video { color: rgb(120 113 108 / var(--tw-text-opacity)); } -.text-stone-300 { - --tw-text-opacity: 1; - color: rgb(214 211 209 / var(--tw-text-opacity)); -} - .text-stone-700 { --tw-text-opacity: 1; color: rgb(68 64 60 / var(--tw-text-opacity)); } +.text-stone-300 { + --tw-text-opacity: 1; + color: rgb(214 211 209 / var(--tw-text-opacity)); +} + .text-red-500 { --tw-text-opacity: 1; color: rgb(239 68 68 / var(--tw-text-opacity)); @@ -2013,6 +2037,11 @@ video { background-color: rgb(202 138 4 / var(--tw-bg-opacity)); } +.hover\:bg-rose-600:hover { + --tw-bg-opacity: 1; + background-color: rgb(225 29 72 / var(--tw-bg-opacity)); +} + .hover\:text-stone-800:hover { --tw-text-opacity: 1; color: rgb(41 37 36 / var(--tw-text-opacity)); @@ -2047,6 +2076,12 @@ video { opacity: 1; } +.hover\:shadow-lg:hover { + --tw-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1); + --tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); +} + .focus\:border-blue-600:focus { --tw-border-opacity: 1; border-color: rgb(37 99 235 / var(--tw-border-opacity)); @@ -2321,6 +2356,10 @@ video { width: 25%; } + .lg\:w-full { + width: 100%; + } + .lg\:flex-row { flex-direction: row; } diff --git a/system/typemill/author/js/vue-themes.js b/system/typemill/author/js/vue-themes.js index a81a1e2..e6d90c8 100644 --- a/system/typemill/author/js/vue-themes.js +++ b/system/typemill/author/js/vue-themes.js @@ -2,7 +2,7 @@ const app = Vue.createApp({ template: `
- +
{{ fieldDefinition.legend }} @@ -111,19 +136,21 @@ const app = Vue.createApp({ `, data() { return { - current: '', - formDefinitions: data.definitions, - formData: data.settings, - readymades: data.readymades, - theme: data.theme, - license: data.license, - message: '', - messageClass: '', - errors: {}, - versions: false, - userroles: false, - showModal: false, - modalMessage: 'default', + current: '', + formDefinitions: data.definitions, + formData: data.settings, + readymadeTitle: '', + readymadeDescription: '', + readymadeError: false, + theme: data.theme, + license: data.license, + message: '', + messageClass: '', + errors: {}, + versions: false, + userroles: false, + showModal: false, + modalMessage: 'default', } }, components: { @@ -247,6 +274,8 @@ const app = Vue.createApp({ }, loadReadymade(name) { + this.readymadeError = false; + if(this.readymades[this.current] && this.readymades[this.current].individual === undefined) { this.readymades[this.current].individual = { 'settings' : this.formData[this.current] }; @@ -258,6 +287,88 @@ const app = Vue.createApp({ eventBus.$emit('codeareaupdate'); } }, + checkTitle() + { + if(this.readymadeTitle.length > 20) + { + this.readymadeTitle = this.readymadeTitle.substring(0, 20); + } + if(this.readymadeTitle.match(/^[a-zA-Z0-9\- ]*$/)) + { + this.readymadeError = false; + } + else + { + this.readymadeError = "Only characters [a-zA-Z0-9- ] are allowed." + } + }, + checkDescription() + { + if(this.readymadeDescription.length > 100) + { + this.readymadeDescription = this.readymadeDescription.substring(0, 100); + } + }, + storeReadymade() + { + this.readymadeError = false; + + var rself = this; + + tmaxios.post('/api/v1/treadymade',{ + 'theme': this.current, + 'settings': this.formData[this.current], + 'readymadetitle': this.readymadeTitle, + 'readymadedesc': this.readymadeDescription + }) + .then(function (response) + { + if(response.data.readymade !== undefined) + { + rself.formDefinitions[rself.current].readymades = Object.assign(rself.formDefinitions[rself.current].readymades, response.data.readymade); + + rself.readymadeTitle = ''; + rself.readymadeDescription = ''; + } + }) + .catch(function (error) + { + if(error.response) + { + if(error.response.data.message !== undefined) + { + rself.readymadeError = error.response.data.message; + } + } + }); + }, + deleteReadymade(name) + { + this.readymadeError = false; + + var rself = this; + + tmaxios.delete('/api/v1/treadymade',{ + data: { + 'theme': this.current, + 'readymadeslug': name + } + }) + .then(function (response) + { + delete rself.formDefinitions[rself.current].readymades[name]; + }) + .catch(function (error) + { + if(error.response) + { + if(error.response.data.message !== undefined) + { + rself.readymadeError = error.response.data.message; + } + } + }); + }, save() { this.reset(); @@ -316,9 +427,10 @@ const app = Vue.createApp({ }, reset() { + this.readymadeError = false; this.errors = {}; this.message = ''; - this.messageClass = ''; + this.messageClass = ''; } }, }) \ No newline at end of file diff --git a/system/typemill/routes/api.php b/system/typemill/routes/api.php index d575712..9e53916 100644 --- a/system/typemill/routes/api.php +++ b/system/typemill/routes/api.php @@ -34,6 +34,8 @@ $app->group('/api/v1', function (RouteCollectorProxy $group) use ($acl) { $group->post('/licensetestcall', ControllerApiSystemLicense::class . ':testLicenseServerCall')->setName('api.license.testcall')->add(new ApiAuthorization($acl, 'user', 'update')); # admin $group->post('/themecss', ControllerApiSystemThemes::class . ':updateThemeCss')->setName('api.themecss.set')->add(new ApiAuthorization($acl, 'system', 'update')); # manager $group->post('/theme', ControllerApiSystemThemes::class . ':updateTheme')->setName('api.theme.set')->add(new ApiAuthorization($acl, 'system', 'update')); # manager + $group->post('/treadymade', ControllerApiSystemThemes::class . ':updateReadymade')->setName('api.treadymade.set')->add(new ApiAuthorization($acl, 'system', 'update')); # manager + $group->delete('/treadymade', ControllerApiSystemThemes::class . ':deleteReadymade')->setName('api.treadymade.delete')->add(new ApiAuthorization($acl, 'system', 'update')); # manager $group->post('/plugin', ControllerApiSystemPlugins::class . ':updatePlugin')->setName('api.plugin.set')->add(new ApiAuthorization($acl, 'system', 'update')); # manager $group->post('/extensions', ControllerApiSystemExtensions::class . ':activateExtension')->setName('api.extension.activate')->add(new ApiAuthorization($acl, 'system', 'update')); # manager $group->post('/versioncheck', ControllerApiSystemVersions::class . ':checkVersions')->setName('api.versioncheck')->add(new ApiAuthorization($acl, 'system', 'update')); # manager diff --git a/themes/cyanine/cyanine.yaml b/themes/cyanine/cyanine.yaml index 05858b3..d82036f 100644 --- a/themes/cyanine/cyanine.yaml +++ b/themes/cyanine/cyanine.yaml @@ -1,6 +1,6 @@ name: Cyanine Theme -version: 2.1.0 -description: Cyanine is a modern and flexible multi-purpose theme and the standard theme for typemill. +version: 2.2.0 +description: 'Cyanine is a modern and flexible multi-purpose theme and the standard theme for Typemill.' author: Trendschau homepage: https://trendschau.net license: MIT @@ -213,43 +213,66 @@ forms: bloghomepage: type: fieldset - legend: Post Configuration (Blog or News) + legend: 'Post Configuration (Blog or News)' fields: blogimage: type: checkbox - label: Hero Images - checkboxlabel: Show hero images in all lists of posts + label: 'Hero Images' + checkboxlabel: 'Show hero images in all lists of posts' blog: type: checkbox - label: Homepage Posts - checkboxlabel: Show posts on the homepage + label: 'Posts on Homepage' + checkboxlabel: 'Show posts on the homepage' blogfolder: type: text - label: Homepage Posts Folder - placeholder: /blog - description: Add the relative pasts to the folder with posts that should appear on the homepage + label: 'Folder for Posts on Homepage' + placeholder: '/blog' + description: 'Add the relative pasts to the folder with posts that should appear on the homepage' blogintro: type: checkbox - label: Homepage Intro Text - checkboxlabel: Show the text of the homepage before the list of posts + label: 'Homepage Intro Text' + checkboxlabel: 'Show the text of the homepage and list posts below' landing: type: fieldset - legend: Landingpage + legend: 'Landingpage' fields: landingpage: type: checkbox - checkboxlabel: Activate a landing page with segments on the homepage + checkboxlabel: 'Activate a landing page with different segments on the homepage. Configure the segments in the fieldsets below.' + introPosition: + type: number + label: 'Position of Intro Segment' + description: 'A intro segment with a title, a slogan, a call to action, and a background-image. Use 0 to disable this section.' + css: 'lg:w-full' + infoPosition: + type: number + label: 'Position of Info Segment' + description: 'A content segment with pure makrdown. Use 0 to disable the section.' + css: 'lg:w-full' + teaserPosition: + type: number + label: 'Position of Teaser Segment' + description: 'Use up to three horizontal teasers with call to action buttons. Use 0 to disable the section.' + css: 'lg:w-full' + contrastPosition: + type: number + label: 'Position of Contrast Segment' + description: 'A content segment wiht pure markdown and inverted colors for contrast. Use 0 to disable the section.' + naviPosition: + type: number + label: 'Position of Navigation Segment' + description: 'A segment with the navigation/table of contents. Use 0 to disable the section.' + newsPosition: + type: number + label: 'Position of News Segment' + description: 'A segment with three content-teasers for latest news or posts from a folder. Use 0 to disable the section.' + css: 'lg:w-full' landingpageIntro: type: fieldset - legend: Landingpage Intro Segment + legend: 'Landingpage Intro Segment' fields: - introPosition: - type: number - label: Position of Intro Segment - description: Use 0 to disable the section - css: 'lg:w-full' introFullsize: type: checkbox label: Full Screen @@ -280,7 +303,7 @@ forms: type: text label: Background Image Opacity placeholder: 0.8 - description: 0 is fully transparent, 1 is without transparency + description: 0 is without transparency, 1 is fully transparent and invisible introImageBlendmode: type: select label: Blend mode for background image @@ -308,10 +331,6 @@ forms: type: fieldset legend: Landingpage Info Segment fields: - infoPosition: - type: number - label: Position of Info Segment - description: Use 0 to disable the section infoMarkdown: type: textarea label: Use Markdown @@ -320,10 +339,6 @@ forms: type: fieldset legend: Landingpage Teaser Segment fields: - teaserPosition: - type: number - label: Position of Teaser Segment - description: Use 0 to disable the section teaser1title: type: text label: Teaser 1 Title @@ -377,10 +392,6 @@ forms: type: fieldset legend: Landingpage Contrast Segment fields: - contrastPosition: - type: number - label: Position of Contrast Segment - description: Use 0 to disable the section contrastTitle: type: text label: Title @@ -400,10 +411,6 @@ forms: type: fieldset legend: Landingpage Navigation Segment fields: - naviPosition: - type: number - label: Position of Navi Segment - description: Use 0 to disable the section naviTitle: type: text label: Title for navigation @@ -415,27 +422,22 @@ forms: type: fieldset legend: Landingpage News Segment fields: - newsPosition: - type: number - label: Position of News Segment - description: Use 0 to disable the section - css: 'lg:w-half' newsHeadline: type: text label: Headline for news-segment placeholder: News - css: 'lg:w-half' + css: 'lg:w-full' newsFolder: type: text label: List entries from folder placeholder: /blog description: Add a path to a folder from which you want to list entries - css: 'lg:w-half' + css: 'lg:w-full' newsLabel: type: text label: Label for read more link placeholder: All News - css: 'lg:w-half' + css: 'lg:w-full' fieldsetAuthor: type: fieldset diff --git a/themes/cyanine/page.twig b/themes/cyanine/page.twig index 935fe65..b1ea322 100644 --- a/themes/cyanine/page.twig +++ b/themes/cyanine/page.twig @@ -7,7 +7,7 @@