Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
73.81% covered (warning)
73.81%
62 / 84
27.27% covered (danger)
27.27%
3 / 11
CRAP
0.00% covered (danger)
0.00%
0 / 1
WidgetController
73.81% covered (warning)
73.81%
62 / 84
27.27% covered (danger)
27.27%
3 / 11
38.14
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 index
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 area
87.50% covered (warning)
87.50%
7 / 8
0.00% covered (danger)
0.00%
0 / 1
2.01
 store
92.86% covered (success)
92.86%
13 / 14
0.00% covered (danger)
0.00%
0 / 1
2.00
 edit
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
 form
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 update
82.35% covered (warning)
82.35%
14 / 17
0.00% covered (danger)
0.00%
0 / 1
9.45
 reorder
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 toggle
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 starterLayout
94.44% covered (success)
94.44%
17 / 18
0.00% covered (danger)
0.00%
0 / 1
3.00
 destroy
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace App\Http\Controllers\admin;
4
5use App\Http\Controllers\Controller;
6use App\Models\Widget;
7use App\Service\CacheService;
8use App\Services\Builder\WidgetService;
9use Illuminate\Http\JsonResponse;
10use Illuminate\Http\RedirectResponse;
11use Illuminate\Http\Request;
12use Illuminate\Validation\Rule;
13use Illuminate\View\View;
14
15/**
16 * Settings → Widgets. Each "area" (config/builder.php → widget_areas) holds
17 * an ordered stack of blocks. The homepage is just the `home_main` area.
18 * The storefront renders an area via <x-widget-area name="…">.
19 */
20class WidgetController extends Controller
21{
22    public function __construct(private WidgetService $widgets) {}
23
24    public function index(): View
25    {
26        $areas = config('builder.widget_areas', []);
27        $counts = Widget::query()->selectRaw('area, count(*) as c')->groupBy('area')->pluck('c', 'area');
28
29        return view('admin.widgets.index', compact('areas', 'counts'));
30    }
31
32    public function area(string $area): View|RedirectResponse
33    {
34        if (! array_key_exists($area, config('builder.widget_areas', []))) {
35            return redirect()->route('admin.widgets.index');
36        }
37
38        return view('admin.widgets.area', [
39            'area' => $area,
40            'areaLabel' => config("builder.widget_areas.$area"),
41            'widgets' => $this->widgets->area($area, onlyEnabled: false),
42            'types' => $this->widgets->types(),
43        ]);
44    }
45
46    public function store(Request $request, string $area): RedirectResponse|JsonResponse
47    {
48        $data = $request->validate([
49            'type' => ['required', Rule::in(array_keys($this->widgets->types()))],
50        ]);
51
52        $handler = $this->widgets->make($data['type']);
53
54        $widget = Widget::create([
55            'area' => $area,
56            'type' => $data['type'],
57            'settings' => $handler->defaults(),
58            'sort_order' => (int) Widget::where('area', $area)->max('sort_order') + 1,
59            'enabled' => true,
60        ]);
61
62        if ($request->expectsJson()) {
63            return response()->json(['ok'=>true,'id'=>$widget->id,'form_url'=>route('admin.widgets.form',$widget)]);
64        }
65        return redirect()->route('admin.widgets.edit', $widget)->with('success', $handler->label() . ' added — set it up below.');
66    }
67
68    public function edit(Widget $widget): View|RedirectResponse
69    {
70        $handler = $widget->handler();
71        if (! $handler) {
72            return redirect()->route('admin.widgets.area', $widget->area)->with('error', 'That widget type is no longer available.');
73        }
74
75        return view('admin.widgets.edit', [
76            'widget' => $widget,
77            'handler' => $handler,
78            'values' => array_merge($handler->defaults(), $widget->settings ?? []),
79        ]);
80    }
81
82    public function form(Widget $widget): View
83    {
84        $handler = $widget->handler();
85        abort_unless($handler, 404);
86        return view('admin.widgets.partials.modal-form', [
87            'widget'=>$widget,'handler'=>$handler,'values'=>array_merge($handler->defaults(),$widget->settings??[]),
88        ]);
89    }
90
91    public function update(Request $request, Widget $widget): RedirectResponse|JsonResponse
92    {
93        $handler = $widget->handler();
94        abort_unless($handler, 404);
95
96        $settings = (array) $request->input('settings', []);
97        $clean = [];
98        foreach ($handler->fields() as $key => $def) {
99            $clean[$key] = match ($def['type'] ?? 'text') {
100                'toggle' => (bool) ($settings[$key] ?? false),
101                'number' => is_numeric($settings[$key] ?? null) ? $settings[$key] + 0 : ($def['default'] ?? 0),
102                'repeater' => array_values(array_filter((array) ($settings[$key] ?? []), fn ($r) => is_array($r) && array_filter($r))),
103                default => $settings[$key] ?? ($def['default'] ?? null),
104            };
105        }
106
107        $widget->update([
108            'settings' => $clean,
109            'enabled' => $request->boolean('enabled'),
110        ]);
111
112        if ($request->expectsJson()) return response()->json(['ok'=>true,'message'=>'Block saved.']);
113
114        return redirect()->route('admin.widgets.area', $widget->area)->with('success', 'Widget saved.');
115    }
116
117    public function reorder(Request $request, string $area): JsonResponse
118    {
119        $ids = (array) $request->input('ids', []);
120
121        foreach (array_values($ids) as $position => $id) {
122            Widget::where('area', $area)->where('id', $id)->update(['sort_order' => $position]);
123        }
124
125        CacheService::forgetWidgets();
126
127        return response()->json(['ok' => true]);
128    }
129
130    public function toggle(Widget $widget): RedirectResponse
131    {
132        $widget->update(['enabled' => ! $widget->enabled]);
133
134        return back()->with('success', $widget->enabled ? 'Block is now visible.' : 'Block is now hidden.');
135    }
136
137    public function starterLayout(string $area): RedirectResponse
138    {
139        abort_unless($area === 'home_main', 404);
140
141        if (Widget::where('area', $area)->exists()) {
142            return back()->with('error', 'Starter layout can only be added to an empty homepage. Your current blocks were left unchanged.');
143        }
144
145        $blocks = [
146            ['hero_slider', []],
147            ['category_grid', ['title' => 'Shop by category', 'limit' => 10, 'parents_only' => true]],
148            ['product_carousel', ['title' => 'New Arrivals', 'layout' => 'carousel', 'source' => 'newest', 'category_id' => '', 'limit' => 8]],
149            ['product_carousel', ['title' => 'Featured Collection', 'layout' => 'carousel', 'source' => 'featured', 'category_id' => '', 'limit' => 8]],
150            ['product_carousel', ['title' => 'Trending Now', 'layout' => 'grid', 'source' => 'newest', 'category_id' => '', 'limit' => 8]],
151            ['product_carousel', ['title' => 'Best Sellers', 'layout' => 'carousel', 'source' => 'newest', 'category_id' => '', 'limit' => 8]],
152            ['product_carousel', ['title' => 'Special Picks', 'layout' => 'carousel', 'source' => 'featured', 'category_id' => '', 'limit' => 8]],
153            ['product_carousel', ['title' => 'Customer Favorites', 'layout' => 'grid', 'source' => 'newest', 'category_id' => '', 'limit' => 8]],
154            ['product_carousel', ['title' => 'More to Explore', 'layout' => 'carousel', 'source' => 'newest', 'category_id' => '', 'limit' => 8]],
155            ['recent_reviews', ['title' => 'What our customers say', 'limit' => 8]],
156        ];
157
158        foreach ($blocks as $position => [$type, $settings]) {
159            Widget::create(compact('area', 'type', 'settings') + ['sort_order' => $position, 'enabled' => true]);
160        }
161
162        return back()->with('success', 'Starter homepage added. Drag any block to reorder it, then click Edit to change its content.');
163    }
164
165    public function destroy(Widget $widget): RedirectResponse
166    {
167        $area = $widget->area;
168        $widget->delete();
169
170        return redirect()->route('admin.widgets.area', $area)->with('success', 'Widget removed.');
171    }
172}