Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -57,4 +57,6 @@ CLOUDINARY_CLOUD_NAME=my-cloud-name
CLOUDINARY_API_KEY=my-api-key
CLOUDINARY_API_SECRET=my-api-secret

GITHUB_TOKEN=

LARAVEL_DEFAULT_FATHOM_SITE_ID=WECFOADW
20 changes: 20 additions & 0 deletions app/Actions/PageAction.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace App\Actions;

use App\DTO\PageDTO;
use App\Models\GithubRepository;
use App\Models\News;
use App\Models\Page;
use App\Models\Product;
Expand Down Expand Up @@ -81,6 +82,25 @@ public function product(Product $product, bool $withReferences = false, ?string
);
}

public function githubRepository(GithubRepository $githubRepository, bool $withReferences = false, ?string $locale = null): PageDTO
{
return new PageDTO(
locale: $locale ?? $githubRepository->locale->value,
routeKey: 'open-source.show',
routeName: Str::slug(title: $locale ?? $githubRepository->locale->value).'.open-source.show',
title: $githubRepository->title,
description: $githubRepository->teaser,
image: $githubRepository->image,
lastModificationDate: $githubRepository->updated_at ?? now(),
routeParameters: ['locale' => $githubRepository->locale, 'githubRepository' => $githubRepository],
referencePages: $withReferences ? $githubRepository->references->map(function (Reference $reference) {
$reference->load(['target']);

return self::githubRepository(githubRepository: $reference->target, withReferences: false, locale: $reference->reference_locale);
}) : null,
);
}

