Skip to content

Commit 3ea2532

Browse files
Merge pull request #14 from designbycode/feature/theme-tag-system-16913403527467858523
Implement Theme Tag System
2 parents 94607de + 6023056 commit 3ea2532

114 files changed

Lines changed: 3033 additions & 3805 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.agents/skills/inertia-react-development/SKILL.md

Lines changed: 0 additions & 524 deletions
This file was deleted.

.claude/skills/inertia-react-development/SKILL.md

Lines changed: 0 additions & 524 deletions
This file was deleted.

AGENTS.md

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ The Laravel Boost guidelines are specifically curated by Laravel maintainers for
99

1010
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.
1111

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

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

121-
=== herd rules ===
122-
123-
# Laravel Herd
124-
125-
- 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.
126-
- 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.
127-
128121
=== tests rules ===
129122

130123
# Test Enforcement

CLAUDE.md

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ The Laravel Boost guidelines are specifically curated by Laravel maintainers for
99

1010
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.
1111

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

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

121-
=== herd rules ===
122-
123-
# Laravel Herd
124-
125-
- 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.
126-
- 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.
127-
128121
=== tests rules ===
129122

130123
# Test Enforcement

GEMINI.md

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ The Laravel Boost guidelines are specifically curated by Laravel maintainers for
99

1010
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.
1111

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

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

121-
=== herd rules ===
122-
123-
# Laravel Herd
124-
125-
- 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.
126-
- 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.
127-
128121
=== tests rules ===
129122

130123
# Test Enforcement

app/Concerns/HasTheme.php

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,6 @@ public static function fromRegistry(array $data): static
120120

121121
$instance->meta = $data['meta'] ?? null;
122122
$instance->docs = $data['docs'] ?? null;
123-
$instance->categories = $data['categories'] ?? [];
124123

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

@@ -154,7 +153,7 @@ public function toRegistry(): array
154153
'cssVars' => $this->buildCssVars(),
155154
'meta' => $this->meta,
156155
'docs' => $this->docs,
157-
'categories' => $this->categories ?? [],
156+
'tags' => $this->tags->pluck('name')->toArray(),
158157
];
159158

160159
if ($this->type === 'registry:style') {

app/Http/Controllers/ThemesController.php

Lines changed: 57 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use Illuminate\Support\Facades\Http;
99
use Illuminate\Support\Str;
1010
use Inertia\Inertia;
11+
use Spatie\Tags\Tag;
1112

1213
class ThemesController extends Controller
1314
{
@@ -85,13 +86,13 @@ public function store(Request $request)
8586
}
8687
}
8788

