Building user profile pages in Kirby 3

Hi !
I’m trying to build automatic user pages with the new Kirby.
From what I understand, they don’t come out of the box.
I tried to add a route like so:

return [
  'routes' => [
    [
      'pattern' => 'profile/(:any)',
      'action'  => function($user) {
        $site = kirby()->site(); 	
        tpl::load(kirby()->roots()->templates() . DS . 'authors.php', array('user' => $user, 'site' => $site), false);
      }
    ]
  ]
];

And I made a authors.php template that goes with it.
But all I get is a 404 error…
Any help is appreciated.
Also, I think it would make a nice addition to the cookbook.
Because I can’t believe I’m the only one trying to do that :wink:
Thank you very much!

Think you have to return the template:

return [
  'routes' => [
    [
      'pattern' => 'profile/(:any)',
      'action'  => function($user) {
        $site = kirby()->site(); 	
        return tpl::load(kirby()->roots()->templates() . DS . 'authors.php', array('user' => $user, 'site' => $site), false);
      }
    ]
  ]
];

Thanks.
I tried but still no luck.
I’m still getting an error page :frowning:

Is that a single or multi-language Kirby installation?

No it’s single language (I started from the v3 starterkit).
But I might have to switch to multi at some point…

Hm, the route works for me…

What is in your authors.php?

Just that:
<?php snippet('header') ?>
<?= $user ?>
<?php snippet('footer') ?>

Returning a template from a route was never such a great deal, actually, with having to return all the data down to the snippets (your current setup would throw errors, even if it didn’t return the 404 page). For example, if your header contains the typical

<?= $page->title() ?>

you will get an error, because the Page object doesn’t exist.

I’d create a parent /profile page and then create the children as virtual pages of that parent.

It’s weird because I remember doing something similar in Kirby2…

