Skip to content

Commit 6cc3824

Browse files
Merge pull request #12 from designbycode/feat/dynamic-fonts-1411746235997234818
Implement Dynamic Google Fonts Registry
2 parents 66e4adf + 7947396 commit 6cc3824

17 files changed

Lines changed: 347 additions & 70 deletions

File tree

.github/workflows/tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ jobs:
1919
runs-on: ubuntu-latest
2020
strategy:
2121
matrix:
22-
php-version: ['8.3', '8.4', '8.5']
22+
php-version: ['8.4', '8.5']
2323

2424
steps:
2525
- name: Checkout code

app/Http/Controllers/FontsController.php

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,25 +2,30 @@
22

33
namespace App\Http\Controllers;
44

5-
use App\Models\Font;
5+
use App\Services\FontService;
66
use Inertia\Inertia;
7+
use Illuminate\Support\Str;
78

89
class FontsController extends Controller
910
{
10-
public function index()
11+
public function index(FontService $fontService)
1112
{
13+
$fonts = collect($fontService->getFonts())->map(fn ($f) => [
14+
'id' => $f['id'],
15+
'name' => "font-{$f['id']}",
16+
'title' => $f['family'],
17+
'fontFamily' => "'{$f['family']}', {$f['category']}",
18+
'fontProvider' => $f['type'],
19+
'fontImport' => Str::studly($f['family']),
20+
'fontVariable' => "--font-{$f['id']}",
21+
'fontWeight' => $f['weights'],
22+
'fontSubsets' => $f['subsets'],
23+
'fontDependency' => ($f['variable'] ?? false) ? "@fontsource-variable/{$f['id']}" : "@fontsource/{$f['id']}",
24+
'category' => $f['category'],
25+
])->values();
26+
1227
return Inertia::render('fonts/index', [
13-
'fonts' => Font::query()->orderBy('title')->get()->map(fn ($f) => [
14-
'name' => $f->name,
15-
'title' => $f->title,
16-
'fontFamily' => $f->font_family,
17-
'fontProvider' => $f->font_provider,
18-
'fontImport' => $f->font_import,
19-
'fontVariable' => $f->font_variable,
20-
'fontWeight' => $f->font_weight,
21-
'fontSubsets' => $f->font_subsets,
22-
'fontDependency' => $f->font_dependency,
23-
]),
28+
'fonts' => $fonts,
2429
]);
2530
}
2631
}

app/Http/Controllers/RegistriesController.php

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,13 @@
66
use App\Models\Font;
77
use App\Models\Registry;
88
use App\Models\Theme;
9+
use App\Services\FontService;
910
use Illuminate\Http\JsonResponse;
11+
use Illuminate\Support\Str;
1012