88-
if (isset($data['categories'])) {
89-
if (! is_array($data['categories'])) {
90-
$errors[] = '"categories" must be an array.';
89+
if (isset($data['tags'])) {
90+
if (! is_array($data['tags'])) {
91+
$errors[] = '"tags" must be an array.';
9192
} else {
92-
foreach ($data['categories'] as $i => $category) {
93-
if (! is_string($category)) {
94-
$errors[] = "\"categories.{$i}\" must be a string.";
93+
foreach ($data['tags'] as $i => $tag) {
94+
if (! is_string($tag)) {
95+
$errors[] = "\"tags.{$i}\" must be a string.";
9596
}
9697
}
9798
}
@@ -120,37 +121,73 @@ public function store(Request $request)
120121
$theme->user_id = auth()->id();
121122
$theme->save();
122123

124+
if (isset($data['tags'])) {
125+
$theme->attachTags($data['tags']);
126+
}
127+
123128
Cache::forget('themes:total_count');
124-
Cache::forget('themes:available_categories');
129+
Cache::forget('themes:available_tags');
125130

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

130135
public function index()
131136
{
132-
$availableCategories = Cache::remember('themes:available_categories', 3600, fn () => Theme::query()
133-
->select('categories')
134-
->get()
135-
->pluck('categories')
136-
->flatten()
137-
->unique()
138-
->sort()
139-
->values()
140-
->all());
137+
$availableTags = Cache::remember('themes:available_tags', 3600, function () {
138+
return Tag::query()
139+
->whereExists(function ($query) {
140+
$query->select(\Illuminate\Support\Facades\DB::raw(1))
141+
->from('taggables')
142+
->whereColumn('taggables.tag_id', 'tags.id')
143+
->where('taggables.taggable_type', Theme::class);
144+
})
145+
->get()
146+
->pluck('name')
147+
->sort()
148+
->values()
149+
->all();
150+
});
151+
152+
$query = Theme::query()->with('tags');
153+
154+
if ($search = request('search')) {
155+
$query->where(function ($q) use ($search) {
156+
$q->where('name', 'like', "%{$search}%")
157+
->orWhere('title', 'like', "%{$search}%")
158+
->orWhere('description', 'like', "%{$search}%");
159+
});
160+
}
161+
162+
if ($tag = request('tag')) {
163+
$query->withAnyTags([$tag]);
164+
}
165+
166+
$themes = $query->paginate(12)->withQueryString();
167+
168+
$themes->getCollection()->transform(function ($theme) {
169+
$data = $theme->toArray();
170+
$data['tags'] = $theme->tags->pluck('name')->toArray();
171+
172+
return $data;
173+
});
141174

142175
return Inertia::render('themes/index', [
143-
'themes' => Inertia::scroll(Theme::paginate(12)->withQueryString()),
144-
'filters' => request()->only(['search', 'category']),
145-
'availableCategories' => $availableCategories,
176+
'themes' => Inertia::scroll($themes),
177+
'filters' => request()->only(['search', 'tag']),
178+
'availableTags' => $availableTags,
146179
'totalThemesCount' => Cache::remember('themes:total_count', 3600, fn () => Theme::count()),
147180
]);
148181
}
149182

150183
public function show(Theme $theme)
151184
{
185+
$theme->load('tags');
186+
$data = $theme->toArray();
187+
$data['tags'] = $theme->tags->pluck('name')->toArray();
188+
152189
return Inertia::render('themes/show', [
153-
'theme' => $theme,
190+
'theme' => $data,
154191
'css' => $theme->toCss(),
155192
]);
156193
}

app/Models/Theme.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
use Illuminate\Database\Eloquent\Model;
1010
use Illuminate\Database\Eloquent\Relations\BelongsTo;
1111
use Illuminate\Database\Eloquent\SoftDeletes;
12+
use Spatie\Tags\HasTags;
1213

1314
#[Fillable([
1415
'name', 'type', 'title', 'description', 'author',
@@ -19,14 +20,14 @@
1920
'font_family', 'font_mono', 'font_serif',
2021
'font_provider', 'font_import', 'font_variable',
2122
'font_weight', 'font_subsets', 'font_selector', 'font_dependency',
22-
'meta', 'docs', 'categories',
23+
'meta', 'docs',
2324
'extends',
2425
'style', 'icon_library', 'base_color', 'theme',
2526
])]
2627
#[ObservedBy(ThemeObserver::class)]
2728
class Theme extends Model
2829
{
29-
use HasTheme, SoftDeletes;
30+
use HasTags, HasTheme, SoftDeletes;
3031

3132
protected $table = 'themes';
3233

@@ -57,7 +58,6 @@ protected function casts(): array
5758
'font_subsets' => 'array',
5859
'theme' => 'array',
5960
'meta' => 'array',
60-
'categories' => 'array',
6161
'created_at' => 'datetime',
6262
'updated_at' => 'datetime',
6363
'deleted_at' => 'datetime',

app/Observers/ThemeObserver.php

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@
88

99
class ThemeObserver
1010
{
11+
/**
12+
* @var array<string>
13+
*/
14+
protected static array $suggestedTagsMap = [];
15+
1116
public function creating(Theme $theme): void
1217
{
1318
if (! $theme->title) {
@@ -16,10 +21,20 @@ public function creating(Theme $theme): void
1621

1722
if (! $theme->description) {
1823
$ai = app(AiService::class);
19-
$theme->description = $ai->generateThemeDescription(
24+
$metadata = $ai->generateThemeMetadata(
2025
$theme->name,
2126
$theme->vars_light ?? []
2227
);
28+
$theme->description = $metadata['description'];
29+
static::$suggestedTagsMap[$theme->name] = $metadata['tags'];
30+
}
31+
}
32+
33+
public function created(Theme $theme): void
34+
{
35+
if (isset(static::$suggestedTagsMap[$theme->name])) {
36+
$theme->attachTags(static::$suggestedTagsMap[$theme->name]);
37+
unset(static::$suggestedTagsMap[$theme->name]);
2338
}
2439
}
2540
}

app/Services/AiService.php

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,25 @@
66

77
class AiService
88
{
9-
public function generateThemeDescription(string $name, array $colors): ?string
9+
/**
10+
* @return array{description: ?string, tags: array<string>}
11+
*/
12+
public function generateThemeMetadata(string $name, array $colors): array
1013
{
1114
$apiKey = config('services.openrouter.key');
1215

1316
if (! $apiKey) {
14-
return null;
17+
return ['description' => null, 'tags' => []];
1518
}
1619

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

21-
$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.";
24+
$prompt = "Generate metadata for a UI theme named \"{$name}\" that uses these colors: {$colorList}.
25+
Return the result in JSON format with two keys:
26+
1. \"description\": a short, engaging description (max 2 sentences) highlighting the mood or style.
27+
2. \"tags\": an array of 2 to 6 relevant style tags (e.g., \"warm\", \"cold\", \"retro\", \"vintage\", \"punk\", \"nature\", \"tech\", \"bold\", \"minimal\", \"elegant\").";
2228

2329
$response = Http::withHeaders([
2430
'Authorization' => 'Bearer '.$apiKey,
@@ -32,14 +38,28 @@ public function generateThemeDescription(string $name, array $colors): ?string
3238
'content' => $prompt,
3339
],
3440
],
41+
'response_format' => ['type' => 'json_object'],
3542
]);
3643

3744
if ($response->failed()) {
38-
return null;
45+
return ['description' => null, 'tags' => []];
3946
}
4047

4148
$data = $response->json();
49+
$content = $data['choices'][0]['message']['content'] ?? '{}';
50+
$decoded = json_decode($content, true);
51+
52+
return [
53+
'description' => $decoded['description'] ?? null,
54+
'tags' => $decoded['tags'] ?? [],
55+
];
56+
}
4257

43-
return $data['choices'][0]['message']['content'] ?? null;
58+
/**
59+
* @deprecated Use generateThemeMetadata instead.
60+
*/
61+
public function generateThemeDescription(string $name, array $colors): ?string
62+
{
63+
return $this->generateThemeMetadata($name, $colors)['description'];
4464
}
4565
}

0 commit comments

Comments
 (0)