Create a guestbook in [Agency]

After trying to use the Guestbook plugin in my Agency themed site, witch result as too hard for me, lets try thing differently !

So now my next option is to try Comments plugin.
When searching for the keywords comments + agency, I get only my own post, so no feedback available on the forum for this combination : Did someone already experienced something similar ?

@citoyencandide, Sharing you requirements might be helpful because a ‘guestbook’ could be very simple, or more complex.

A very simple solution could be a ‘contact’-like form that sends a notification email and saves the form into a file, like plugin Guestbook and Comments do. A small plugin can then read the file, pass the reviews into a Twig template that formats the list. No interface in Admin, just manually editing the file containing the feedback and set the ‘approved’ variable to true.

A more complex scenario is that an end-user must be able to approve / disapprove / delete reviews from Admin. Is pagination required for a long list of reviews?

Simple vs complex depends on your requirements.

Simple is beautiful :

A form where visitor submit a comment, witch is concatenated with previous comments.
Manual edition is fine to me to set the approved messages.
Pagination, you mean several columns ? It’s a good idea ^^

But in my mind, a typical guest book is a blank notebook in which the comments are manually written one below the other …

on internet, comments are more likely written one above the other, so it would be preferable, all on one column.

@citoyencandide,

Pagination, you mean several columns ?

No, pagination is often used in Blogs where there are many blog-items. By pagination only 5-10 blog items are shown at the same time. Using buttons one can see the next/previous set of 5-10 blog-items.

Ok, I’ve a had a little fun and tried to create a proof-of-concept of my own suggestion… Here is how to get a very simple and unpolished guestbook in theme Agency.

Setup:

  • Download and install skeleton Agency
  • Install Devtools to easily create themes/plugins: $ bin/gpm install devtools

Theme:

  • To prevent loss of modification when Agency gets updated, create an inheriting (child) theme of Agency: $ bin/plugin devtools new-theme.
    Name the theme “MyAgency”
  • Tell Grav in /user/config/system.yaml to use the new theme:
    pages:
      theme: my-agency
    
  • In our child-theme, we are going to override template /user/themes/agency/templates/modular/form.html.twig, to fix a few things.
    • Copy file /user/themes/agency/templates/modular/form.html.twig into folder /user/themes/my-agency/templates/modular/
    • Above line 10, add the following:
      {% set form = forms(page.header.form.name) %}
      
      This will fix collisions with the form for the guestbook.
    • To fix non closing elements, add the following at the bottom of the file:
          </div>
        </div>
      </section>
      

