Leaderboard
Popular Content
Showing content with the highest reputation since 10/21/2024 in all areas
-
The traveling over the last month or so is finally finished. In late September/early October my family traveled to Spain, France, and Italy for the first time. And the last couple weeks my wife and I were in Holland on a bike trip where we lived on a boat for a week and biked all over the Netherlands (~150 miles of biking), and got to see a large portion of it. Our forum administrator @Pete was also there, as was Jan, who maintains our website on AWS, so sometimes it felt like a mini ProcessWire meetup too. The trip was one from Tripsite, a company using ProcessWire for more than 15 years, and this trip was their 25th anniversary. There were about 30 other people there as well, several whom also work ProcessWire as editors. It was an amazing trip, and now I'm completely sold on bike and boat trips being the best way to experience a country. I felt like I was a resident rather than a tourist. I’m sorry there have not been a lot of updates here lately due to all of the travel, but now that it’s done, it’s time to get back to work on our next main/master version, which I’m greatly looking forward to. While there have only been 3 commits this week, there have been 25 commits since 3.0.241, so I’m bumping the dev branch version up to 3.0.242, to get the momentum going again. Thanks for reading, and for your patience while I catch up with communications and such, and have a great weekend! Below is a photo of Pete, Jan and Ryan on the boat in Amsterdam.37 points
-
This week on the dev branch, we have a new $page->preload() method that enables you to preload multiple fields in one query (in this GitHub commit). This is kind of like what the autojoin option does (when configured with a field), or what the $pages->findJoin() method does, but with one big difference. Those options happen as part of the page loading process. Whereas $page->preload() can be applied to a page that has already loaded. Here’s one example where you might find this useful. Say you have a page living at /products/product/ and it has a hundred fields. At the top of your template file that renders the page, you could have a $page->preload(‘field1’, ‘field2’, ‘field3’); to preload all those fields before outputting them. This enables you to load field1, field2 and field3 in 1 query rather than 3. On your first call to $page->field1 it won’t trigger a load from the database and instead will return the value that has already been preloaded. You can also call $page->preload(); without any arguments, and it will preload ALL the supported fields for the page. In reality, ProcessWire is already pretty quick with loading fields, so you probably won’t benefit from preloading until the scale is quite large. While developing this, I was testing by iterating 500 pages and accessing 50 different fields from each. Without preload this took 12 seconds. With preload it took 6 seconds. So for this particular case, preloading cut the time in half. I’m not a query counter, as very often lots of simple DB queries are faster than a single big query, but I’ll mention that it also reduced the quantity of database queries by more than a factor of 10. For this large scale case, that meant more than 20000 queries down to well under 2000. Like with autojoin, there are some limitations with preloading. It supports primarily Fieldtypes that use the core loading mechanism. Some Fieldtypes (especially non-core) override this, and preload() skips over those fields. It also skips over most FieldtypeMulti (multi-row fields), but FieldtypePage is supported when used with Page fields that carry one value. Multi-value can be enabled for testing with an option you’ll find in the function $options, but like autojoin, is limited by MySQL’s group_concat limit. By default that limit is 1024, which supports 140-170 page reference values in a given page field. That's quite a lot, but I don't want to assume folks won't go over that, so it's disabled by default. I’m guessing that most won’t need the preload() function, but a few might really benefit from it, especially at larger scale. So I think it’s a worthwhile addition to the core, and another method that answers a need that may arise as an installation grows, further supporting ProcessWire’s ability to scale up as needs do. Though consider it experimental and "work in progress" at the moment, as we’ll need to do more testing to make sure it is fully stable in a broader range of situations, and further improvements are likely. Special thanks to @tuomassalo at Avoine who came up with the idea for preload() and helped me to get started developing it. Last week I told you how Pete, Jan and I met up in Holland. I also wanted to mention, a couple weeks ago, right before I left for Amsterdam, Oliver from @update AG (update.ch) in Zürich, Switzerland sent me a DM saying that he was in my neighborhood, so we got together for coffee near my house. They’ve been using ProcessWire at Update AG almost as long as PW has been open source, so it was good to meet another long time ProcessWire user, and a nice coincidence that he was in the neighborhood. It’s always great to meet ProcessWire users in person and I hope to meet more of you in the future as well. Thank you for reading and have a great weekend!23 points
-
I didn't know where the right place to share this was on the forums, so I'll post it here since it may be helpful to those getting started with ProcessWire hooks, or some experienced ProcessWire developers who might find them useful. Either way, dear reader, If someone already wrote it, why write it again? If you're someone with experience, feedback is welcome! If there are better or more efficient ways to do something, I would love being a student. Some of these may either address challenges that others have experienced as well or were inspired by the awesome community sharing their solutions. Kudos to all of the people out there helping all of us. If someone sees something that was solved elsewhere please share it in the comments to give credit. I have to make a disclaimer- these have worked for me and while most of them are ready to copy/paste, a few of them are going to need customization or tweaking to make them work for your use case. I don't currently have the resources (time) to provide a lot of support. Some of these were slightly rewritten or adapted for the examples. If you run into issues, the best thing to do is research the solution so that you know exactly what is happening in your application. If you adapt something, fix a bug, or address an edge case, it would be great if you can come back and share that. Be smart, if you're going to run hooks that modify or create data, run a DB backup first. This is the part where I say "I'm not responsible if your site blows up". I don't think that's possible, but do the right thing. There are dozens of hooks in the project I am sharing these from, and to manage that I created a file structure to handle this because there were far too many to put in one file and keeping the init.php and ready.php files clean really makes a huge difference in maintainability. Being able to jump between files by filename is a supremely efficient way to work as well. The filenames don't matter, they're there to identify the files and make it easy to locate/switch between them. Here's my approach to directory organization: /site - hooks -- HookUtils -- init -- lazy_cron -- ready init.php ready.php The ready.php file contents: <?php namespace ProcessWire; if(!defined("PROCESSWIRE")) die(); /** @var ProcessWire $wire */ // Import all ready hooks foreach (glob(__DIR__ . '/hooks/ready/*.php') as $hook) { require_once $hook; } The init.php file contents: <?php namespace ProcessWire; if(!defined("PROCESSWIRE")) die(); /** @var ProcessWire $wire */ // Import all init hooks foreach (glob(__DIR__ . '/hooks/init/*.php') as $hook) { require_once $hook; } // Import all LazyCron hooks, import after init hooks as there may be dependencies foreach (glob(__DIR__ . '/hooks/lazy_cron/*.php') as $hook) { require_once $hook; } Operational Hooks Here are some favorites. Sort items in a Repeater matrix when a page is saved This one helped sort RM items by a date subfield to help the user experience when editing pages. This implementation is configured to only fire on a specific template but can be modified to fire everywhere if modified to check that a field exists on the page being saved first. This was adapted from an answer here in the PW forum but can't find the original post, so I'm going to include it. If you're having issues getting items to sort the way you want, check out this post about natural sorting, which also works elsewhere in ProcessWire. Github Gist Automatically add a new child page under a page having a specific template when created This automatically creates a new child page and saves it when a page having a specific template is created. This also has the ability to show a message to the user in the admin when new page(s) have been created by this hook. It is also error safe by catching any potential exceptions which will show an informative error to the admin user and log the exception message. The messaging/logging operation is abstracted to a separate object to allow reuse if creating multiple pages. Github Gist Conditionally show the user a message while editing a page This one shows a message on a page with a specific template under specific conditions. May be page status, field value, type of user, etc. Visual feedback when editing complex pages can be very helpful, especially when an operation may or may not take place depending on factors like the values of multiple fields. This can reduce the amount of explanations needed on each field or training required for users to use a ProcessWire application. In my case, a message is shown if the state of a page indicates that another operation that is triggered by other hooks will or will not run, which is something that the user doesn't directly trigger or may not be aware of. Github Gist Show the user a message when viewing the page tree This is intended to display a message, warning, or error when the page tree is viewed, such as on login, but in this case executes any time the main page tree is viewed to provide consistent communication and awareness. In my case it displays if there is an activity page located under an "Uncategorized" page for an event. This is something that may be buried in the page hierarchy and not noticeable, but if an activity isn't categorized, then is isn't visible on the website, and if it's not visible on the website, people aren't seeing it or buying tickets. So having a persistent message can bring visibility to important but otherwise potentially unnoticed issues. Or you can just say hi and something nice. Github Gist Hook Enhancement - Fast user switching Hooks can run on triggers that vary widely. Some can and should be identified as those that are triggered by the current user, others may be more autonomous like executing via cron. There may be other hooks that are executed by a user that isn't logged in. Depending on the type of action and your need to identify or track it, switching from the current user to another user created specifically to handle certain tasks can be very helpful. ProcessWire tracks a number of things that are attributed to users- log entries note the user, the user that creates pages is stored, the user that last updated the page is stored, etc. You may want to know who did what when, or only take action if the last user that touched something was X and not Y. I created a separate user that has been provided only the specific permissions it needs to complete jobs that are triggered by hooks or crons. Creating a user with less permissions may also help prevent accidental behaviors, or at least help you be very intentional in determining what actions are delegated. Creating custom permissions is also useful. With a dedicated user I can see explicitly that the last update on some pages were made by an autonomous script that syncs information between the ProcessWire application and a third party platform. Github Gist - Fast user switcher Github Gist - Example of switching users in a hook Fast, powerful, and very (very) easy custom admin buttons I needed a way to add custom interactive buttons that had some specific requirements. Needs to be a button that can be clicked by the user and does something Can be conditionally shown to the user with an alternate message if that action is not available Needs to do something on the server and interact with ProcessWire Here's what that looked like for my application. The green "Refresh Activity" button in the top right. That's a custom button and you don't have to author an Inputfield module to get it. When a user clicks that button, it sends a request to the server with GET variables that are recognized in a hook, actions are taken, then a nice message indicating success or failure is shown to the user. To do this you'll need to install FieldtypeRuntimeOnly and create a new field. Following the documentation for that field, create a button with a URL to the current page with GET variables appended. Then create a hook that watches for the specific GET variable that executes if it's present. Shoutout to @Robin S for helping make short work of a potentially complex task. Note that the field code contains JS that handles the URL on page load. Since the hook is looking for a GET variable in the URL, using the back button or refreshing the page will cause the action to run twice. The JS in that example removes the entry from the browser history and also removes the GET parameter after the page loads if it's present. Github Gist - An example gist for the hook that handles the action Github Gist - An example of the FieldtypeRuntimeOnly code that is displayed and interacted with by the user. Automatically convert logged object or array data to JSON If you're using the outstanding Logs JSON Viewer (yet another great one by @Robin S module, then this hook makes for a thoroughly enjoyable logging experience. Using array or stdClass data when logging your values helps store additional information in an organized way Github Gist <?php $log->save('log_name_here', 'Regular string message'); // Remains a string $log->save('log_name_here', ['gets' => 'converted', 'to' => 'json']); $log->save('log_name_here', (object) ['is' => 'stdClass', 'object' => 'friendly']); Use a separate field to store address data for a FieldtypeMapMarker field This one is really simple, more just sharing an implementation and idea, but proved valuable for reducing data redundancy. I have a FieldtypeMapMarker field but the way that I needed to store address data was much better suited to using multiple fields for things like street, city, state, and zip code. I wanted those fields to be the "controlling" fields for the map marker field to prevent needing to edit 2 fields to keep updated, or accidental content diversion between them. On page save the value from the address fields are pulled and converted into a single string that is added to the FieldtypeMapMarker field's "address" property. I used a Custom Field (ProFields) for my address fields but this can be modified to suit your use case very easily. Github Gist You might also consider hiding the address input on the FieldtypeMapMarker field itself to reduce confusion since the values will be updated automatically anyway. You'll need to have this in a file that is appended to the Admin styles /* You can find the appropriate class for the template you are applying this to in the <body> element when editing a page You can omit that if you want to apply this everywhere */ .ProcessPageEdit-template-your_template_name .InputfieldMapMarker.Inputfield_activity_location .InputfieldMapMarkerAddress, .ProcessPageEdit-template-your_template_name .InputfieldMapMarker.Inputfield_activity_location .InputfieldMapMarkerToggle, .ProcessPageEdit-template-your_template_name .InputfieldMapMarker.Inputfield_activity_location .InputfieldMapMarkerLat, .ProcessPageEdit-template-your_template_name .InputfieldMapMarker.Inputfield_activity_location .InputfieldMapMarkerLng { display: none !important; } <?php // Add this to your ready.php file or ready-firing hook to insert the file containing that CSS to your admin. $config->styles->add("/path/to/your/custom/admin/css/file.css"); Not-A-Hook Bonus - Here's code for an interactive Google Map Renders a Google Map using a FieldtypeMapMarker field, a separate address field, Alpine.js, and Tailwind. You'll need a Google Maps API key, a styled map ID from your Google Developer account, and the aforementioned fields. I wrote it using the latest Google Maps API. Saved you some time. You'll probably need to tweak it. I adapted this so if you find a bug please let me know and I'll update the gist. Note- this makes use of the AlpineJS Intersect plugin to improve performance by only loading/initializing the map when a user scrolls close enough to it. If you don't want that, remove the x-intersect directive. If you want to see it in action, you can check it out here. Github Gist Hook Support Class - A static method class to translate a field into all languages automatically If you use the Fluency translation module, this is a class that will help out with translating a field into all languages programmatically. Sharing this here because the next hook uses this as a dependency. I keep this in the HookUtils directory noted in the file structure above. Usage is demonstrated in the next hook. Github Gist Translate all translatable fields using Fluency on page save whether from UI or API. This is useful for instances where you want a page translated automatically and especially helpful when you are creating pages programmatically. This requires the above hook support class, as well as Fluency connected to an API account. Here are things that must be kept in mind. Please read them, the code for the hook, and the code for the support class to ensure that it works to your needs. You should modify Fluency before using this, really. Change the value of CACHE_EXPIRY on line 19 in the TranslationCache file to WireCache::expireNever. Do this to prevent chewing through your API usage from month to month on repeat translations. This will become standard in the next release of Fluency. This is an expensive operation in terms of API usage, which is why you very much should modify the caching behavior. This hook does not make an effort to determine which fields have changed before translating because it doesn't really matter if the translation is already cached. First time translations of pages with a significant amount of fields/content may be slow, like noticeably slower first time page save because this operation is only as fast as the speed of the request/response loop between ProcessWire and the translation API. Later page saves will be much faster thanks to cached translations. This will not attempt to translate empty fields, so those won't cause any delays. This works with multi-language text/textarea/TinyMCE/CKEditor fields, RepeaterMatrix fields, and the newer Custom Fields (ProFields). Other fields haven't been tested, but it's definitely possible to adapt this to those needs. I prefer to target specific templates with hooks, you can add multiple but be mindful of your use case. Consider adding excluded fields to the array in the hook if it makes sense Consider adding a field to enable/disable translations from the UI, a checkbox field or something This hook is probably one of the uglier ones, sorry. If you run out of API usage on your account, you're going to see a big ugly exception error on screen. This is due to Fluency not handling an account overage error properly because the return type was not as expected. Will be fixed in the next version of the module This is one that may be tailored to my PW application, I think it's general enough to use as-is for your project, but testing is definitely required. Read all the code please. Github Gist ProcessWire Object Method & Property Hooks The following are custom methods that add functionality to native ProcessWire objects. Add a getMatrixChildren() method to RepeaterMatrixPage objects RepeaterMatrix fields represent nesting depth as an integer on each RepeaterMatrixPage item. So top level is 0, first nested level is 1, second 2, etc. When looping through RM items, determining nesting requires working with that integer. It works, but adding adding some functionality helps out. This is infinitely nestable, so accessing children, children of children, children of children of children, and so on works. Fun for the whole family. This was inspired by a forum post, another one I can't find... Github Gist <?php // Access nested RepeaterMatrix items as child PageArray objects $page->repeater_matrix_field->first()->getMatrixChildren(); // => PageArray ?> <!-- Assists with rendering nested RM items in templates Sponsors are nested under sponsorship levels in the RM field --> <div> <?php foreach ($page->sponsors as $sponsorshipLevel): ?> <h2><?=$sponsorshipLevel->title?></h2> <?php if ($sponsorshipLevel->getMatrixChildren()->count()): ?> <ul> <?php foreach ($sponsorshipLevel->getMatrixChildren() as $sponsor): ?> <li> <img src="<?=$sponsor->image->url?>" alt="<?=$sponsor->image->description?>"> <?=$sponsor->title?> </li> <?php endforeach ?> </ul> <?php endif ?> <?php endforeach ?> </div> Add a resizeAspectRatio() method to PageImage objects Adds a simple way to quickly resize an image to a specific aspect ratio. Use cases include sizing images for Google Structured Data and formatting images for consistency in image carousels. Could be improved by accepting second argument to specify an image width, but didn't fit my use case. Github Gist <?php $page->image_field->resizeAspectRatio('square')->url; // Alias for 1:1 $page->image_field->resizeAspectRatio('video')->url; // Alias for 16:9 $page->image_field->resizeAspectRatio('17:10')->url; // Arbitrary values accepted Add a responsiveAttributes() method to PageImage objects Adds a very helpful method to generate image variations and accompanying 'srcset' and 'sizes' attributes for any image. Designed to be very flexible and is Tailwind ready. Responsive sizing can be as simple or complex as your needs require. Includes an optional 'mobile' Tailwind breakpoint that matches a custom tailwind.config.js value: screens: { 'mobile': '320px'}. I added this breakpoint largely to further optimize images for small screens. The array of Tailwind breakpoints and size definitions can be edited to suit your specific setup if there are customizations When sizing for Tailwind, the last media query generated will automatically be switched to "min-width" rather than "max-width" to prevent problems arising from restricting widths. Example, you can specify values only for 'sm' and 'md' and the 'md' size will have the media query correctly adjusted so that it applies to all breakpoints above it. Github Gist <-- The responsiveAttributes() returns a renderable attribute string: srcset="{generated values}" sizes="{generated values}" --> <-- Create responsive images with arbitrary width and height at breakpoints --> <img src="<?=$page->image->url?>" <?=$page->image->responsiveAttributes([ [240, 125, '(max-width: 300px)'], [225, 125, '(max-width: 600px)'], [280, 165, '(max-width: 900px)'], [210, 125, '(max-width: 1200px)'], [260, 155, '(min-width: 1500px)'], ])?> width="240" height="125" alt="<?=$page->image->description?>" > <-- Heights can be selectively ommitted by setting the height value to null --> <img src="<?=$page->image->url?>" <?=$page->image->responsiveAttributes([ [240, 125, '(max-width: 300px)'], [225, null, '(max-width: 600px)'], [280, 165, '(max-width: 900px)'], [210, null, '(max-width: 1200px)'], [260, null, '(min-width: 1500px)'], ])?> width="240" height="125" alt="<?=$page->image->description?>" > <-- Create responsive images with only widths at breakpoints --> <img src="<?=$page->image->url?>" <?=page->image->responsiveAttributes([ [240, '(max-width: 300px)'], [225, '(max-width: 600px)'], [280, '(max-width: 900px)'], [210, '(max-width: 1200px)'], [260, '(min-width: 1500px)'], ])?> width="240" height="125" alt="<?=$page->image->description?>" > <-- Create custom sizes matched to Tailwind breakpoints --> <img src="<?=$page->image->url?>" <?=$page->image->responsiveAttributes([ 'mobile' => [240, 125], <!-- Custom tailwind directive --> 'sm' => [225, 125], 'md' => [280, 165], 'lg' => [210, 125], 'xl' => [260, 155], ])?> width="240" height="125" alt="<?=$page->image->description?>" > <!-- Resizes width of image to fit Tailwind breakpoints, useful for full width images such as hero images, doesn't change height. Also accepts 'tw' as an alias for 'tailwind' --> <img src="<?=$page->image->url?>" <?=$heroImage->responsiveAttributes('tailwind')?> width="240" height="125" alt="<?=$page->image->description?>" > Add PHP higher-order function methods to WireArray and WireArray derived objects WireArray objects are incredibly powerful and have tons of utility, but there are situations where I find myself needing to work with plain PHP arrays. I'm a very big fan of PHP's array functions that are efficient and make for clean readable code. I found myself often reaching for $wireArrayThing->getArray() to work with data then using functions like array_map, array_filter, and array_reduce. These return arrays, but could easily be modified to return WireArray objects if that is more helpful. Github Gist <?php // The EventPage page class has a method that determines sold out status from more than one source of data/page fields // which means that it isn't queryable using a ProcessWire selector. This returns a single integer calculated from ticket availability // of all events from non-queryable data. $totalEventsAvailable = $eventPages->reduce( fn ($total, $eventPage) => $count = $eventPage->isActive() ? $total++ : $total, 0 ); // Requires using a page class to determine status reliant on multiple data points not queryable via a selector. Knowing what the event // page is for an activity can't be determined using a selector for activity pages. $displayableActivities = $matches->filterToArray( fn ($activityPage) => $activityPage->eventPage()->isPublic() && $activityPage->isActive() ); // Iterating over each Page in a PageArray and processing data for sorting/ordering before rendering on a search results page // Executed within a page class $results = $pages->mapToArray(function($page) { return (object) [ 'page' => $page, 'summary' => $this->createResultSummary(page: $page, maxLength: 750), 'keywordMatchCount' => $this->getQueryMatchCount(page: $page), ]; }); Add an image orientation method/property to PageImage objects Get the portrait or landscape orientation of a PageImage. Github Gist <?php $page->image->orientation; $page->image->orientation(); Add the ability to get all related pages for Page objects at once Gets all of the related pages to a page at once by both page reference fields and links in fields. Transparently passes native arguments to Page methods for native behavior Github Gist <?php $page->allPageReferences(); $page->allPageReferences(true); // Optionally include all pages regardless of status $page->allPageReferences('your_selector=here', 'field_name'); // Use with native Page::references() and Page::links() arguments Add a saveWithoutHooks() convenience method to Page objects The number of hooks in my most recent project was... a lot. There were many that hooked into the page save event and a lot of operations that happen in the background where pages needed to be modified and saved quietly to prevent clearing ProCache files or excessive DB operations through chained hooks. Being able to use a method to do this rather than passing options felt more deliberate and clear when working across hundreds of files and in critical areas of very expensive operations. This method also accepts page save options, but in a way that hooks will always be disabled even if an option is accidentally passed enabling them. Furthermore, it also accepts a string as the first argument that, if debug mode is enabled, will dump a message to the bar via Tracy. Github Gist <?php // Adding a message can be very helpful during testing, especially when saving a page with/without hooks is conditionally based // where the result of another operation determines how a page is saved $page->saveWithoutHooks('updated event sync data hash, saved without hooks'); $page->saveWithoutHooks(['resetTrackChanges' => true]); $page->saveWithoutHooks('message and options', ['resetTrackChanges' => true]); These are a few that I've used to show some diversity in application. Hooking to ProcessWire events makes it possible to build beyond simple websites and implement truly custom behavior. Hope these may be useful to others. If you have any favorite hooks of your own, have corrections of those I've shared, or improvements, sharing them in the comments would be stellar. Cheers!21 points
-
This week most of the core dev branch commits are related to minor fixes and improvements. While last week we added a new $page->preload() method, I’m going to avoid more major additions or features so that we’re not creating more things that need lots of testing. For that reason, the commits over the next weeks or month will be similar to those from this week, so that we can get a new main/master version out as soon as possible. I was just looking at the date of our last master version (3.0.229) and see that it’s been more than a year! It feels like it’s been 3 months to me — time sure does fly! Seeing how long it’s been definitely motivates me to not wait too much longer on this next main version. The current dev branch fixes and adds quite a few things relative to 3.0.229 as well, so I think of it as being the more stable version at this point… a good sign it’s about time for a new release version. Thanks for reading and have a great weekend!17 points
-
Hello, I have created a simple module to preview theater seat reservations. This is my very first module, so be gentle as I'm not a coder. What is it about? The module creates 5 fields that must be added to the template of your choice. (e.g. event-post) In the template, you can then set the number of rows and the number of seats in each row. After save your preview is created. You can then book or cancel seats by clicking on the seats boxes or trash icon to cancel them. We have a small theater and sometimes we remove some seats, so I also added the option to remove them. Seat-booking.mp4 You can the render this on your frontend with: <?php // Assuming $page is the current page object $rows = $page->rows ?: 9; // Default to 9 rows if not set $seatsPerRow = $page->seats_per_row ?: 8; // Default to 8 seats per row if not set // Load the existing CSS for styling $cssFile = $this->wire()->config->urls->siteModules . 'TheaterSeating/styles.css'; echo '<link rel="stylesheet" href="' . $cssFile . '">'; // Start the seating chart output echo '<div class="theater-seating">'; // Loop through rows for ($i = $rows; $i > 0; $i--) { echo '<div class="row">'; // Start a new row echo '<div class="row-label">Vrsta ' . $i . '</div>'; // Row label // Loop through seats for ($j = 1; $j <= $seatsPerRow; $j++) { $seatId = "$i-$j"; $occupiedClass = in_array($seatId, explode(',', $page->booked_seats ?: '')) ? 'selected' : ''; $disabledClass = in_array($seatId, explode(',', $page->disabled_seats ?: '')) ? 'disabled' : ''; // Output the seat div echo '<div class="seat ' . $occupiedClass . ' ' . $disabledClass . '" data-seat-id="' . $seatId . '">'; // Add the cross overlay for disabled seats if ($disabledClass) { echo '<div class="cross">✖</div>'; // X overlay } echo '</div>'; // Close seat div } echo '</div>'; // Close row div } echo '<div class="stage">Oder</div>'; echo '</div>'; // Close theater seating div ?> and maybe style with: .seat { width: 50px; height: 50px; margin: 0 5px; /* Horizontal margin between seats */ background-color: #ccc; cursor: default; /* Change cursor to indicate no interaction */ display: flex; align-items: center; justify-content: center; position: relative; } .seat.occupied { background-color: #f00; /* Red for occupied seats */ } .seat.selected { background-color: #0f0; /* Green for selected (booked) seats */ } .seat.disabled { background-color: rgba(255, 0, 0, 0.5); /* Semi-transparent red for disabled seats */ } .cross { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: rgba(255, 0, 0, 0.5); /* Semi-transparent overlay */ display: flex; align-items: center; justify-content: center; font-size: 24px; color: white; } .row-label { font-size: 16px; margin-right: 10px; /* Space between the label and seats */ font-weight: 600; width: 100px; /* Set a fixed width to align labels */ } I hope someone will find it usefull. Fell free to make it better. 😉 You can download it here: TheaterSeating.zip Cheers 😉 Roych17 points
-
11 points
-
Here's my next ProcessWire build. After dipping my toe in the ProcessWater with a simple holding page ( https://www.threehills.farm/ ) followed by a blog ( https://www.eltikon.online/ ) that I shared here, this time I have focussed on a site that uses the Repeater Matrix to manage blocks of content, in order to see how easy it is to transfer my WordPress "Advanced Custom Fields Flexible Content" approach to ProcessWire. Result: it's really easy once you get the hang of it. Site: https://www.zigpress.com/ This is my business site, and is a single-page site (plus 404 etc) where each section of content is managed in a Repeater Matrix. Modules installed: AdminStyleRock (so I can use my Nord admin stylesheet!), Pro Fields Repeater Matrix, Seo Maestro, FrontendForms, Less, Redirects, my own ZP Traffic module, and Wire Mail SMTP. I also included Perishable Press's 7G Firewall. I still have more testing to do now that it's live, but it seems to be behaving OK so far. All feedback welcome, including criticism/bugs/whatever.11 points
-
Is preload() already available in the DEV branch? I'm asking because the version is still 3.0.242. What would be the best way to really test the performance boost here? Need to try this in a recent project. Maybe we should create some kind of meetups in the US and maybe even in the EU next year. That would be fun.8 points
-
Hello Everyone, For our KIT325 Cybersecurity Project, we recently checked the security of ProcessWire CMS, a system used for managing website content. We wanted to see if its default settings are secure enough based on the OWASP Top 10 standards, which are common web security guidelines. Here’s a quick look at what we found and what could be improved: Blocking Brute Force Login Attempts: What We Found: ProcessWire does slow down login attempts if someone keeps trying the wrong password. But it only blocks based on username, not by tracking where the login attempts come from (like IP addresses). Suggestion: It would be safer if ProcessWire blocked login attempts based on IP as well. Also, the system could use a response code like “429 Too Many Requests” to alert attackers that they’re being blocked. Session Cookie Security: What We Tried: Session cookies (used to keep users logged in) seem secure, but we couldn’t fully test if they were safe from all advanced attacks. Future Testing: We’d need more tools and knowledge to explore if these session cookies could ever be forged to trick the system. File Access Control: What We Saw: Files from unpublished pages could still be accessed if someone knew the file path, which could leak private information. Fix: ProcessWire should make a certain setting ($config->pagefileSecure) enabled by default to restrict file access based on page permissions. This way, only authorized users can see those files. HTTPS (Secure Connection) Enforcement: Current Setup: ProcessWire requires HTTPS (secure connection) settings to be turned on manually in the .htaccess file, which may not be done by every user. Recommendation: It would be better if HTTPS were enabled by default, so all sites are secure right from the start. Improving Activity Logs: Missing Logs: Some important activities like content changes and role updates aren’t logged by default. Suggestion: ProcessWire should add logs for these actions. This way, any unusual activity can be tracked and traced back to the user who made the changes. Password Rules: Issue: Passwords set through the API (another way to interact with the system) might not meet the same security rules as those set in the admin panel. Improvement: ProcessWire should require all passwords to meet the same standard, ideally making them at least 12 characters long and easier for users to remember. Overall, ProcessWire has a strong security foundation, but these adjustments could make it even safer. This experience showed us the value of secure default settings, especially for users who might not make these changes on their own.8 points
-
7 points
-
Ryan - it was great to meet you and Jan in person finally after 12 years 😊 That was a fantastic trip with a great group of people through some really interesting locations. We were pretty lucky with the weather too!6 points
-
Big thumbs up for that idea. If the timing is right, I might even make it to both, since I'm planning to head across the pond and hike the John Muir Trail next summer. Love the preload method. I've got a shop solution built on PW with about 120k orders and 150k order positions by now, all as pages. Performance is becoming a bit of an issue in the backend with long range price change statistics and detailed Excel exports, and this seems like a perfect use case.5 points
-
When you encounter a bardump output or errors in TracyDebugger, a link typically appears below the message, allowing you to open the file and line where the output occurred, with VSCode as the default editor. However, in a Windows WSL2 environment, this feature doesn’t work by default. To enable these links, add the `editor` and `localRootPath` variables to the TracyDebugger module's config or in your `site/config-dev.php` (or site/config.php). Here’s an example in my config-dev.php that works for me. Make sure to use `vscode://vscode-remote/wsl+nameOfYourDistro/pathToYourFiles/%file:%line`. This establishes a remote connection to the selected Linux distro. $config->tracy = array( 'frontendPanels' => array('mailInterceptor', 'panelSelector'), 'nonToggleablePanels' => array('mailInterceptor', 'tracyToggler'), 'outputMode' => 'DEVELOPMENT', 'forceIsLocal' => true, 'guestForceDevelopmentLocal' => true, 'editor' => 'vscode://vscode-remote/wsl+Ubuntu22.04/home/jmartsch/htdocs/fugamo/fugamo-shop/%file:%line', 'localRootPath' => 'dist/', 'backendPanels' => array( // 'processwireInfo', // 'requestInfo', // 'processwireLogs', // 'tracyLogs', // 'methodsInfo', // 'debugMode', // 'console', 'mailInterceptor', 'panelSelector', 'tracyToggler' ), ); Now you can click on the filename, and get directly to the corresponding line.5 points
-
This powerful module has been under ongoing development for several years. The history dates back to the year 2016 🤯 Back then it was built on top of https://datatables.net/. Later I switched to https://www.ag-grid.com/ and finally settled with https://tabulator.info/ to do the heavy lifting. With RockGrid you can display any kind of tabular data on the ProcessWire backend. It helps you structure your code in a way to keep it maintainable and it comes with a lot of helpers that customise tabulator to the needs of ProcessWire. Download & Docs: baumrock.com/RockGrid5 points
-
https://processwire.com/docs/security/admin/#preventing-dictionary-attacks For sites with simultaneous users coming from the same shared IP address, throttling by IP address may lock out legitimate users. Had this scenario with a project with about 1.000 frontend user accounts, which could sign in for courses. All get an E-Mail with their login credentials at about the same time. We had about 50-100 users from a big company using a shared IP address. Here some (5-10) of those users where blocked. So I allowed some IP ranges to not lock out legitimated users sharing the same IP address, simply to reduce the support request for my clients site operators. If this scenario doesn‘t matter for your sites, I would always turn on throttling by IP address.5 points
-
Thanks for posting @omshah. I was also a part of this assessment group, in my day job I work on antarctica.gov.au, and several other large Processwire sites. What are the impacts of having it enabled by default? Is it just extra overhead? Certainly agree that permissions changes should be logged somewhere for accountability purposes. Not sure if it should be a new log, or part of the session log? Maybe different is best. Upon reflection, I think you're right here @teppo - I think 429 is best returned for legitimate (authenticated) responses to something like an API to indicate that whilst successful and allowed, the rate limit has been exceeded. It is best to hide the fact any security actions have occurred. Overall Processwire is so solid, I've used it for over 12 sites now. Everything from small business to large government entities - it's such a blast to work with.5 points
-
I needed to do this and thought the code might be useful for others too. In my case the organisation has a main site (Site A) and a related but separate site (Site B). The objective is for the users at Site B to be automatically kept in sync with the users of Site A via PW multi-instance. Users are only manually created or deleted in Site A. Both sites have the same roles configured. // InputfieldPassword::processInput $wire->addHookAfter('InputfieldPassword::processInput', function(HookEvent $event) { /** @var InputfieldPassword $inputfield */ $inputfield = $event->object; $input = $event->arguments(0); /** @var UserPage $page */ $page = $inputfield->hasPage; if($page->template != 'user') return; // Return early if there are any password errors if($inputfield->getErrors()) return; // Get the new password as cleartext from $input $pass = $input->get($inputfield->name); if(!$pass) return; // Set the password as a custom property on the Page object $page->newPass = $pass; }); // Pages::saved $pages->addHookAfter('saved', function(HookEvent $event) { /** @var UserPage $page */ $page = $event->arguments(0); if($page->template != 'user') return; if($page->isUnpublished()) return; // Update or create user in Site B $site_b = new ProcessWire('/home/siteb/siteb.domain.nz/', 'https://siteb.domain.nz/'); /** @var UserPage $u */ $u = $site_b->users->get($page->name); // Create a new user if none exists with this name if(!$u->id) $u = $site_b->users->add($page->name); // Set the password if the custom property was set in the InputfieldPassword::processInput hook if($page->newPass) $u->pass = $page->newPass; // Set email address $u->email = $page->email; // Set roles $u->roles->removeAll(); foreach($page->roles as $role) { $u->addRole($role->name); } $u->save(); }); // Pages::deleteReady $pages->addHookAfter('deleteReady', function(HookEvent $event) { /** @var Page $page */ $page = $event->arguments(0); if($page->template != 'user') return; // Delete user in Site B $site_b = new ProcessWire('/home/siteb/siteb.domain.nz/', 'https://siteb.domain.nz/'); $u = $site_b->users->get($page->name); if(!$u->id) return; $site_b->users->delete($u); }); This assumes the use of the default "user" template and not an alternative template. In my case the user template only has the default fields, but the code could be adapted if you have additional fields in your user template. This doesn't handle renaming of users as that's not something I have a need for. But there would be ways to achieve this too, e.g. store the user ID for Site B in a field on the user template in Site A, and then get the Site B user by ID rather than name.5 points
-
Another module that was created through the development of RockCommerce 😎 Ever needed a list of all countries of the world? Wanted to show a subset? Needed to translated them to other languages? Check out RockCountries, which can help you with these things: // using tracy debugger's bd() bd(rockcountries()->countries()->get('alpha2=aut')); Download & Docs: baumrock.com/RockCountries5 points
-
https://github.com/processwire/processwire-issues/issues/1516 https://github.com/processwire/processwire-issues/issues/18235 points
-
TinyMCE itself supports this via the style_formats setting, where you would define a "class" value rather than a "classes" value, e.g. style_formats: [ { title: 'Table row 1', selector: 'tr', class: 'tablerow1' } ] But PW's implementation of TinyMCE doesn't provide a way to directly control this setting and instead parses the contents of the "Custom style formats CSS" config textarea into the style_formats setting. Situations like this are why I think PW should provide a hookable method allowing the array of data that becomes the TinyMCE settings to be modified at runtime, as mentioned in this issue: https://github.com/processwire/processwire-issues/issues/19814 points
-
Hetzner is quite cheap and reliable, and they make it easy to change PHP configurations. I also like their transparency with prices and communication. The control panel is admittedly not very attractive or intuitive, but we can't have it all, can we? 🤷4 points
-
Hehe, ProcessWire Weekly #546 RSS just dropped into my Reeder app and it seems ZigPress is site of the week! 😊 I'm extremely flattered and honestly quite surprised considering the high standard of sites in the showcase... thanks to whoever made the choice.4 points
-
you will find on the forum and internet a lot of threads about getting started on pw. There is one nice guide made by a big company I like to quote since: https://www.ionos.com/digitalguide/hosting/cms/processwire/ ps: nice move 🤙4 points
-
Perhaps this will may help you: https://youtube.com/playlist?list=PLOrdUWNK38ibz8U_5Vq4zSPZfvFKzUuiT&si=eZK8fSrXVWJs65_k4 points
-
It was a pleasure speak with you today @bernhard. For the others wondering, we mainly talked about "rabbit hole" you can go down with ecommerce and how there has to be a line drawn between what one gets with a fresh installation vs. what one must build on their own. Basically, how turn-key the solution is and how that relates to the target audience the system would be for.4 points
-
In case you need it, DDEV Manager for VS Code.3 points
-
DDEV works great on Ubuntu in WSL2 on Windows 11. Installed VS Code in Windows with an extension to work remote on Linux too. So I do all my Web dev work in Ubuntu. Ubuntu file system shows up in Windows explorer too. This makes it easy to transfer files from Ubuntu to the WWW server using Filezilla on Windows by just using the local path to Ubuntu. Copying files from Windows to Ubuntu can easily be done via cp /mnt/c/Users/user/Downloads/file.ext . too. So WSL2 in comnination with DDEV and VS Code is really fun.3 points
-
I hate to break it to you, but... you are now 😉 Seriously though, thanks for sharing this module!3 points
-
ProcessWire's Page constructor: https://github.com/processwire/processwire/blob/3cc76cc886a49313b4bfb9a1a904bd88d11b7cb7/wire/core/Page.php#L594 public function __construct(Template $tpl = null) { parent::__construct(); if($tpl !== null) { $tpl->wire($this); $this->template = $tpl; } $this->useFuel(false); // prevent fuel from being in local scope $this->parentPrevious = null; $this->templatePrevious = null; $this->statusPrevious = null; } So in extending custom page calsses I do this: function __construct(Template $tpl = null) { parent::__construct($tpl); // my initializations go here... } I had no issues with it so far.3 points
-
I have a little update on this procedure: When disabling the two-factor authorization it is straight-forward and fast to add the customers instagram account into the app. First you don't have to ask the customer for the security code (on each login attempt) and second you don't need to add the clients instagram account to your facebook accounts center in the first place. When the app is working the client can feel free to re-activate the two-factor authorization.3 points
-
You might be experiencing this issue: https://github.com/processwire/processwire-issues/issues/1952 https://github.com/processwire/processwire-issues/issues/1974 To fix you can update to the latest PW dev version so as to get this commit, or change this line of .htaccess to: RewriteRule ^(.*)$ index.php?it=$1 [L,QSA,UnsafeAllow3F]3 points
-
Hey @olivetree thank you for your question. I think in terms of use cases you can do quite the same with both modules. (See note at the end) The main difference between both modules is how/where data is handled. ListerPro handles all data on the server side, which means there is basically no limit in terms of scale. Pagination etc. is all done by the backend (php) and only small chunks of the data are sent to the client. With RockGrid, on the other hand, all the data is sent to the client at once and that data is then handled by the client. That has the drawback that you might hit limits earlier than with ListerPro, but it has the benefit that sorting and filtering is done on the client and produces instant results. There is no need for any ajax requests, no waiting for receiving the data, etc.; Another benefit of using RockGrid is that you have unlimited possibilities in HOW you present your data. The downside is that you need to define all that with a mix of PHP (the data selection, basically just a PW selector) and JS (the visual part). With ListerPro you can build your data listings via GUI with just a few clicks. That means that you are very limited in terms of visually presenting data. In terms of scalability RockGrid should be fine quite far, though. Users reported good results with grids having 75 columns and up to 150.000 rows! That's a lot. It always depends on the device though, but I've never had tables with 75 columns and less columns means more rows possible. If you have a look at the demo image of RockGrid: How would you build that with ListerPro? BUT: ListerPro will show page actions by default and you will not have to do anything. With RockGrid every piece of the representation comes from code, so if you need page actions you need to add code to do so (there are helper functions there, but it's more work than with ListerPro). Oh, I almost forgot 🙂 You can use RockGrid as an Inputfield as well! For the RockCommerce module I'm using RockGrid to select the variations of a product. You can filter by variation name, then select all options of the variation by clicking the variation column, then deselect single items that you don't need by clicking on the option column: So RockGrid can not only be used for similar use cases like ListerPro but also for use cases that you have used page reference fields in the past. 😎 I'll have to write better docs and make a video about it, but I wanted/needed to release it as it is a dependency for RockCommerce and for that module you don't need to create any grids on your own, you just install the module and use it. That's why for RockCommerce the RockGrid docs are not a necessity. It's a really powerful module and it has gone a long way. 🙂 Does that answer your question? PS: Oh and here is an example how you can use it for more complex input scenarios, like adding prices and doing calculations on the fly: That's also very different to what ListerPro offers 🙂3 points
-
How did you use to do it? I think most of our solutions should apply to any PHP based CMS.3 points
-
Hi there. I'm using ProcessWire on Litespeed servers for 10+ years. There are NO compatibility issues. Everything you do with regular Apache you can do on Litespeed too. Google "Litespeed vs Apache" and you can easily find out why you should stay with Litespeed 🙂3 points
-
You can do something like this: $wire->addHookBefore('Inputfield::render', function(HookEvent $event) { /** @var Inputfield $inputfield */ $inputfield = $event->object; $process = $this->wire()->process; // Return early if this is not ProcessPageEdit if(!$process instanceof ProcessPageEdit) return; // The page being edited $page = $process->getPage(); // The field associated with the inputfield, if any // Useful for when the inputfield is in a repeater, as the inputfield name will have a varying suffix $field = $inputfield->hasField; // The page that the inputfield belongs to // Useful for identifying if the inputfield is in a repeater $inputfield_page = $inputfield->hasPage; // Return early if this is not a page we are targeting if($page->template != 'test_combo') return; // Do some check to identify the inputfield by name or field name if($field && $field->name === 'text_1' && $inputfield_page->template == 'repeater_test_repeater') { // Check some other field value if the message depends on it if($page->test_combo->my_date === '2024-10-18 00:00:00') { // Show an error message $inputfield->error('This is a test error message'); } } // Do some check to identify the inputfield by name or field name if($inputfield->name === 'test_combo_my_date') { // Check some other field value if the message depends on it if($page->test_repeater->first()->text_1 === 'hello') { // Show an error message $inputfield->error('Another test error message'); } } });3 points
-
Sure @flydev If anyone is wondering what this thread is about: https://baumrock.github.io/Consenty/3 points
-
In case you have no plans on the weekend: https://processwire.com/docs/tutorials/ The tutorials are awesome and a great starting point. Let's say you know a bit about programming, the absolute foundation of if/else and echo/return, and a bit of HTML/CSS/JS... you could be up and running within a weekend. That's how I got into ProcessWire and started loving it.3 points
-
Hey @Jonathan Lahijani just wanted to thank, because even though I'm sure that was not your intention, you made me remember and try out FieldsetPage. This will not only be very helpful for RockCommerce and for RockSettings, it is also a great way to make things that are under control of RockMigrations extendable by users/developers 😎 It also led to a knew PR that I just submitted: https://github.com/processwire/processwire/pull/306 Like it if you like it 😄3 points
-
This seems to be because the XHR request that gets the live results as JSON requests the same URL and headers as a normal visit to the search page, so the browser gives you its cached version. You can also observe it the other way around: If you head to https://processwire.com/search/?q=hello and then use the search box for the same keyword, the browser console will show a JSON parse error because the live search got the full page instead of JSON. I imagine this could be fixed by sending the ol’ X-Requested-With: XMLHttpRequest header. Or by using the appropriate content negotiation header (I think “Accept: application/json”?).3 points
-
$wire->addHookBefore('Inputfield::render', function (HookEvent $event) { $inputfield = $event->object; // Proceed only for 'yourField' if ($inputfield->name !== 'yourField') { return; } // Get the associated page if (!($page = $inputfield->hasPage)) { return; } // Check if the page is part of the yourPageArrayField on page with id 1 // (or whatever the ID of the page with your pageArraField is) $productPages = pages()->get(1)->yourPageArrayField; if ($productPages->has($page)) { $inputfield->attr(['checked' => 'checked', 'value' => 1]); } });3 points
-
@bernhard I didn't found a thread about Consenty so im writing here. Are you wiling to accept a PR about making Consenty as node.js module? Creating a npm account isn't necessary as we can install libs from github. It wouldn't change nothing for people grabbing the minified file but it would be easier for installing and using it for people building apps using npm or yarn or whatever. I just realize that consenty isn't a pw module and I should have opened an issue on github 😅2 points
-
I have started with this playlist on YouTube: https://www.youtube.com/playlist?list=PLbrh75U7tpN97Gs_GQcyz1FiXsLnG_W-Q It will show you how to make a blog, from scratches and step by step. You will be able to grasp some ProcessWire specificities which make it so efficient, powerful and easy to develop with.2 points
-
I spent the last two days going back and forth between FieldtypeRepeater and FieldtypeCombo for some fields, but FieldsetPage was exactly what I was looking for! I wish I had discovered it earlier 🤦♂️ Thanks @bernhard!2 points
-
Payment Module for RockCommerce that can also be used as standalone module. Download & Docs: baumrock.com/RockMollie2 points
-
2 points
-
Hi @adrian. Thanks for the comments. That was the intention, but I missed it in one important place - hopefully now fixed. I think my new version handles this. Ditto. My code only replaces config items which have page, field, template, role, or user in the key name (case insensitive) and where the value is numeric and is a valid id for the related type. Hopefully this avoids the problem you mention. If someone builds a module with a type name included in the config key with numeric values which are valid ids, but where the values are not intended to refer to ids of that object type, then it won’t work, but that just seems really perverse to me! I have uploaded the new code to my GitHub and have incremented the version and amended the ReadMe. Obviously it is pretty hard to test it on all modules, certainly as regards importing, but it seems ok so far. Let me know if you find any exceptions. One module that causes problems is (deprecated?) ProcessEmailToPage but it is a problem even without my changes. That is because the configdata is wrong - it self-overwrites - so not something that your module can fix, I think.2 points
-
2 points
-
@bernhard Mollie is supported ONLY on 32 countries and half of them are required to have a minimum sales volume. You will instantly exclude potential customers from many countries including USA. More info here. Stripe is supported on 50 countries and PayPal on more than 200. I think it will be better if you create a system with external payment modules where the RockCommerce clients will be able to use your ready-made payment modules for Mollie, Stripe, PayPal or they can create their own for any other payment provider. I don't know if Mollie supports it but PayPal and Stripe supports seamless integration too. The user completes the payment without leaving the client's website which is by far the best solution. I'm still a Padloper 1 user and created my own modules to do that for PayPal and Stripe (I haven't released it yet).2 points
-
I have to implement Google Consent v2 together with PrivacyWire for the first time. I tried to summarize the most imporant things and make a quick example. Hope that helps. Improvements welcome. Basically Google wants you to send the user preferences. This way Google can process at least anonymized data if the user denies permissions (and promises to respect privacy). Luckily PrivacyWire gives us the possibility to define a custom js function in the module configuration, which is executed whenever user preferences change. PrivacyWire stores user preferences in localStorage. From there we can fetch this information when needed. So we have to: 1. Integrate Google Tag Manger without modifying the script tag. 2. Set Consent v2 defaults (by default deny every permission): <!-- GOOGLE CONSENT V2 --> <script async src="https://www.googletagmanager.com/gtag/js?id=GTM-ID-HERE"></script> <script> window.dataLayer = window.dataLayer || []; function gtag(){dataLayer.push(arguments);} gtag('consent', 'default', { 'ad_storage': 'denied', 'analytics_storage': 'denied', 'ad_user_data': 'denied', 'ad_personalization': 'denied' }); gtag('js', new Date()); gtag('config', 'GTM-ID-HERE'); </script> <!-- End GOOGLE CONSENT V2 --> 3. Use a function called by PrivacyWire, when user changes preferences. Fetch user preferences from localStorage and send them to Google: function updateConsentFromPrivacyWire() { console.log('update consent from privacy wire...'); const privacyWireData = localStorage.getItem('privacywire'); if (privacyWireData) { try { const consentData = JSON.parse(privacyWireData); const consentPreferences = { // Set Google params based on user preferences 'ad_storage': consentData.cookieGroups.marketing ? 'granted' : 'denied', 'analytics_storage': consentData.cookieGroups.statistics ? 'granted' : 'denied', 'ad_user_data': consentData.cookieGroups.marketing ? 'granted' : 'denied', 'ad_personalization': consentData.cookieGroups.marketing ? 'granted' : 'denied' }; // Update google consent gtag('consent', 'update', consentPreferences); console.log(consentPreferences); } catch (e) { console.error('Error parsing PrivacyWire-Data:', e); } } else { console.warn('No PrivacyWire-Data found in localStorage'); } } // Update consent at pageload document.addEventListener('DOMContentLoaded', updateConsentFromPrivacyWire); 5. This is what the parameters control: ad_storage: Controls whether ad-related cookies and identifiers are stored (e.g., for remarketing). analytics_storage: Controls whether analytics-related cookies are stored (e.g., for Google Analytics tracking). ad_user_data: Controls the collection and use of user data for advertising purposes. ad_personalization: Controls the use of data to personalize ads based on user behavior and preferences. 4. Configure the function name (here updateConsentFromPrivacyWire) in PrivacyWire module settings.2 points