1113
class RegistriesController extends Controller
1214
{
13-
public function show(string $type, string $name): JsonResponse
15+
public function show(string $type, string $name, FontService $fontService): JsonResponse
1416
{
1517
$model = match ($type) {
1618
'fonts' => Font::class,
@@ -20,16 +22,35 @@ public function show(string $type, string $name): JsonResponse
2022
};
2123

2224
if ($name === 'registry') {
23-
$items = $model::all()->map->toRegistry()->values();
25+
$items = $model::all()->map->toRegistry();
26+
27+
if ($type === 'fonts') {
28+
$fonts = collect($fontService->getFonts())->map(fn ($f) => $fontService->toRegistry($f));
29+
$items = $items->merge($fonts);
30+
}
2431

2532
return response()->json([
2633
'$schema' => 'https://ui.shadcn.com/schema/registry.json',
2734
'name' => 'designbycode',
2835
'homepage' => 'https://ui.designbycode.co.za',
29-
'items' => $items->all(),
36+
'items' => $items->values()->all(),
3037
]);
3138
}
3239

40+
if ($type === 'fonts') {
41+
$font = Font::where('name', $name)->first();
42+
if ($font) {
43+
return response()->json($font->toRegistry());
44+
}
45+
46+
$fontId = Str::after($name, 'font-');
47+
$fontData = $fontService->getFont($fontId);
48+
49+
if ($fontData) {
50+
return response()->json($fontService->toRegistry($fontData));
51+
}
52+
}
53+
3354
return response()->json(
3455
$model::where('name', $name)->firstOrFail()->toRegistry()
3556
);

app/Providers/AppServiceProvider.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,11 @@ protected function configureDefaults(): void
5454

5555
protected function shareThemes(): void
5656
{
57-
if (! Schema::hasTable('themes')) {
57+
try {
58+
if (! Schema::hasTable('themes')) {
59+
return;
60+
}
61+
} catch (\Exception) {
5862
return;
5963
}
6064

app/Services/FontService.php

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
<?php
2+
3+
namespace App\Services;
4+
5+
use Illuminate\Support\Facades\Http;
6+
use Illuminate\Support\Facades\Cache;
7+
use Illuminate\Support\Str;
8+
9+
class FontService
10+
{
11+
protected string $baseUrl = 'https://api.fontsource.org/v1';
12+
13+
public function getFonts(): array
14+
{
15+
return Cache::remember('fonts_list', 86400, function () {
16+
$response = Http::get("{$this->baseUrl}/fonts");
17+
if ($response->failed()) {
18+
return [];
19+
}
20+
return $response->json();
21+
});
22+
}
23+
24+
public function getFont(string $id): ?array
25+
{
26+
return Cache::remember("font_details_{$id}", 86400, function () use ($id) {
27+
$response = Http::get("{$this->baseUrl}/fonts/{$id}");
28+
if ($response->failed()) {
29+
return null;
30+
}
31+
return $response->json();
32+
});
33+
}
34+
35+
public function toRegistry(array $fontData): array
36+
{
37+
$id = $fontData['id'];
38+
$family = $fontData['family'];
39+
$importName = Str::studly($family);
40+
$variableName = "--font-{$id}";
41+
$isVariable = $fontData['variable'] ?? false;
42+
$dependency = $isVariable ? "@fontsource-variable/{$id}" : "@fontsource/{$id}";
43+
44+
return [
45+
'name' => "font-{$id}",
46+
'title' => $family,
47+
'type' => 'registry:font',
48+
'meta' => [
49+
'version' => '1.0.0',
50+
'category' => 'fonts'
51+
],
52+
'author' => 'designbycode',
53+
'font' => [
54+
'family' => "'{$family}', sans-serif",
55+
'provider' => 'google',
56+
'import' => $importName,
57+
'variable' => $variableName,
58+
'subsets' => $fontData['subsets'] ?? ['latin'],
59+
'dependency' => $dependency
60+
]
61+
];
62+
}
63+
}

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
],
1010
"license": "MIT",
1111
"require": {
12-
"php": "^8.3",
12+
"php": "^8.4",
1313
"inertiajs/inertia-laravel": "^3.0",
1414
"laravel/cashier-paddle": "^2.8",
1515
"laravel/fortify": "^1.34",

resources/js/hooks/use-in-view.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { useEffect, useState, useRef } from 'react';
2+
3+
export interface UseInViewOptions extends IntersectionObserverInit {
4+
triggerOnce?: boolean;
5+
}
6+
7+
export function useInView(options?: UseInViewOptions) {
8+
const [inView, setInView] = useState(false);
9+
const ref = useRef<any>(null);
10+
11+
useEffect(() => {
12+
const currentRef = ref.current;
13+
const observer = new IntersectionObserver(([entry]) => {
14+
if (entry.isIntersecting) {
15+
setInView(true);
16+
if (options?.triggerOnce) {
17+
observer.unobserve(entry.target);
18+
}
19+
} else if (!options?.triggerOnce) {
20+
setInView(false);
21+
}
22+
}, options);
23+
24+
if (currentRef) {
25+
observer.observe(currentRef);
26+
}
27+
28+
return () => {
29+
if (currentRef) {
30+
observer.unobserve(currentRef);
31+
}
32+
};
33+
}, [options]);
34+
35+
return { ref, inView };
36+
}

resources/js/layouts/main-layout.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import { usePage } from '@inertiajs/react';
2-
import { PlaceholderPattern } from '@/components/ui/placeholder-pattern';
31
import MainFooter from '@/layouts/main/main-footer';
42
import MainNavigation from '@/layouts/main/main-navigation';
53
import { GlowStack } from '@/registry/new-york/components/ui/glow/glow-stack';

resources/js/layouts/main/theme/main-editor-block.tsx

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,7 @@ export default function MainEditorBlock({
165165
// ── Mount ────────────────────────────────────────────────────────────────
166166

167167
useEffect(() => {
168+
// eslint-disable-next-line react-hooks/set-state-in-effect
168169
setMounted(true);
169170
}, []);
170171

@@ -255,9 +256,9 @@ export default function MainEditorBlock({
255256
);
256257
}
257258

258-
// ── Controls ──────────────────────────────────────────────────────────────
259+
// ── Render ────────────────────────────────────────────────────────────────
259260

260-
const Controls = () => (
261+
const controls = (
261262
<div className="flex items-center gap-1">
262263
{showCopyButton && (
263264
<div className="flex items-center gap-1">
@@ -308,8 +309,6 @@ export default function MainEditorBlock({
308309
</div>
309310
);
310311

311-
// ── Render ────────────────────────────────────────────────────────────────
312-
313312
return (
314313
<div
315314
ref={containerRef}
@@ -326,13 +325,13 @@ export default function MainEditorBlock({
326325
<span className="font-mono text-sm font-bold text-muted-foreground">
327326
{normalizedLanguage}
328327
</span>
329-
<Controls />
328+
{controls}
330329
</div>
331330
)}
332331

333332
{variant === 'minimal' && (
334333
<div className="absolute top-2 right-2 z-10 opacity-0 transition-opacity group-hover/editor-block:opacity-100">
335-
<Controls />
334+
{controls}
336335
</div>
337336
)}
338337

resources/js/layouts/main/theme/main-package-manager-search.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,13 +83,20 @@ export function MainPackageManagerSearch({
8383
const initialName = defaultRegistry ?? extractRegistryName(initialCodes);
8484
const registryName = selectedRegistry || initialName;
8585

86-
useEffect(() => {
86+
const [lastQuery, setLastQuery] = useState('');
87+
if (searchQuery !== lastQuery) {
88+
setLastQuery(searchQuery);
8789
if (!searchQuery || searchQuery.length < 2) {
88-
setResults((prev) => (prev.length > 0 ? [] : prev));
90+
setResults([]);
91+
}
92+
}
8993

94+
useEffect(() => {
95+
if (!searchQuery || searchQuery.length < 2) {
9096
return;
9197
}
9298

99+
// eslint-disable-next-line react-hooks/set-state-in-effect
93100
setLoading(true);
94101
const params = new URLSearchParams({ q: searchQuery });
95102
fetch(`/api/registries/search?${params}`)

0 commit comments

Comments
 (0)