Left sidebar in vue base plugin

Hi,

I’ve created a vue based plugin showing a panel page with a court-schedule.
The custom panel page works fine.
But I cannot manage to get the default Kirby left sidebar into my page.
Probably something I miss in my approach.

Any tips someone?

Thanks in advance.

Below my screenprint and plugins index.php

<?php
// site/plugins/plg-courtschedule/index.php

use Kirby\Cms\App;

Kirby::plugin('padeltime/courtschedule', [
    'areas' => [
        'courtschedule2' => function ($kirby) {
            return [
                'label' => 'CourtSchedule',
                'icon' => 'calendar',
                'menu' => true,
                'link' => 'courtschedule2',
                'searches' => [
                    'pages' => [
                        'label' => 'Pages',
                        'icon' => 'page',
                        'query' => function (string|null $query, int $limit, int $page) {
                            $kirby = App::instance();
                            return $kirby->site()->index()->search($query)->limit($limit);
                        }
                    ]
                ],
                'views' => [
                    [
                        'pattern' => 'courtschedule2',
                        'action' => function () {
                            return [
                                'component' => 'k-courtschedule2-view',
                                'title' => 'Court Schedule 2',
                                'props' => [
                                    'layout' => 'inside',
                                    'message' => 'Welcome to CourtSchedule2!'
                                ]
                            ];
                        }
                    ]
                ]
            ];
        }
    ],
    'api' => [
        'routes' => [
            [
                'pattern' => 'courtschedule2/clubs',
                'method' => 'GET',
                'action' => function () {
                    $courtschedulePage = site()->find('courtschedule');

                    if (!$courtschedulePage) {
                        return [
                            'success' => false,
                            'error' => 'Court schedule page not found'
                        ];
                    }

                    // Get filtered clubs using the model method
                    $clubs = $courtschedulePage->filteredClubs();

                    // Format clubs data for JSON response
                    $clubsData = [];
                    foreach ($clubs as $club) {
                        $clubsData[] = [
                            'id' => $club->id(), // Full path like 'clubs/bakkershaag'
                            'uid' => $club->uid(), // Just 'bakkershaag'
                            'title' => $club->title()->value(),
                            'openingTime' => $club->opening_time()->or('08:00')->value(),
                            'closingTime' => $club->closing_time()->or('22:00')->value()
                        ];
                    }

                    return [
                        'success' => true,
                        'clubs' => $clubsData
                    ];
                }
            ],
            [
                'pattern' => 'courtschedule2/schedule/(:any)/(:any)',
                'method' => 'GET',
                'action' => function (string $clubUid, string $date) {
                    try {
                        $courtschedulePage = site()->find('courtschedule');

                        if (!$courtschedulePage) {
                            return [
                                'success' => false,
                                'error' => 'Court schedule page not found'
                            ];
                        }

                        // Construct full club path from UID
                        $club = page('clubs/' . $clubUid);
                        if (!$club) {
                            return [
                                'success' => false,
                                'error' => 'Club not found: ' . $clubUid
                            ];
                        }

                        // Get schedule data (works for both weekly and daily views)
                        $weekStart = date('Y-m-d', strtotime('monday this week', strtotime($date)));
                        $lessons = $courtschedulePage->getWeekSchedule($club, $weekStart);
                        $courts = $club->childrenAndDrafts()->filterBy('template', 'court');

                        // Format lessons data
                        $lessonsData = [];
                        foreach ($lessons as $lesson) {
                            $lessonsData[] = [
                                'id' => $lesson['id'],
                                'lessonGroupId' => $lesson['lessonGroupId'],
                                'title' => $lesson['title'],
                                'courtId' => $lesson['court'] ? $lesson['court']->id() : null,
                                'courtTitle' => $lesson['court'] ? $lesson['court']->title()->value() : null,
                                'date' => $lesson['date'],
                                'day' => $lesson['day'],
                                'startTime' => $lesson['startTime'],
                                'duration' => $lesson['duration'],
                                'trainer' => $lesson['trainer'],
                                'participants' => $lesson['participants'],
                                'maxParticipants' => $lesson['maxParticipants'],
                                'lessonNumber' => $lesson['lessonNumber'],
                                'totalLessons' => $lesson['totalLessons']
                            ];
                        }

                        // Format courts data
                        $courtsData = [];
                        foreach ($courts as $court) {
                            $courtsData[] = [
                                'id' => $court->id(),
                                'title' => $court->title()->value(),
                                'name' => $court->court_name()->value()
                            ];
                        }

                        return [
                            'success' => true,
                            'lessons' => $lessonsData,
                            'courts' => $courtsData,
                            'clubId' => $club->id(),
                            'clubTitle' => $club->title()->value(),
                            'openingTime' => $club->opening_time()->or('08:00')->value(),
                            'closingTime' => $club->closing_time()->or('22:00')->value()
                        ];
                    } catch (Exception $e) {
                        return [
                            'success' => false,
                            'error' => 'Error loading schedule: ' . $e->getMessage(),
                            'trace' => $e->getTraceAsString()
                        ];
                    }
                }
            ],
            [
                'pattern' => 'courtschedule2/lesson/create',
                'method' => 'POST',
                'action' => function () {
                    try {
                        $data = kirby()->request()->data();
                        $courtId = $data['courtId'] ?? null;
                        $lessonGroupId = $data['lessonGroupId'] ?? null;
                        $date = $data['date'] ?? null;
                        $startTime = $data['startTime'] ?? null;

                        if (!$lessonGroupId || !$date || !$startTime) {
                            return [
                                'success' => false,
                                'error' => 'Missing required fields: lessonGroupId, date, or startTime'
                            ];
                        }

                        // Get the lesson group page
                        $lessonGroup = page($lessonGroupId);
                        if (!$lessonGroup) {
                            return [
                                'success' => false,
                                'error' => 'Lesson group not found'
                            ];
                        }

                        // Create a unique slug for the lesson
                        $lessonSlug = 'lesson-' . $date;

                        // Check if lesson already exists
                        $counter = 1;
                        $originalSlug = $lessonSlug;
                        while ($lessonGroup->children()->find($lessonSlug)) {
                            $lessonSlug = $originalSlug . '-' . $counter;
                            $counter++;
                        }

                        // Create the lesson page
                        $lesson = $lessonGroup->createChild([
                            'slug' => $lessonSlug,
                            'template' => 'lesson',
                            'content' => [
                                'date' => $date,
                                'start_time' => $startTime,
                                'court' => $courtId ? 'page://' . $courtId : '',
                                'duration' => '60'
                            ]
                        ]);

                        return [
                            'success' => true,
                            'lessonId' => $lesson->id()
                        ];
                    } catch (Exception $e) {
                        return [
                            'success' => false,
                            'error' => 'Error creating lesson: ' . $e->getMessage(),
                            'trace' => $e->getTraceAsString()
                        ];
                    }
                }
            ]
        ]
    ]
]);