public function service(Service $service, bool $withReferences = false, ?string $locale = null): PageDTO
{
return new PageDTO(
Expand Down
22 changes: 7 additions & 15 deletions app/Actions/ViewDataAction.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,9 @@

use App\DTO\ContactDTO;
use App\Enums\ContactSectionEnum;
use App\Models\Configuration;
use App\Models\Contact;
use App\Models\GithubRepository;
use App\Models\News;
use App\Models\OpenSource;
use App\Models\Product;
use App\Models\Service;
use App\Models\Technology;
Expand All @@ -17,15 +16,6 @@

class ViewDataAction
{
public function configuration(string $locale): ?Configuration
{
$key = Str::slug("configuration_{$locale}");

return Cache::rememberForever($key, function () {
return Configuration::first();
});
}

public function products(string $locale): Collection
{
$key = Str::slug("products_published_{$locale}");
Expand Down Expand Up @@ -62,12 +52,12 @@ public function technologies(string $locale): Collection
});
}

public function openSource(string $locale): Collection
public function githubRepositories(string $locale): Collection
{
$key = Str::slug("open_source_published_{$locale}");
$key = Str::slug("github_repositories_published_{$locale}");

return Cache::rememberForever($key, function () use ($locale) {
return OpenSource::where('locale', $locale)->where('published', true)->orderByDesc('downloads')->get();
return GithubRepository::where('locale', $locale)->where('published', true)->orderByDesc('downloads')->get();
});
}

Expand All @@ -82,7 +72,9 @@ public function contacts(string $locale): object
->get();

return (object) collect([
ContactSectionEnum::EMPLOYEES,
ContactSectionEnum::SOFTWARE_ENGINERING,
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The same typo "ENGINERING" appears here and should be "ENGINEERING" to match the corrected enum constant name.

Suggested change
ContactSectionEnum::SOFTWARE_ENGINERING,
ContactSectionEnum::SOFTWARE_ENGINEERING,

Copilot uses AI. Check for mistakes.
ContactSectionEnum::DIGITAL_TRANSFORMATION,
ContactSectionEnum::SCANNING,
ContactSectionEnum::COLLABORATIONS,
ContactSectionEnum::BOARD_MEMBERS,
])->mapWithKeys(function (string $section) use ($publishedContacts, $locale): array {
Expand Down
134 changes: 134 additions & 0 deletions app/Console/Commands/SyncRepositoriesCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
<?php

namespace App\Console\Commands;

use App\Enums\LocaleEnum;
use App\Models\GithubRepository;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Str;

class SyncRepositoriesCommand extends Command
{
protected $signature = 'sync:repositories';

protected $description = 'Sync public GitHub repositories from the codebar-ag organization';

private const ORG = 'codebar-ag';

private const DEFAULT_IMAGE = 'https://res.cloudinary.com/codebar/image/upload/c_scale,dpr_2.0,f_auto,q_auto,w_1200/www-codebar-ch/seo/seo_codebar.webp';

public function handle(): int
{
$this->info('Fetching public repositories from GitHub...');

$repos = $this->fetchAllRepositories();

if ($repos === null) {
$this->error('Failed to fetch repositories from GitHub.');

return self::FAILURE;
}

$this->info(sprintf('Found %d public repositories.', count($repos)));

$synced = 0;

foreach ($repos as $repo) {
if ($repo['fork'] ?? false) {
continue;
}

$this->syncRepository($repo);
$synced++;
}

$this->info(sprintf('Synced %d repositories.', $synced));

return self::SUCCESS;
}

private function fetchAllRepositories(): ?array
{
$repos = [];
$page = 1;

$headers = [];
$token = config('services.github.token');
if ($token) {
$headers['Authorization'] = "Bearer {$token}";
}

do {
$response = Http::withHeaders($headers)
->accept('application/vnd.github+json')
->get(sprintf('https://api.github.com/orgs/%s/repos', self::ORG), [
'type' => 'public',
'per_page' => 100,
'page' => $page,
]);

if ($response->failed()) {
$this->error(sprintf('GitHub API error: %s', $response->body()));

return null;
}

$batch = $response->json();

if (empty($batch)) {
break;
}

$repos = array_merge($repos, $batch);
$page++;
} while (count($batch) === 100);

return $repos;
}

private function syncRepository(array $repo): void
{
$slug = Str::slug($repo['name']);
$title = Str::of($repo['name'])->replace('-', ' ')->title()->toString();
$teaser = $repo['description'] ?? '';
$topics = $repo['topics'] ?? [];
$downloads = $this->fetchPackagistDownloads($repo['full_name']);

foreach (LocaleEnum::cases() as $locale) {
$entry = GithubRepository::updateOrCreate(
[
'locale' => $locale->value,
'slug' => $slug,
],
[
'published' => true,
'title' => $title,
'teaser' => $teaser,
'image' => self::DEFAULT_IMAGE,
'tags' => $topics,
'link' => $repo['html_url'],
'downloads' => $downloads,
'stars' => $repo['stargazers_count'] ?? 0,
'forks' => $repo['forks_count'] ?? 0,
'primary_language' => $repo['language'],
'github_name' => $repo['full_name'],
]
);

$this->line(sprintf(' %s [%s] %s downloads', $entry->title, $locale->value, number_format($downloads)));
}
}

private function fetchPackagistDownloads(string $fullName): int
{
$response = Http::accept('application/json')
->get(sprintf('https://packagist.org/packages/%s.json', $fullName));

if ($response->failed()) {
return 0;
}

return (int) data_get($response->json(), 'package.downloads.total', 0);
}
}
76 changes: 76 additions & 0 deletions app/Data/GkiServiceData.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<?php

namespace App\Data;

use Illuminate\Support\Collection;

class GkiServiceData
{
public static function all(): Collection
{
return collect([
self::strategy(),
self::sprint(),
self::build(),
]);
}

public static function findBySlug(string $slug): ?array
{
return self::all()->firstWhere('slug', $slug);
}

private static function strategy(): array
{
return [
'slug' => 'gki-strategy',
'name' => 'GKI Strategy',
'teaser' => 'Strategische Einordnung von KI im Unternehmen.',
'features' => [
'KI-Readiness Assessment',
'Identifikation priorisierter Use Cases',
'Business Case & Wertschöpfungslogik',
'Governance- und Compliance-Rahmen',
'Roadmap (6–12 Monate)',
],
'closing' => null,
'audience' => 'Ideal für Geschäftsleitungen, Innovationsverantwortliche und Hochschulen.',
];
}

private static function sprint(): array
{
return [
'slug' => 'gki-sprint',
'name' => 'GKI Sprint',
'teaser' => 'Vom Problem zum funktionierenden Prototyp in 2–5 Tagen.',
'features' => [
'Use-Case-Schärfung',
'Prompt-Architektur',
'MVP-Entwicklung (z.B. interner Copilot, Wissensagent, Automationslösung)',
'Nutzer-Test',
'Skalierungsentscheidung',
],
'closing' => 'Kein PowerPoint. Nur funktionierende Systeme.',
'audience' => null,
];
}

private static function build(): array
{
return [
'slug' => 'gki-build',
'name' => 'GKI Build',
'teaser' => 'Technische Integration in bestehende Systeme.',
'features' => [
'API-Integration',
'CRM- / ERP-Anbindung',
'Interne Wissens-GPTs',
'Automatisierungsstrecken',
'Dokumentation & Betriebskonzept',
],
'closing' => 'Wir bauen Lösungen, die produktiv laufen – nicht Demo-Umgebungen.',
'audience' => null,
];
}
}
6 changes: 5 additions & 1 deletion app/Enums/ContactSectionEnum.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@

enum ContactSectionEnum: string
{
const string EMPLOYEES = 'employees';
const string SOFTWARE_ENGINERING = 'software_engineering';
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo in constant name: "ENGINERING" should be "ENGINEERING"

Suggested change
const string SOFTWARE_ENGINERING = 'software_engineering';
const string SOFTWARE_ENGINEERING = 'software_engineering';

Copilot uses AI. Check for mistakes.

const string DIGITAL_TRANSFORMATION = 'digital_transformation';

const string SCANNING = 'scanning';

const string COLLABORATIONS = 'collaborations';

Expand Down
19 changes: 19 additions & 0 deletions app/Http/Controllers/Ai/AiIndexController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

namespace App\Http\Controllers\Ai;

use App\Actions\PageAction;
use App\Data\GkiServiceData;
use App\Http\Controllers\Controller;
use Illuminate\View\View;

class AiIndexController extends Controller
{
public function __invoke(): View
{
return view('app.ai.index')->with([
'page' => (new PageAction(locale: null, routeName: 'ai.index'))->default(),
'services' => GkiServiceData::all(),
]);
}
}
23 changes: 23 additions & 0 deletions app/Http/Controllers/Ai/AiShowController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

namespace App\Http\Controllers\Ai;

use App\Actions\PageAction;
use App\Data\GkiServiceData;
use App\Http\Controllers\Controller;
use Illuminate\View\View;

class AiShowController extends Controller
{
public function __invoke(string $slug): View
{
$service = GkiServiceData::findBySlug($slug);

abort_unless((bool) $service, 404);

return view('app.ai.show')->with([
'page' => (new PageAction(locale: null, routeName: 'ai.index'))->default(),
'service' => $service,
]);
}
}
11 changes: 11 additions & 0 deletions app/Http/Controllers/Contact/ContactIndexController.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,19 @@ class ContactIndexController extends Controller
*/
public function __invoke(): View
{
$openingHours = [
['day' => 'Monday', 'open' => '08:00', 'close' => '18:00'],
['day' => 'Tuesday', 'open' => '08:00', 'close' => '18:00'],
['day' => 'Wednesday', 'open' => '08:00', 'close' => '18:00'],
['day' => 'Thursday', 'open' => '08:00', 'close' => '18:00'],
['day' => 'Friday', 'open' => '08:00', 'close' => '18:00'],
['day' => 'Saturday', 'open' => '08:00', 'close' => '18:00'],
['day' => 'Sunday', 'open' => null, 'close' => null],
];

return view('app.contact.index')->with([
'page' => (new PageAction(locale: null, routeName: 'contact.index'))->default(),
'openingHours' => $openingHours,
]);
}
}
Loading