Jump to content

Stefanowitsch

Members
  • Posts

    293
  • Joined

  • Last visited

  • Days Won

    5

Everything posted by Stefanowitsch

  1. @bernhard do you think this is something the RockPageBuilder can handle?
  2. That is an interesting idea to handle fetching data. Would you mind showing us a short code example? I am planning to do the same in a project. Besides from that the the page you built is very avant-garde! It's kind of cool to see that designers go unusual and experimental ways in webdesign. It reminds me of the early days of the internet. Bedsides from that I have to admit - I also had my struggles understanding how navigating this website actually works 🙂
  3. I think thats also what I've been looking fore. Where do I have to put this config object?
  4. I am glad that it's working for you! I did a quick test with repeater matrix items and as you described it seems not to work with that. I don't use the Repeater Matrix field anymore (instead the RockPageBuilder from @bernhard) but I haven't tried the autocomplete search with that module either!
  5. @bernhard While the new Drag & Drop feature (with the cross icon) works on the RPB Blocks itself it seems not work with repeater items inside the blocks anymore. Can you verify this behaviour? EDIT: ooooops. I just read @netcarver post and your reply on that one. So it will be fixed with the next release.
  6. This error completely disappeared after I changed the PHP ini settings to: open_basedir: none Before that I got all kinds of "open_basedir restriction in effect" error messages.
  7. @Roope I am using this module on PHP 8.1.2 and just received this error message: PHP Deprecated: Implicit conversion from float 521638.00000000006 to int loses precision in /home/users/xxx/site/modules/EmailObfuscation/EmailObfuscation.module:211 The module seems to work normally though. Is this something to be concerned about?
  8. @Robin S I have a question: On the latest sites that are using this module all of a sudden the image URLs get crawled by google and they appear in the search console (they are not indexed, though). This is a bit of a strange behaviour, I've never seen this on my older websites. Does this have anything to do with the way this modules handles the delayed image generation?
  9. Yes it somehow makes sense. I thought if this checkbox is unchecked it will stop sending error mails at all.
  10. Since upgrading Tracy Debugger I always get this message here when logging into the backend: Tracy Debugger "Email Sent" flag has been set. Clear it to continue receiving further emails +1 I find the feature very handy to receive error messages, but I don't always want that so I disabled this checkbox here in the module settings: Anyway after that I always get the message above on all admin pages. Is there an option to disable this behaviour?
  11. Back in the days when almost everything was build via tables in HTML you could actually use Photoshop to export your layout directly as HTML code. I used this for newsletter templates in the past. But beware, no one would ever design a page layout using tables anymore (at least I hope so...). Creating good designs is one thing and converting these designs in to a functional and responsive website is still really a piece of handcraft IMHO. So to answer your question: I use no AI helpers to turn those design files into working code. Too often you have to think around the corner "does this layout work on smaller viewports? How should the elements re-arrange, what elements should be hidden?" etc. etc. There is no way any AI can do that for me or for my clients - yet 😉
  12. In this tutorial I want to show you how to set up a simple ajax-driven auto-complete search within ProcessWire. Something that looks like this: Requirements: 1. Use the Fieldtype Cache to create a search index For fast and easy search queries we will create a search index field. You can read more on this here: https://processwire.recipes/recipes/set-up-search-index-with-fieldtypecache/ First of all, go to Modules and install a Core module called FieldtypeCache. Add a new field “search_cache” and select “Cache” as it’s type. Save the field. On the Details tab choose the fields you want to make searchable. Save again. Add “search_cache” to all templates you want to include in your search results. Optional but recommended: use the “Regenerate Cache” option found from the Details tab of the field to make existing content instantly searchable. In my case i only want to search inside body and title fields, so I included those fields in the settings of the search cache field: 2. Install the Pages2JSON module We want to make ajax requests to the search template and those results should be returned in JSON format so that our java script can process the results inside the auto-complete dropdown. Therefore we make use of this module here: https://processwire.com/modules/pages2-json/ In the module settings we define what data will be included in the JSON object that is returned. Remember that this is the data that we want to display in our auto-complete dropdown. So for my case I only want the title and the url field. Now let's start: 1. Setting up the search template Set up a template file called „search“. Then create a page using this template. On most ProcessWire installations this is already the case and this template exits in the file system. This template will handle the search queries and list all the results for us and that in two ways: - you can send a search query and the search result page will list all the results (as you would expect) - you can send an ajax search query and the result page will return the results as a json object to be processed within javascript and displayed in the frontend in real-time search.php: <?php if($config->ajax) { // Return search results in JSON format $q = $sanitizer->selectorValue($input->get->q); $results = $pages->find('search_cache%=' . $q);; // Find all pages and save as $results header("Content-type: application/json"); // Set header to JSON echo $results->toJSON(); // Output the results as JSON via the toJSON function return $this->halt(); } ?> <main> <div class="uk-container"> <?php // look for a GET variable named 'q' and sanitize it $q = $sanitizer->selectorValue($input->get->q); // did $q have anything in it? if($q) { // Find pages that match the selector $matches = $pages->find('search_cache%=' . $q); // did we find any matches? ... if($matches->count) { echo "<h2>We found $matches->count results:</h2>"; echo "<ul class='uk-list uk-list-square'>"; foreach($matches as $match) { echo "<li><a href='$match->url'>$match->title</a>"; echo "<div class='summary'>$match->summary</div></li>"; } echo "</ul>"; } else { ?> <h2>No results found.</h2> <div uk-grid class="uk-flex uk-flex-center"> <div class="uk-width-1-1 uk-width-1-2@m"> <?= $files->render('elements/_searchbox'); ?> </div> </div> <? } } else { ?> <h2>Search:</h2> <div uk-grid class="uk-flex uk-flex-center"> <div class="uk-width-1-1 uk-width-1-2@m"> <?= $files->render('elements/_searchbox'); ?> </div> </div> <? } ?> </div> </main> Explanation: This part here at the top of the template handles the requests that are send via ajax. This is the important part for later on. <?php if($config->ajax) { // Return search results in JSON format $q = $sanitizer->selectorValue($input->get->q); $results = $pages->find('search_cache%=' . $q);; // Find all pages and save as $results header("Content-type: application/json"); // Set header to JSON echo $results->toJSON(); // Output the results as JSON via the toJSON function return $this->halt(); } ?> What this does: check if the current request is an ajax request if so, search inside the search cache field return the results in JSON format then quit processing the rest of the template (we don’t want to render any markup in that case!) Everything below this part is the normal search template logic. If you send a search request via a form somewhere on the website you want to be redirected to the result page and all the results will be listed on the page just like you would expect. 2. Make the search „work“ Create a template file called "_searchbox.php" (this file is also included in the search template code above, so adjust your paths/names accordingly). To make search requests you want to include this search form anywhere on your page. _searchbox.php: <div class="search-wrapper uk-padding uk-text-center"> <h4>Searchbox</h4> <form class="searchform uk-position-relative uk-flex" method="get" action="<?= $pages->get("template=search")->url ?>"> <input class="uk-input" id="searchInput" name="q" type="search" aria-label="Suchen" autocomplete="off" placeholder="z.B. Bagger"> <button class="uk-button uk-button-primary search-button" type="submit"> <span class="uk-visible@s">Search</span> </button> <div id="suggestions" class="uk-box-shadow-medium"> </div> </form> </div> With this simple search form you should now be able to do a basic search that leads you to a search result page. 3. Make the ajax-search „work“ Now comes the interesting part. We will add in a java script snippet into our just created _searchbox.php that sends ajax requests to the search template page while we are typing into the search intput field and it will display the results in a nice little dropdown. Feel free to adjust the code to your needs! <script> document.addEventListener("DOMContentLoaded", () => { const searchForm = document.querySelector('.searchform'); const searchUrl = searchForm.getAttribute('action'); const searchInput = document.getElementById('searchInput'); const suggestionsDiv = document.getElementById('suggestions'); let selectedSuggestionIndex = -1; // close the auto-complete container when clicked outside the element document.addEventListener('click', function(event) { if (!suggestionsDiv.contains(event.target)) { suggestionsDiv.style.display = 'none'; } }); searchInput.addEventListener('input', () => { const searchText = searchInput.value.toLowerCase(); // Immediately Invoked Function Expression (IIFE) (async () => { try { const response = await fetch(searchUrl+"?q="+searchText, { headers: { 'X-Requested-With': 'XMLHttpRequest' } }); if (response.status === 200) { const data = await response.json(); showResults(data); } else { console.log(response.status); console.log(response.statusText); } } catch (error) { console.error(error); } })(); function showResults(data) { // Show suggestions only if the input has at least two characters if (searchText.length >= 2) { const suggestionHTML = data.map(item => { // Highlight the matching characters using a <span> element with a CSS class const highlightedTitle = item.title.replace( new RegExp(searchText, 'gi'), match => `<span class="highlight">${match}</span>` ); return `<li class="suggestion"><a href="${item.url}">${highlightedTitle}</a></li>`; }).join(''); // Array to string conversion suggestionsDiv.innerHTML = ''; // Clear the suggestions if input length is less than two characters // create list and append search results const suggestionList = document.createElement("ul"); suggestionList.classList.add('suggestion-list', 'uk-list'); suggestionList.innerHTML = suggestionHTML; suggestionsDiv.appendChild(suggestionList); selectedSuggestionIndex = -1; // show the results suggestionsDiv.style.display = "block"; } else { suggestionsDiv.innerHTML = ''; // Clear the suggestions if input length is less than two characters } } }); // Event listener for arrow key presses searchInput.addEventListener("keydown", function (event) { const suggestions = document.querySelectorAll(".suggestion"); if (event.key === "ArrowDown") { event.preventDefault(); selectedSuggestionIndex = Math.min( selectedSuggestionIndex + 1, suggestions.length - 1 ); } else if (event.key === "ArrowUp") { event.preventDefault(); selectedSuggestionIndex = Math.max(selectedSuggestionIndex - 1, -1); } else if (event.key === "Enter") { event.preventDefault(); if (selectedSuggestionIndex === -1) { searchForm.submit(); } else { window.location = suggestions[selectedSuggestionIndex].querySelector('a').href; } } // Highlight the selected suggestion suggestions.forEach((suggestion, index) => { suggestion.classList.toggle( "selected", index === selectedSuggestionIndex ); }); }); }); </script> Keep in mind that you need some CSS styes to make it look good and actually work. These are my styles(in LESS format) but feel free to write you own stylesheet for this. search.less: .search-wrapper { position: relative; background: @secondary-blue; h4 { color: @primary-blue; } .highlight { color: @primary-red; } } #suggestions { display: none; position: absolute; top: 100%; left: 0; right: 0; z-index: 10; background: @white; .suggestion-list { margin: 0; li { transition: background-color 150ms ease-in-out; a { padding: 10px; display: block; text-align: left; &:hover, &:focus, &:active { text-decoration: none; } } &:hover, &:focus, &:active, &.selected { background: @secondary-blue; } } li + li { border-top: 1px solid @secondary-blue; } } } That's it! Again feel free to adjust all of the code to your needs. If you have any suggestions how to achieve things a bit easier just let me know.
  13. "Everything" in ProcessWire is a page. So for example I have a "Global Settings" Page (which is actually a page!) in my backend here: Inside this page you can add whatever fields you like to store data that you want to use "globally" on your website. For example text blocks, or repeaters that contain page references to use as a footer menu: In my ready.php file have this global variable that acts as a page reference to the settings page: // Global Settings Page as API variable $wire->wire('globalSettings', $pages->get('template=global-settings')); Then on any template file I can access a field on that global settings page like this: <?= $globalSettings->body; ?>
  14. As far as I know this is a default behaviour on Macs and/or iOS devices. Maybe this plugin can help? https://github.com/lazd/iNoBounce
  15. @apeisa I am using this module on a page and I am getting this error in my log frequently: PHP Warning: Undefined array key "path" in /xxxxx/site/modules/ProcessRedirects/ProcessRedirects.module:653 The PHP Version is 8.1. What causes this message to appear?
  16. @bernhard Thanks for the hint. You can alternatively log out (and stay on the same page) via tracy debugger here. I didn't know this function existed! Just click on Logout (admin) This will be my new solution for this 🙂
  17. I know this code snippet that @Klenkes showed and was using it in some projects, too. In the past I found the reason for this on css-tricks.com, or here: https://snook.ca/archives/html_and_css/font-size-with-rem html {font-size: 62.5%;}/* set scale for the document */ body {font-size:1.6rem;} /* this makes 1.6rem 16px, 2rem 20px, and so on... rem sizes are easier to read that way */ The background is that if you use the markup above you don't need some weird "px to rem calculations" in the frontend. If you want to have a headline that is like 38px in your layout file (photoshop or XD, or whatever) you always had to calculate the REM value for the font size. For example: 38px would be 2.375rem in your stylesheet. That is hard to keep in mind and by just looking at it you get confused. font-size: 38px; font-size: 2.375rem; With the CSS markup above - 38px would be written as "3.8rem" which is far more straight-forward. font-size: 3.8rem; But you have to keep in mind that the base font size has to be changed to make this work. So the base font size hat is used by RockFrontend (16px I assume) does not work anymore for this kind of workflow. So I too would recommend to make the font base size a variable to make easy adjustments. 16 should be the standard value, though.
  18. I want to show a new website that I made at the beginning of this year using @bernhard's RockPageBuilder module: https://www.kurrat-terrassendaecher.de/ The client: Kurrat Terrassendächer (which translates to "Kurrat Terrace Roofs") is a dealer from germany that is specialized in building custom made terrace roofings, awnings and solar protection. The goal: The customer had a old website that was used as a basis for the new design. The new website should offer a more "catalogue-like" look with lots of information for the customer. Fortunately the client had access to high quality images of the products that really make the whole website shine. The page features three main categories: 1. Terrace Roofs 3. Solar Protection 3. Winter Gardens Each category subpage is made of modular content blocks: The user is able to make custom page layouts using these blocks directly in the frontend. With the RockPageBuilder module it is super fun an super straight forward to work with these content blocks. If you don't know this module yet I highly recommend to check it out! If you like the RepaterMatrix you will love this module. It is far superior IMHO! Inserting a content block looks like this: It is also possible to edit content on-the-fly in the frontend: As with the RepeaterMatrix each content block an also be added end edited in the backend of the page: Here you see the list of content blocks used on the home page. To edit a block, just click on it and you can edit it just like using the RepaterMatrix. Screenshots from the website: Modules used for this page: - RockFrontend (for asset bundling, minifying, etc.) - RockPageBuilder (instead of RepeaterMatrix! For building and editing all content in the frontend) - WireMailSMTP - PageImageSource (for creating webp image variations) - PrivacyWire (for cookie consent banner) - SEO Maestro - Redirects The frontend framework is UIKit
  19. I was in the need to add a "log-out" shortcut to the RockFrontend topbar. The motivation behind this is that I am using @bernhard's RockPageBuilder module and when you are logged in and do some frontend-editing you want to have a quick look on the page without the RockPageBuilder markup (all the edit-icons, etc.) . You just want to check how the the page looks like to the user. Yeah, you coooould open the page on a separate browser but I like this solution: In the topbar.php file in the RockFrontend module folder go to line 58 and append this code: <a href="<?=$config->urls->admin?>login/logout/"> <svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M502.6 278.6C515.1 266.1 515.1 245.8 502.6 233.3L374.6 105.3C362.1 92.8 341.8 92.8 329.3 105.3C316.8 117.8 316.8 138.1 329.3 150.6L402.7 224H192C174.3 224 160 238.3 160 256C160 273.7 174.3 288 192 288H402.7L329.3 361.4C316.8 373.9 316.8 394.2 329.3 406.7C341.8 419.2 362.1 419.2 374.6 406.7L502.6 278.7V278.6ZM160 96C177.7 96 192 81.7 192 64C192 46.3 177.7 32 160 32H96C43 32 0 75 0 128V384C0 437 43 480 96 480H160C177.7 480 192 465.7 192 448C192 430.3 177.7 416 160 416H96C78.3 416 64 401.7 64 384V128C64 110.3 78.3 96 96 96H160Z" fill="black"/> </svg> </a> This will render a new "log-out" button in the top bar: I just inserted a random svg code here, so the styling does not 100% fit the look of the other icons. After clicking you will be logged out and redirected to the login-mask. BUT: What I want to achieve was: "Log me out but instead of redirecting me to the login mask, I want to stay on the same page that I logged out from!" 1. We alter the code from above to include a redirect parameter that includes the current page URL: <a href="<?=$config->urls->admin?>login/logout/?redirect=<?=urlencode($page->url)?>"> <svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M502.6 278.6C515.1 266.1 515.1 245.8 502.6 233.3L374.6 105.3C362.1 92.8 341.8 92.8 329.3 105.3C316.8 117.8 316.8 138.1 329.3 150.6L402.7 224H192C174.3 224 160 238.3 160 256C160 273.7 174.3 288 192 288H402.7L329.3 361.4C316.8 373.9 316.8 394.2 329.3 406.7C341.8 419.2 362.1 419.2 374.6 406.7L502.6 278.7V278.6ZM160 96C177.7 96 192 81.7 192 64C192 46.3 177.7 32 160 32H96C43 32 0 75 0 128V384C0 437 43 480 96 480H160C177.7 480 192 465.7 192 448C192 430.3 177.7 416 160 416H96C78.3 416 64 401.7 64 384V128C64 110.3 78.3 96 96 96H160Z" fill="black"/> </svg> </a> 2. For the redirect you have to put this hook function inside your ready.php file: $wire->addHookBefore("ProcessLogin::executeLogout", null, "setRedirect"); function setRedirect(HookEvent $event) { $input = wire('input'); // Check if the 'redirect' query parameter is present if ($input->get->redirect) { // Get the redirect URL from the query parameter $redirectURL = $input->get->redirect; // Set the logout URL to the specified redirect URL $event->object->setLogoutURL($redirectURL); } } After doing that the log-out button will log you out BUT keep you staying on the same page.
  20. @Robin S THIS IS A BLAST! I just testet it on a image-heavy site and it works perfectly. Thanks a lot!!! This definitely belongs to the core of PW in a newer version.
  21. I know this behaviour and it bothers me on every project. I always get a 500 time out error when loading loads of images the first time. This happens mostly in my local dev environment, though. The live server seems to be powerful enough to handle it faster without the timeout.
  22. As one of the earliest RockPageBuilder users (I think you can say so) I have to admit that this is my go-to module since day one. If you liked the RepeaterMatrix module for building pages you will absolutely love this one. In my opinion it is far superior and the frontend editing possibilities seem to be the "missing link" that developers & designers were always looking for. I made around 7 websites with this tool (small and medium in scale) and I definitely will showcase some of them as this module is finally released 🙂
  23. Tutorial Time! So finally I was able to make fetching the Instagram data work. And here's my tutorial how to achieve this. These are my requirements: - Load images from an instagram account and append them into a carousel slider on a website - To reduce loading times and file sizes I want to apply a "scrollspy" behaviour that only fetches data when the carousel is about to enter the viewport - I only load a certain amount of data and once the user is about to reach the end of the carousel, new data is fetched and appended automatically Using the UIKit framework comes in handy here. I am in fact using the UIKit slider component in combination with the UIKit scrollspy component here. Let's start: 1. This piece of code has to be implemented at the very top of your template PHP file (in my case it's the home.php) What this code does is basically: "If the this page is requested through an ajax all then get the media from the instagram account, print it out and then stop processing the rest of the template code." <?php $instagram = $modules->get('InstagramBasicDisplayApi'); if($config->ajax) { echo $instagram->getImages('USERNAME',6); // change USERNAME to the name that is authorized in the InstagramBasicDisplayApi module settings die(); } ?> 2. This is the HTML markup for our slider carousel (using the UIKit slider). Put it into the same template that has the code above. <section class="instagram-feed" uk-scrollspy="cls:uk-animation-slide-bottom"> <div class="uk-container"> <div class="uk-text-center"> <h2>MY INSTAGRAM FEED</h2> </div> <div class="insta-spinner uk-text-center uk-flex uk-flex-column uk-flex-middle"> <div uk-spinner="ratio: 2"></div> <p><b>Loading data...</b></p> </div> <div uk-slider> <div class="uk-position-relative"> <div class="uk-slider-container uk-margin"> <ul class="uk-slider-items uk-child-width-1-2 uk-child-width-1-5@m uk-grid" uk-scrollspy="target: > li > .last-item"> </ul> </div> <div class="uk-visible@s"> <a href="#" class="slider-arrow uk-position-center-left-out uk-position-medium" uk-slider-item="previous"><img src="/site/templates/images/icons/icon-arrow-left.svg" width="30" height="30" alt="Pfeil Links" loading="lazy"></a> <a href="#" class="slider-arrow uk-position-center-right-out uk-position-medium" uk-slider-item="next"><img src="/site/templates/images/icons/icon-arrow-right.svg" width="30" height="30" alt="Pfeil Rechts" loading="lazy"></a> </div> </div> <ul class="uk-slider-nav uk-dotnav uk-flex-center uk-margin"></ul> </div> </div> </section> Note: - I am using a loading spinner right away, this will be removed later on when data is fetched. - We are implementing the scrollspy component for the whole markup block. So when this block enters the viewport an event is fired that allows us to then start fetching data. - We are also implementing the scrollspy component that looks for the <li> with the "last-item" class. This will be our indicator for appending new data later on. - There are currently no <li> elements! That is of of course what we desire. We will load images from instagram and create those <li> elements on the fly. 3. This is the script that will fetch data and append the new carousel items. Put this code below the HTML markup or in a separate JS file: <script> document.addEventListener("DOMContentLoaded", () => { let feed = document.querySelector('.instagram-feed'); let sliderList = document.querySelector('.uk-slider-items'); let spinner = document.querySelector('.insta-spinner'); // listen for the scrollspy 'inview' event and execute this function if the carousel element comes into the viewport feed.addEventListener('inview', () => fetchData()); // create new carousel items and append them to the carousel let appendSlides = function(data) { data = JSON.parse(data); data.forEach((item) => { let image = document.createElement('li'); image.innerHTML = `<a href="${item.link}" target="_blank" rel="noopener" class="uk-display-block uk-height-1-1 uk-position-relative"><img src="${item.src}" alt="${item.alt}"/></a>`; sliderList.appendChild(image); }); // add new 'last-item' class to the second last carousel item so that new data is fetched when this element comes into the viewport const lastItem = sliderList.querySelector('li:nth-last-child(2) > a'); lastItem.classList.add('last-item'); // listen for the scrollspy 'inview' event and execute this function if the last carousel item comes into the viewport if (lastItem) { lastItem.addEventListener('inview', () => fetchData()); } }; // Make an ajax request using the fetch API async function fetchData() { try { const response = await fetch(window.location.href, { method: 'GET', headers: { 'X-Requested-With': 'XMLHttpRequest' } }); let data = await response.text(); // Once data is received pass this data to our function to generate the carousel items appendSlides(data); // and don't forget to remove the loading spinner! spinner.remove(); } catch (error) { console.error(error); } } }); </script> That's it! If everything works correctly it should look like this:
  24. Added before line 439 this does not log anything (but also no error!): $this->log(json_encode($data)); This logs "false": bd(json_encode($data));
  25. This outputs an Array containing all the data I would expect. No errors at all.
×
×
  • Create New...