So what you suggest is making a ‘real’ /profile page with a dedicated blueprint and all? And then launching a virtual page like shown here (https://getkirby.com/docs/guide/routing#virtual-pages)? How would you go about passing the user id?

Once the /profile page exists, I’m thinking I could just pass the user id through a param and then use it to find the user…

Yes, that would also be an option… Instead of using the parameter, you can use the the route as before, but instead of trying to load a template, return the parent page with the user as additional data for the template.

1 Like

OK I got it to work in the end by making a ‘real’ /profile page (with blueprint, but not listed in the panel) and passing the user id as a parameter. It’s not very ‘clean’ but it does the job… (if someone decides to erase the profile page through the panel, I’m toast…)

I’d be happy to find out if there’s a better way to do it at some point. Even though routing and stuff is a bit beyond my skillset…

Thanks a lot for bearing with me and trying to find a solution…

1 Like

@texnixe there is now an easier way to display the user pages ?

You can create them as virtual children of a parent as I suggested in one of the replies above. I think that’s a better option than working with routes.

See the docs: https://getkirby.com/docs/guide/virtual-pages

Only your virtual content wouldn’t be a database or csv files, but the users.

I tried that:

config.php

  'routes' => [
          [
            'pattern' => 'mitglied/(:any)',
            'action'  => function($user) {
              $site = kirby()->site(); 	
              return tpl::load(kirby()->roots()->templates() . DS . 'member.php', array('user' => $user, 'site' => $site), false);
            }
          ]
          ],

template collection of users

<?php foreach($kirby->users()->sortBy('name','asc') as $user):
		 
			if($user->member_type() == "member"): ?>
				<article class="grid-col-30">
				<a href="mitglied/"<? echo $user->id() ?> >
					<h2><?php echo $user->member_type() ?></h2>
					<h2><?php echo $user->name() ?></h2>
				</a>
				</article>
			<?php endif ?>

		<?php endforeach ?>

and added the dir content/member

what do I have to use the correct url to display a user page?
<a href=“mitglied/”<? echo $user->id() ?> >

maybe it’s simpler (with my knowledge) to switch between user collection and singel user with JS

I suggested to use a model instead of this template stuff…

Create a members page in /content with a members.txtand then this model members.php

<?php

class MembersPage extends Page
{
  public function children()
  {
    $usersPages = [];
    $users      = kirby()->users();
    foreach ($users as $key => $user) {
      $userPages[] = [
        'slug'     => Str::slug($user->id()), // or username if unique
        'num'      => $user->indexOf($users),
        'template' => 'member',
        'model'    => 'member',
        'content'  => [
          'title'    => $user->username(),
           // more fields here
        ]
      ];
    }
    return Pages::factory($userPages, $this);

  }
}

Then you can list them in your member.php template and the children work like a normal page with their own url.

4 Likes

Thank you very much, works wonderfully.

Hello,

I used the same principle and it works well for simple fields.

I can’t display a structure correctly.

I have an error:
Invalid structure data for "user" field on parent

How did you add the structure to your content array? Please post your code!

Hello,

I think I don’t know how to add the structure to the models members.php

My structure field is: social_events_listing (and to make it more complex, I have a substructure).

<?php

class MembersPage extends Page
{
  public function children()
  {
    $usersPages = [];
    $users      = kirby()->users();
    foreach ($users as $key => $user) {
      $userPages[] = [
        'slug'     => Str::slug($user->firstname() ." ". $user->namefamily()), // or username if unique
        'num'      => $user->indexOf($users),
        'template' => 'member',
        'model'    => 'member',
        'content'  => [
          'user'    => $user,
          'name'    => $user->name(),
          'title'    => $user->firstname() ." ". $user->namefamily(),
          'firstname'    => $user->firstname(),
          'namefamily'    => $user->namefamily(),
          'member'    => $user->member(),
          'sex'    => $user->sex(),
          'birthday'    => $user->birthday(),
          'street'    => $user->street(),
          'zip'    => $user->zip(),
          'city'    => $user->city(),
          'country'    => $user->country(),
          'phone'    => $user->phone(),
          'phonemobile'    => $user->phonemobile(),
          'licence_date'    => $user->licence_date(),
          'size'    => $user->size(),
          'weight'    => $user->weight(),
          'bio'    => $user->bio(),
          'url_strava'    => $user->url_strava(),
          'photo'    => $user->file(),
          'social_events_listing'    => $user->social_events_listing(),
          //'social_events_date'    => $user->social_events_date(),
           // more fields here
        ]
      ];
    }
    return Pages::factory($userPages, $this);

  }
}

At the beginning I displayed all the data on a page which lists all the members with photo.

We click on the photo to display a modal with all the information.

But I preferred to create a page for each user.

Here is the part with the structure and substructure that works.

I thought it was enough to copy the same model and change something at first, but I don’t know how.

              <?php if ($user->social_events_listing()->length() > 0): ?>
              <h4 id="scrollspyHeading2-membre-<?= mb_strtolower(str_replace(' ', '', $user->name())) ?>">Participation
              </h4>

              <div class="accordion accordion-flush mb-5" id="accordionFlushExample">
                <?php 
                    $items = $user->social_events_listing()->toStructure()->flip();
                    foreach ($items as $item):   
                ?>
                <div class="accordion-item ps-0">
                  <h2 class="accordion-header" id="flush-headingOne">
                    <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse"
                      data-bs-target="#flush-collapse-<?= $item->id(); ?>" aria-expanded="false"
                      aria-controls="flush-collapseOne">
                      <b>Année <?php echo $item->social_events_date() ?></b>
                    </button>
                  </h2>
                  <div id="flush-collapse-<?= $item->id(); ?>" class="accordion-collapse collapse"
                    aria-labelledby="flush-headingOne" data-bs-parent="#accordionFlushExample">
                    <div class="accordion-body">

                      <div class="alert alert-light" role="alert">
                        <?= $item->social_events_description()->kirbytext() ?>
                      </div>

                      <div class="card">
                        <ul class="list-group list-group-flush">
                          <?php
                            //Sous-liste
                            $listings = $item->social_events_listing2()->toStructure();
                            foreach($listings as $listing): 
                          ?>

                          <li class="list-group-item">
                            <div class="row">
                              <div class="col-6">
                                <h3><?= $listing->social_events_listing2_title(); ?></h3>
                              </div>
                              <div class="col-6 ms-auto text-end">
                                <div class="d-none d-lg-inline-flex tag">
                                  <span class="badge bg-light text-primary mx-1"><i class="far fa-calendar-alt"></i>
                                    <?= $listing->social_events_listing2_date()->toDate('%A %e %B %Y'); ?></span>
                                  <span class="badge bg-light text-primary mx-1"><i class="fas fa-map-marker-alt"></i>
                                    <?= $listing->social_events_listing2_city(); ?></span>
                                </div>
                              </div>
                              <div class="col-12 fst-italic">
                                <?= $listing->social_events_listing2_text()->kirbytext(); ?>
                              </div>
                              <div class="col-12">
                                <!-- Actualités ou Séjours en relation -->
                                <?php foreach($listing->social_events_listing2_related()->toPages() as $storierelated): ?>

                                <a class="btn btn-secondary text-start" href="<?= $storierelated->url() ?>">Lire
                                  <?= lcfirst($storierelated->title()) ?> <i class="fa-solid fa-chevron-right"></i></a>

                                <?php endforeach ?>
                                <!-- ./Actualités ou Séjours en relation -->
                              </div>
                            </div>
                          </li>
                          <?php endforeach ?>
                        </ul>
                      </div>

                    </div>
                  </div>
                </div>
                <?php endforeach ?>
              </div>
              <?php endif ?>

If it can help. This is what the file looks like: blueprints/users/client.yml

title: Membre
# Set page where you want to redirect the user to after login
#home: /

permissions:
  access:
    panel: false

tabs:
  profil:
    label: Mon profil
    icon: text
    columns:
      - width: 1/3
        sections:
          downloads:
            headline:
              label: Photo
            type: files
            layout: cards
            info: "{{ file.caption }}"
            image:
              ratio: 3/4
            max: 1 #maximum 1
            
      - width: 2/3
        # Kirby has many different field types, from simple text fields to the more complex structure field that contains subfields
        # All available field types: https://getkirby.com/docs/reference/panel/fields
        fields:
          firstname:
            label: Prénom
            type: text
            width: 1/2
          namefamily:
            label: Nom
            type: text
            width: 1/2
          member:
            label: Membre du bureau (facultatif)
            type: text
            placeholder: Président, Secrétaire, Trésorier ...
            width: 1/1
          sex:
            label: Sexe
            type: select
            default: sex_default
            options:
              sex_default: non défini
              sex_man: homme
              sex_woman: femme
            width: 1/2
          birthday:
            label: Date de naissance
            type: date
            display: DD/MM/YYYY
            width: 1/2

  contactdetails:
    label: Coordonnées
    icon: text
    sections:
      contactdetails:
        type: fields
        fields:
          street:
            label: Adresse
            type: text
          zip:
            label: Code postal
            type: tel
            icon: none
            width: 1/4
          city:
            label: Ville
            type: text
            width: 3/4
          country:
            label: Pays
            type: text
            default: France
            placeholder: France
          phone:
            label: Téléphone
            type: tel
            icon: phone
            maxlength: 10
            width: 1/4
          phonemobile:
            label: Téléphone portable
            type: tel
            icon: mobile
            maxlength: 10
            width: 1/4
            
  licence:
    label: Ma licence
    icon: text
    sections:
      licence:
        type: fields
        fields:
          licence_number:
            label: Numéro de licence
            type: tel
            icon: account
            width: 1/2
          licence_date:
            label: Date d'entrée au club
            type: date
            display: DD/MM/YYYY
            width: 1/2

  athlete:
    label: L'athlète
    icon: text
    sections:
      athlete:
        type: fields
        fields:
          size:
            label: Taille
            type: number
            after: cm
            width: 1/4
          weight:
            label: Poids
            type: number
            after: kg
            width: 1/4
          bio:
            label: Bio
            type: textarea
            width: 1/1

  social:
    label: Suivez-moi !
    icon: text
    sections:
      social:
        type: fields
        fields:
          url_strava:
            label: Lien Strava
            type: url
            width: 1/2
          # social_events:
          #   label: Suivez-moi sur un événement
          #   type: textarea
          #   width: 1/1
          social_events_listing:
            label: Historique
            type: structure
            fields:
              social_events_date:
                label: Date
                type: number
                max: 2050
                width: 1/2
              social_events_description:
                label: Description
                type: textarea
                width: 1/1
              social_events_listing2:
                label: Sous-texte(s)
                type: structure
                fields:
                  social_events_listing2_title:
                    label: Titre
                    type: text
                    width: 1/1
                  social_events_listing2_date:
                    label: Date
                    type: date
                    display: D MMMM YYYY
                    width: 1/2
                  social_events_listing2_city:
                    label: Ville
                    type: text
                    size: small
                    width: 1/2
                  social_events_listing2_text:
                    label: Texte
                    type: textarea
                    size: small
                    width: 1/1
                  social_events_listing2_related:
                    label: Lien(s)
                    type: pages
                    query: site.find('sejours', 'actualites')
                    multiple: true

You need to convert the structure to an array:

'social_events_listing'    => $user->social_events_listing()->toStructure()->toArray(),

should do the job.