Skip to content
Merged
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
524 changes: 0 additions & 524 deletions .agents/skills/inertia-react-development/SKILL.md

This file was deleted.

524 changes: 0 additions & 524 deletions .claude/skills/inertia-react-development/SKILL.md

This file was deleted.

9 changes: 1 addition & 8 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ The Laravel Boost guidelines are specifically curated by Laravel maintainers for

This application is a Laravel application and its main Laravel ecosystems package & versions are below. You are an expert with them all. Ensure you abide by these specific packages & versions.

- php - 8.4
- php - 8.3
- inertiajs/inertia-laravel (INERTIA_LARAVEL) - v3
- laravel/fortify (FORTIFY) - v1
- laravel/framework (LARAVEL) - v13
Expand Down Expand Up @@ -118,13 +118,6 @@ This project has domain-specific skills available in `**/skills/**`. You MUST ac

- Laravel can be deployed using [Laravel Cloud](https://cloud.laravel.com/), which is the fastest way to deploy and scale production Laravel applications.

=== herd rules ===

# Laravel Herd

- The application is served by Laravel Herd at `https?://[kebab-case-project-dir].test`. Use the `get-absolute-url` tool to generate valid URLs. Never run commands to serve the site. It is always available.
- Use the `herd` CLI to manage services, PHP versions, and sites (e.g. `herd sites`, `herd services:start <service>`, `herd php:list`). Run `herd list` to discover all available commands.

=== tests rules ===

# Test Enforcement
Expand Down
9 changes: 1 addition & 8 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ The Laravel Boost guidelines are specifically curated by Laravel maintainers for

This application is a Laravel application and its main Laravel ecosystems package & versions are below. You are an expert with them all. Ensure you abide by these specific packages & versions.

- php - 8.4
- php - 8.3
- inertiajs/inertia-laravel (INERTIA_LARAVEL) - v3
- laravel/fortify (FORTIFY) - v1
- laravel/framework (LARAVEL) - v13
Expand Down Expand Up @@ -118,13 +118,6 @@ This project has domain-specific skills available in `**/skills/**`. You MUST ac

- Laravel can be deployed using [Laravel Cloud](https://cloud.laravel.com/), which is the fastest way to deploy and scale production Laravel applications.

=== herd rules ===

# Laravel Herd

- The application is served by Laravel Herd at `https?://[kebab-case-project-dir].test`. Use the `get-absolute-url` tool to generate valid URLs. Never run commands to serve the site. It is always available.
- Use the `herd` CLI to manage services, PHP versions, and sites (e.g. `herd sites`, `herd services:start <service>`, `herd php:list`). Run `herd list` to discover all available commands.

=== tests rules ===

# Test Enforcement
Expand Down
9 changes: 1 addition & 8 deletions GEMINI.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ The Laravel Boost guidelines are specifically curated by Laravel maintainers for

This application is a Laravel application and its main Laravel ecosystems package & versions are below. You are an expert with them all. Ensure you abide by these specific packages & versions.

- php - 8.4
- php - 8.3
- inertiajs/inertia-laravel (INERTIA_LARAVEL) - v3
- laravel/fortify (FORTIFY) - v1
- laravel/framework (LARAVEL) - v13
Expand Down Expand Up @@ -118,13 +118,6 @@ This project has domain-specific skills available in `**/skills/**`. You MUST ac

- Laravel can be deployed using [Laravel Cloud](https://cloud.laravel.com/), which is the fastest way to deploy and scale production Laravel applications.

=== herd rules ===

# Laravel Herd

- The application is served by Laravel Herd at `https?://[kebab-case-project-dir].test`. Use the `get-absolute-url` tool to generate valid URLs. Never run commands to serve the site. It is always available.
- Use the `herd` CLI to manage services, PHP versions, and sites (e.g. `herd sites`, `herd services:start <service>`, `herd php:list`). Run `herd list` to discover all available commands.

=== tests rules ===

# Test Enforcement
Expand Down
3 changes: 1 addition & 2 deletions app/Concerns/HasTheme.php
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,6 @@ public static function fromRegistry(array $data): static

$instance->meta = $data['meta'] ?? null;
$instance->docs = $data['docs'] ?? null;
$instance->categories = $data['categories'] ?? [];

$instance->extends = $data['extends'] ?? null;

Expand Down Expand Up @@ -154,7 +153,7 @@ public function toRegistry(): array
'cssVars' => $this->buildCssVars(),
'meta' => $this->meta,
'docs' => $this->docs,
'categories' => $this->categories ?? [],
'tags' => $this->tags->pluck('name')->toArray(),
];

if ($this->type === 'registry:style') {
Expand Down
77 changes: 57 additions & 20 deletions app/Http/Controllers/ThemesController.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Str;
use Inertia\Inertia;
use Spatie\Tags\Tag;

class ThemesController extends Controller
{
Expand Down Expand Up @@ -85,13 +86,13 @@ public function store(Request $request)
}
}

if (isset($data['categories'])) {
if (! is_array($data['categories'])) {
$errors[] = '"categories" must be an array.';
if (isset($data['tags'])) {
if (! is_array($data['tags'])) {
$errors[] = '"tags" must be an array.';
} else {
foreach ($data['categories'] as $i => $category) {
if (! is_string($category)) {
$errors[] = "\"categories.{$i}\" must be a string.";
foreach ($data['tags'] as $i => $tag) {
if (! is_string($tag)) {
$errors[] = "\"tags.{$i}\" must be a string.";
}
}
}
Expand Down Expand Up @@ -120,37 +121,73 @@ public function store(Request $request)
$theme->user_id = auth()->id();
$theme->save();

if (isset($data['tags'])) {
$theme->attachTags($data['tags']);
}

Cache::forget('themes:total_count');
Cache::forget('themes:available_categories');
Cache::forget('themes:available_tags');

return redirect()->route('themes.show', $theme->name)
->with('success', 'Theme created successfully.');
}

public function index()
{
$availableCategories = Cache::remember('themes:available_categories', 3600, fn () => Theme::query()
->select('categories')
->get()
->pluck('categories')
->flatten()
->unique()
->sort()
->values()
->all());
$availableTags = Cache::remember('themes:available_tags', 3600, function () {
return Tag::query()
->whereExists(function ($query) {
$query->select(\Illuminate\Support\Facades\DB::raw(1))
->from('taggables')
->whereColumn('taggables.tag_id', 'tags.id')
->where('taggables.taggable_type', Theme::class);
})
->get()
->pluck('name')
->sort()
->values()
->all();
});

$query = Theme::query()->with('tags');

if ($search = request('search')) {
$query->where(function ($q) use ($search) {
$q->where('name', 'like', "%{$search}%")
->orWhere('title', 'like', "%{$search}%")
->orWhere('description', 'like', "%{$search}%");
});
}

if ($tag = request('tag')) {
$query->withAnyTags([$tag]);
}

$themes = $query->paginate(12)->withQueryString();

$themes->getCollection()->transform(function ($theme) {
$data = $theme->toArray();
$data['tags'] = $theme->tags->pluck('name')->toArray();

return $data;
});

return Inertia::render('themes/index', [
'themes' => Inertia::scroll(Theme::paginate(12)->withQueryString()),
'filters' => request()->only(['search', 'category']),
'availableCategories' => $availableCategories,
'themes' => Inertia::scroll($themes),
'filters' => request()->only(['search', 'tag']),
'availableTags' => $availableTags,
'totalThemesCount' => Cache::remember('themes:total_count', 3600, fn () => Theme::count()),
]);
}

public function show(Theme $theme)
{
$theme->load('tags');
$data = $theme->toArray();
$data['tags'] = $theme->tags->pluck('name')->toArray();

return Inertia::render('themes/show', [
'theme' => $theme,
'theme' => $data,
'css' => $theme->toCss(),
]);
}
Expand Down
6 changes: 3 additions & 3 deletions app/Models/Theme.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\SoftDeletes;
use Spatie\Tags\HasTags;

#[Fillable([
'name', 'type', 'title', 'description', 'author',
Expand All @@ -19,14 +20,14 @@
'font_family', 'font_mono', 'font_serif',
'font_provider', 'font_import', 'font_variable',
'font_weight', 'font_subsets', 'font_selector', 'font_dependency',
'meta', 'docs', 'categories',
'meta', 'docs',
'extends',
'style', 'icon_library', 'base_color', 'theme',
])]
#[ObservedBy(ThemeObserver::class)]
class Theme extends Model
{
use HasTheme, SoftDeletes;
use HasTags, HasTheme, SoftDeletes;

protected $table = 'themes';

Expand Down Expand Up @@ -57,7 +58,6 @@ protected function casts(): array
'font_subsets' => 'array',
'theme' => 'array',
'meta' => 'array',
'categories' => 'array',
'created_at' => 'datetime',
'updated_at' => 'datetime',
'deleted_at' => 'datetime',
Expand Down
17 changes: 16 additions & 1 deletion app/Observers/ThemeObserver.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@

class ThemeObserver
{
/**
* @var array<string>
*/
protected static array $suggestedTagsMap = [];

public function creating(Theme $theme): void
{
if (! $theme->title) {
Expand All @@ -16,10 +21,20 @@ public function creating(Theme $theme): void

if (! $theme->description) {
$ai = app(AiService::class);
$theme->description = $ai->generateThemeDescription(
$metadata = $ai->generateThemeMetadata(
$theme->name,
$theme->vars_light ?? []
);
$theme->description = $metadata['description'];
static::$suggestedTagsMap[$theme->name] = $metadata['tags'];
}
}

public function created(Theme $theme): void
{
if (isset(static::$suggestedTagsMap[$theme->name])) {
$theme->attachTags(static::$suggestedTagsMap[$theme->name]);
unset(static::$suggestedTagsMap[$theme->name]);
}
}
}
30 changes: 25 additions & 5 deletions app/Services/AiService.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,25 @@

class AiService
{
public function generateThemeDescription(string $name, array $colors): ?string
/**
* @return array{description: ?string, tags: array<string>}
*/
public function generateThemeMetadata(string $name, array $colors): array
{
$apiKey = config('services.openrouter.key');

if (! $apiKey) {
return null;
return ['description' => null, 'tags' => []];
}

$colorList = collect($colors)
->map(fn ($value, $key) => "{$key}: {$value}")
->implode(', ');

$prompt = "Generate a short, engaging description (max 2 sentences) for a UI theme named \"{$name}\" that uses these colors: {$colorList}. The description should highlight the mood or style of the theme.";
$prompt = "Generate metadata for a UI theme named \"{$name}\" that uses these colors: {$colorList}.
Return the result in JSON format with two keys:
1. \"description\": a short, engaging description (max 2 sentences) highlighting the mood or style.
2. \"tags\": an array of 2 to 6 relevant style tags (e.g., \"warm\", \"cold\", \"retro\", \"vintage\", \"punk\", \"nature\", \"tech\", \"bold\", \"minimal\", \"elegant\").";

$response = Http::withHeaders([
'Authorization' => 'Bearer '.$apiKey,
Expand All @@ -32,14 +38,28 @@ public function generateThemeDescription(string $name, array $colors): ?string
'content' => $prompt,
],
],
'response_format' => ['type' => 'json_object'],
]);

if ($response->failed()) {
return null;
return ['description' => null, 'tags' => []];
}

$data = $response->json();
$content = $data['choices'][0]['message']['content'] ?? '{}';
$decoded = json_decode($content, true);

return [
'description' => $decoded['description'] ?? null,
'tags' => $decoded['tags'] ?? [],
];
}

return $data['choices'][0]['message']['content'] ?? null;
/**
* @deprecated Use generateThemeMetadata instead.
*/
public function generateThemeDescription(string $name, array $colors): ?string
{
return $this->generateThemeMetadata($name, $colors)['description'];
}
}
1 change: 0 additions & 1 deletion boost.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
"socialite-development",
"wayfinder-development",
"pest-testing",
"inertia-react-development",
"tailwindcss-development"
]
}
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@
"laravel/socialite": "^5.27",
"laravel/tinker": "^3.0",
"laravel/wayfinder": "^0.1.14",
"spatie/laravel-permission": "^7.4"
"spatie/laravel-permission": "^7.4",
"spatie/laravel-tags": "^4.11"
},
"require-dev": {
"fakerphp/faker": "^1.24",
Expand Down
Loading