Plugin:

  • Create your own custom Guestbook plugin: $ bin/plugin devtools new-plugin.
    Name the plugin SimpleGuestbook. The name Guestbook is not possible because it already exists.

  • Add the following to the configuration file user/plugins/simple-guestbook/simple-guestbook.yaml:

    enabled: true
    addCss: true      # true|false to add css to give Guestbook same styling as Contact
    routes:           # Add routes on which Guestbook should be displayed
      # - /             # Home page
    
  • Copy above config file into folder user/config/plugins/ and alter for your environment.

  • Add logic to user/plugins/simple-guestbook/simple-guestbook.php to retrieve the items from the Guestbook. Replace entire contents with the following code:

    Content of simple-guestbook.php
    <?php
    
    namespace Grav\Plugin;
    
    use Composer\Autoload\ClassLoader;
    use Grav\Common\Plugin;
    use Grav\Framework\File\File;
    use Symfony\Component\Yaml\Yaml;
    
    /**
     * Class SimpleGuestbookPlugin
     * @package Grav\Plugin
     */
    class SimpleGuestbookPlugin extends Plugin
    {
      /**
       * @return array
       *
       * The getSubscribedEvents() gives the core a list of events
       *     that the plugin wants to listen to. The key of each
       *     array section is the event that the plugin listens to
       *     and the value (in the form of an array) contains the
       *     callable (or function) as well as the priority. The
       *     higher the number the higher the priority.
       */
      public static function getSubscribedEvents(): array
      {
        return [
          'onPluginsInitialized' => [
            // Uncomment following line when plugin requires Grav < 1.7
            // ['autoload', 100000],
            ['onPluginsInitialized', 0]
          ]
        ];
      }
    
      /**
       * Composer autoload
       *
       * @return ClassLoader
       */
      public function autoload(): ClassLoader
      {
        return require __DIR__ . '/vendor/autoload.php';
      }
    
      /**
       * Initialize the plugin
       */
      public function onPluginsInitialized(): void
      {
        // Don't proceed if we are in the admin plugin
        if ($this->isAdmin()) {
          return;
        }
    
        // Enable the main events we are interested in
        $this->enable([
          // Put your main events here
          'onAssetsInitialized' => ['onAssetsInitialized', 0],
          'onTwigTemplatePaths' => ['onTwigTemplatePaths', 0],
          'onTwigSiteVariables' => ['onTwigSiteVariables', 0],
        ]);
      }
    
      public function isOnRoute(): bool
      {
        $uri = $this->grav['uri']->uri();
        $routes = $this->config->get("plugins.$this->name.routes", '');
    
        $enable = $routes && (
          (gettype($routes) === 'string' && $routes === $uri) ||
          (is_array($routes) && in_array($uri, $routes)));
    
        return $enable;
      }
    
      public function onTwigTemplatePaths(): void
      {
        $this->grav['twig']->twig_paths[] = "plugins://$this->name/templates";
      }
    
      public function onTwigSiteVariables(): void
      {
        $twig = $this->grav['twig'];
        $twig->twig_vars['simpleGuestbook'] = $this->readGuestbookEntries();
      }
    
      public function readGuestbookEntries(): array
      {
        /** @var File */
        $fileInstance = new File('user-data://simple-guestbook/guestbook.yaml');
    
        if (!$fileInstance->exists()) {
          $fileInstance->save('');
        }
    
        $content = $fileInstance->load();
        $comments = Yaml::parse($content) ?? [];
    
        $approved = array_filter($comments, function ($comment) {
          return $comment['approved'];
        });
    
        return $approved;
      }
    
      public function onAssetsInitialized()
      {
        if ($this->config->get('plugins.simple-guestbook.addCss', true)) {
          $this->grav['assets']->addCss("plugin://simple-guestbook/css/style.css");
        }
      }
    }
    
  • Create new template /user/plugins/simple-guestbook/templates/modular/guestbook-form.html.twig to display the Guestbook.

    Content of guestbook-form.html.twig
    <section id="simple-guestbook">
      <div class="container">
    
        <div class="row">
          <div class="col-lg-12 text-center">
            {{ content|raw }}
          </div>
        </div>
    
        {% if simpleGuestbook|count > 0 %}
          <div class="row">
            <div class="col-lg-12 text-center">
              {% include "partials/simple-guestbook-list.html.twig" %}
            </div>
          </div>
        {% endif %}
    
        <div class="row">
    
          {% set form = forms('simple-guestbook') %}
    
          {% if form is null %}
            {% set form = grav.session.getFlashObject('form') %}
          {% endif  %}
    
          {% include 'partials/form-messages.html.twig' %}
    
          {% set scope = scope ?: 'data.' %}
          {% set multipart = '' %}
          {% set method = form.method|upper|default('POST') %}
    
          {% for field in form.fields %}
            {% if (method == 'POST' and field.type == 'file') %}
              {% set multipart = ' enctype="multipart/form-data"' %}
            {% endif %}
          {% endfor %}
    
          {% set action = form.action ? base_url ~ form.action : base_url ~ page.route ~ uri.params %}
    
          {% if (action == base_url_relative) %}
            {% set action = base_url_relative ~ '/' ~ page.slug %}
          {% endif %}
    
          <form name="{{ form.name }}" action="{{ action }}" method="{{ method }}" {{ multipart }} {% if form.id %} id="{{ form.id }}" {% endif %} {% block form_classes %} {% if form.classes %} class="{{ form.classes }}" {% endif %} {% endblock %}> {% block inner_markup_fields_start %}{% endblock %}
    
            <div class="col-md-6">
              {% for field in form.fields %}
                {% if field.position == 'left' %}
                  {% set value = form.value(field.name) %}
                  <div class="form-group">
                    {% include "forms/fields/#{field.type}/#{field.type}.html.twig" ignore missing %}
                  </div>
                {% endif %}
              {% endfor %}
            </div>
            <div class="col-md-6">
              {% for field in form.fields %}
                {% if field.position == 'right' %}
                  {% set value = form.value(field.name) %}
                  <div class="form-group">
                    {% include "forms/fields/#{field.type}/#{field.type}.html.twig" ignore missing %}
                  </div>
                {% endif %}
              {% endfor %}
            </div>
    
            {% include "forms/fields/formname/formname.html.twig" %}
    
            {% block inner_markup_fields_end %}{% endblock %}
    
            {% block inner_markup_buttons_start %}
              <div class="buttons">
              {% endblock %}
    
              <div class="col-lg-12 text-center">
                <div class="form-group">
                  {% for button in form.buttons %}
                    {% if button.outerclasses is defined %}
                      <div class="{{ button.outerclasses }}">
                      {% endif %}
                      {% if button.url %}
                        <a href="{{ button.url starts with 'http' ? button.url : url(button.url) }}">
                        {% endif %}
                        <button {% if button.id %} id="{{ button.id }}" {% endif %} {% block button_classes %} class="{{ button.classes|default('button') }}" {% endblock %} {% if button.disabled %} disabled="disabled" {% endif %} type="{{ button.type|default('submit') }}" {% if button.task %} name="task" value="{{ button.task }}" {% endif %}>
                          {{ button.value|t|default('Submit') }}
                        </button>
                        {% if button.url %}
                        </a>
                      {% endif %}
                      {% if button.outerclasses is defined %}
                      </div>
                    {% endif %}
                  {% endfor %}
                </div>
              </div>
    
              {% block inner_markup_buttons_end %}
              </div>
            {% endblock %}
    
            {{ nonce_field('form', 'form-nonce')|raw }}
          </form>
        </div>
      </div>
    </section>
    
  • Create new template /user/plugins/simple-guestbook/templates/forms/data.yaml.twig to save the form in Yaml format.

    Content of data.yaml.twig
    {%- macro render_field(form, fields, scope) %}
      {%- import _self as self %}
      {{- "-\n" }}
      {%- for index, field in fields %}
        {%- set show_field = attribute(field, "input@") ?? field.store ?? true %}
        {%- if field.fields %}
          {%- set new_scope = field.nest_id ? scope ~ field.name ~ '.' : scope -%}
          {{- "  " }}{{- self.render_field(form, field.fields, new_scope) }}
        {%- else %}
          {%- if show_field %}
            {%- set value = form.value(scope ~ (field.name ?? index)) -%}
            {%- if value -%}
              {{- "  " ~ (field.name|t|e) ~ ": " }}
              {%- if field.type == "textarea" %}
                {{- (string(value is iterable ? value|json_encode : value)|e|replace({'\n': '<br>'})|raw) ~ "\n" }}
              {%- else %}
                {{- string(value is iterable ? value|json_encode : value) ~ "\n" }}
              {%- endif %}
            {%- endif -%}
          {%- endif %}
        {%- endif %}
      {%- endfor %}
      {{- "  approved: false" }}
    {%- endmacro %}
    
    {%- import _self as macro %}
    {%- autoescape false %}
      {{- macro.render_field(form, form.fields, '') ~ "\n" }}
    {%- endautoescape %}
    
  • Create new template user/plugins/simple-guestbook/templates/partials/simple-guestbook-list.html.twig to list the Guestbook entries from /user/data/simple-guestbook/guestbook.yaml:

    Content of simple-guestbook-list.html.twig
    <p class="count">{{ simpleGuestbook|count }} guest(s) has left a note</p>
    
    <ul>
      {% for item in simpleGuestbook %}
        <li>
          <div class="name">{{ item.name }}</div>
          <div class="email">{{ item.email }}</div>
          <div class="message">{{ item.message| raw }}</div>
        </li>
      {% endfor %}
    </ul>
    
  • Add css file user/plugins/simple-guestbook/css/style.css, to create same format as Contact form. Yes, I know, we should use scss…

    Content of style.css
    #simple-guestbook h3,
    #simple-guestbook label {
      color: #999;
    }
    
    section#simple-guestbook {
      background-color: #222;
      background-image: url(../img/map-image.png);
      background-position: center;
      background-repeat: no-repeat;
    }
    
    section#simple-guestbook h2 {
      color: #fff;
    }
    
    section#simple-guestbook .form-group {
      margin-bottom: 25px;
    }
    
    section#simple-guestbook .form-group input,
    section#simple-guestbook .form-group textarea {
      padding: 20px;
    }
    
    section#simple-guestbook .form-group input.form-control {
      height: auto;
    }
    
    section#simple-guestbook .form-group textarea.form-control {
      height: 236px;
    }
    
    section#simple-guestbook .form-control:focus {
      border-color: #fed136;
      box-shadow: none;
    }
    
    section#simple-guestbook::-webkit-input-placeholder {
      text-transform: uppercase;
      font-family: Montserrat, 'Helvetica Neue', Helvetica, Arial, sans-serif;
      font-weight: 700;
      color: #bbb;
    }
    
    section#simple-guestbook:-moz-placeholder {
      text-transform: uppercase;
      font-family: Montserrat, 'Helvetica Neue', Helvetica, Arial, sans-serif;
      font-weight: 700;
      color: #bbb;
    }
    
    section#simple-guestbook::-moz-placeholder {
      text-transform: uppercase;
      font-family: Montserrat, 'Helvetica Neue', Helvetica, Arial, sans-serif;
      font-weight: 700;
      color: #bbb;
    }
    
    section#simple-guestbook:-ms-input-placeholder {
      text-transform: uppercase;
      font-family: Montserrat, 'Helvetica Neue', Helvetica, Arial, sans-serif;
      font-weight: 700;
      color: #bbb;
    }
    
    section#simple-guestbook .text-danger {
      color: #e74c3c;
    }
    
    section#simple-guestbook .count,
    section#simple-guestbook .name,
    section#simple-guestbook .email,
    section#simple-guestbook .message {
      color: #fff;
    }
    