What does the Vue component look like?

You should have a basic structure like this:

<template>
  <k-panel-inside>
    <k-header>
      Some header
    </k-header>
      <div>Some stuff here</div>
  </k-panel-inside>
</template>

Wow!!
Thanks sonja, I asked “Claude” to apply your approach and… boom!
The only thing is that I get an empty header now (see image), any idea how to remove this?
Of course css is possible but I got the feeling it must be possible without css too…

Anyhow super thanks for the fast response!

To be complete here my vue file and plugins index.php
! btw. this is not the courtschedule. plugin from the first post, but another simpler dashboard plugin.

<template>
  <k-panel-inside class="dashboard-view" :data-theme="theme">
    <k-header></k-header>

    <div class="dash-content">
      <div class="dash-cards">
        <!-- Clubs Card -->
        <div class="dash-card" @click="navigateTo('clubs')">
          <div class="dash-card-icon">
            <k-icon type="archive" />
          </div>
          <div class="dash-card-content">
            <h2>Clubs</h2>
          </div>
          <div class="dash-card-arrow">
            <k-icon type="angle-right" />
          </div>
        </div>

        <!-- Courts Card -->
        <div class="dash-card" @click="navigateTo('courts')">
          <div class="dash-card-icon">
            <k-icon type="grid" />
          </div>
          <div class="dash-card-content">
            <h2>Courts</h2>
          </div>
          <div class="dash-card-arrow">
            <k-icon type="angle-right" />
          </div>
        </div>

        <!-- Trainers Card -->
        <div class="dash-card" @click="navigateTo('trainers')">
          <div class="dash-card-icon">
            <k-icon type="users" />
          </div>
          <div class="dash-card-content">
            <h2>Trainers</h2>
          </div>
          <div class="dash-card-arrow">
            <k-icon type="angle-right" />
          </div>
        </div>

        <!-- Court Schedule Card -->
        <div class="dash-card" @click="navigateTo('courtschedule')">
          <div class="dash-card-icon">
            <k-icon type="calendar" />
          </div>
          <div class="dash-card-content">
            <h2>Court Schedule</h2>
          </div>
          <div class="dash-card-arrow">
            <k-icon type="angle-right" />
          </div>
        </div>
      </div>
    </div>
  </k-panel-inside>
</template>