Page:

  • Create a new guestbook page: /user/pages/01.home/_guestbook/guestbook-form.md, with the following content:

    Content of guestbook-form.md
    ---
    title: Guestbook
    cache_enable: false
    
    form:
      name: simple-guestbook
      action: /#guestbook
      fields:
        - name: name
          label: Name
          classes: form-control
          placeholder: Enter your name
          autofocus: off
          autocomplete: on
          type: text
          position: left
          validate:
              required: true
    
        - name: email
          label: Email
          classes: form-control
          placeholder: Enter your email address
          type: email
          position: left
          validate:
              required: true
    
        - name: message
          label: Message
          placeholder: Enter your feedback
          type: textarea
          classes: form-control
          position: right
          validate:
              required: true
    
      buttons:
        - type: submit
          classes: 'btn btn-primary btn-lg'
          value: Submit
    
      process:
        - save:
              filename: guestbook.yaml
              body: '{% include "forms/data.yaml.twig" %}'
              operation: add
        - message: Thank you! Your feedback will be moderated shortly.
        - reset: true
    ---
    
    ## Feedback
    
    ### Lorem ipsum dolor sit amet consectetur.
    
  • Add menu-item for new page in user/config/site.yaml:

    links:
      - ...
      - title: Guestbook
        url: '#simple-guestbook'
    

Todo:

  • Alter template user/plugins/simple-guestbook/templates/partials/simple-guestbook-list.html.twig to your own liking.
  • Add css to style the list of Guestbook entries.
  • And so much more can be done to polish it…
  • Fix errors/bugs I’ve created… :man_facepalming:
1 Like

hoawoo ! gratitude ! :heart_eyes: :heart_decoration:

your idea of pagination is great! I would love to have this !

But I’m getting blocked by the really second point :

I don’t own the server and only have a SFTP access.

@citoyencandide, You might really want to consider to setup a local development environment…

never done this, don’t know how to do it neither where to find information on how-to … is it difficult ?

feel like i’m entering the code zone and will become a coder someday …

@citoyencandide, Depends on the OS you are using… But whichever OS, Google search is your friend!

I’m trying not to use g00gle services … because it is definitively not my friend, I would say it’s no one friends,!

anyway, there are other search engines …

And the OS is ubuntu here …

I’ve found this || alternate link but there are plenty of programming language and I guess they are not all useful for this project…

Let’s have a look to the the grav github repository to find out what could be the useful language … PHP at 99.7, and 0.3% for other, ok PHP only !

OK, new search, now found this

It say’s :

This involves installing and configuring the PHP engine, a MySQL database, an Apache web server, and the XDebug debugger.

As mentioned above, I don’t own the server : someone gave me access to only a part of his server, so I won’t be root on it.

@pamtbaau I’m I wrong thinking the solution you offered me is not available for me ?

This is an environment running PHP and Apache on a local machine i.e. laptop/desktop (Apple, Windows or Linux) and not a remote server over which one has no control.

Does Grav fit your needs?
Please note, Grav has been created with developers in mind. Grav is indeed a breeze to adapt and extend when one has experience with programming languages like PHP, Twig and, to a lesser extent, Javascript.

A non-developer, might use Grav successfully if one can except what Grav themes offer out-of-the-box, maybe extended with plugins which offer extra functionality. But as soon as that does not fit ones needs, experience with PHP, Twig, HTML, CSS/SCSS are essential.

If the skills are not available, maybe other environments like Wix (“Wix is user-friendly and makes it possible to build a professional website without knowing how to code.”) , wordpress.com, or others may be a better choice.

Is it a subtle way of saying : you don’t belong here ?

Anyway, right now, i’m setting up Netbeans, an Integrated Development Environment (IDE) …

and it’s too heavy for my old laptop…

back to commande line …
upgrade of the disto … been messing it out
now all thing corrected

stack overflow is my friend

Edit :

can’t get the database working, either mySQL or Mariadb …
been asking for help on ubuntu forum.

I cannot change my previous answer, so I write a new one even if this is not the best practice with Discourse…

My laptop is up and running with a working IDE !

The learning curve is steep but continuous

It’s done ! looking for my next step, now !