<script>
export default {
  name: 'DashboardView',
  data() {
    return {
      currentTheme: 'dark'
    }
  },
  computed: {
    theme() {
      return this.currentTheme;
    }
  },
  watch: {
    '$panel.theme': {
      handler(newTheme) {
        // Extract theme value
        if (typeof newTheme === 'string') {
          this.currentTheme = newTheme;
        } else if (newTheme && typeof newTheme === 'object') {
          const themeValue = newTheme.value ||
                            newTheme.current ||
                            newTheme.name ||
                            newTheme.id ||
                            (newTheme.toString && newTheme.toString());
          this.currentTheme = themeValue || 'dark';
        }
      },
      deep: true,
      immediate: true
    }
  },
  methods: {
    navigateTo(destination) {
      switch (destination) {
        case 'clubs':
          if (this.$panel && this.$panel.open) {
            this.$panel.open('/pages/clubs')
          } else {
            window.location.href = '/panel/pages/clubs'
          }
          break
        case 'courts':
          // Navigate to clubs first, then user can select club to see courts
          if (this.$panel && this.$panel.open) {
            this.$panel.open('/pages/clubs')
          } else {
            window.location.href = '/panel/pages/clubs'
          }
          break
        case 'trainers':
          if (this.$panel && this.$panel.open) {
            this.$panel.open('/pages/trainers')
          } else {
            window.location.href = '/panel/pages/trainers'
          }
          break
        case 'courtschedule':
          if (this.$panel && this.$panel.open) {
            this.$panel.open('/courtschedule2')
          } else {
            window.location.href = '/panel/courtschedule2'
          }
          break
      }
    }
  }
}
</script>

<style scoped>
.dashboard-view {
  min-height: 100vh;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}

/* Dark theme */
.dashboard-view[data-theme="dark"] {
  background: #1a1a1a;
  color: #e0e0e0;
}

.dashboard-view[data-theme="dark"] .dash-header {
  background: #252525;
  border-bottom: 1px solid #333;
}

.dashboard-view[data-theme="dark"] .dash-header h1 {
  color: #ffffff;
}

.dashboard-view[data-theme="dark"] .dash-card {
  background: #252525;
  border: 1px solid #333;
}

.dashboard-view[data-theme="dark"] .dash-card:hover {
  background: #2a2a2a;
  border-color: #444;
}

.dashboard-view[data-theme="dark"] .dash-card h2 {
  color: #ffffff;
}

.dashboard-view[data-theme="dark"] .dash-card p {
  color: #b0b0b0;
}

/* Light theme */
.dashboard-view[data-theme="light"] {
  background: #f7f7f7;
  color: #333;
}

.dashboard-view[data-theme="light"] .dash-header {
  background: #ffffff;
  border-bottom: 1px solid #ddd;
}

.dashboard-view[data-theme="light"] .dash-header h1 {
  color: #000000;
}

.dashboard-view[data-theme="light"] .dash-card {
  background: #ffffff;
  border: 1px solid #ddd;
}

.dashboard-view[data-theme="light"] .dash-card:hover {
  background: #f9f9f9;
  border-color: #ccc;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}

.dashboard-view[data-theme="light"] .dash-card h2 {
  color: #000000;
}

.dashboard-view[data-theme="light"] .dash-card p {
  color: #666;
}

/* Content area */
.dash-content {
  padding: 2rem;
  max-width: 1200px;
  margin: 0 auto;
}

/* Cards grid */
.dash-cards {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
  gap: 1.5rem;
}

/* Card styling */
.dash-card {
  border-radius: 0.5rem;
  padding: 1.5rem;
  display: flex;
  align-items: center;
  gap: 1rem;
  cursor: pointer;
  transition: all 0.2s ease;
}

.dash-card-icon {
  font-size: 3rem;
  opacity: 0.8;
}

.dash-card-content {
  flex: 1;
}

.dash-card-content h2 {
  font-size: 1.25rem;
  font-weight: 400;
  margin: 0;
}

.dash-card-arrow {
  opacity: 0.5;
  transition: opacity 0.2s ease;
}

.dash-card:hover .dash-card-arrow {
  opacity: 1;
}

/* Responsive adjustments */
@media (max-width: 768px) {
  .dash-cards {
    grid-template-columns: 1fr;
  }

  .dash-content {
    padding: 1rem;
  }
}
</style>

<?php
// site/plugins/pt-dashboard/index.php

use Kirby\Cms\App;

Kirby::plugin('padeltime/dashboard', [
    'areas' => [
        'pt-dashboard' => function ($kirby) {
            return [
                'label' => 'Dashboard',
                'icon' => 'dashboard',
                'menu' => fn() => true,
                'link' => 'pt-dashboard',
                'searches' => [
                    'pages' => [
                        'label' => 'Pages',
                        'icon' => 'page',
                        'query' => function (string|null $query, int $limit, int $page) {
                            $kirby = App::instance();
                            return $kirby->site()->index()->search($query)->limit($limit);
                        }
                    ]
                ],
                'views' => [
                    [
                        'pattern' => 'pt-dashboard',
                        'action' => function () {
                            return [
                                'component' => 'k-dashboard-view',
                                'title' => 'Dashboard',
                                'props' => [
                                    'layout' => 'inside'
                                ]
                            ];
                        }
                    ]
                ]
            ];
        }
    ]
]);

Regards

Sonja,

Removed the <k-header> completely from vue.js Now it looks how it has to look!

Again thanks a lot!

Case closed