Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
73.81% |
62 / 84 |
|
27.27% |
3 / 11 |
CRAP | |
0.00% |
0 / 1 |
| WidgetController | |
73.81% |
62 / 84 |
|
27.27% |
3 / 11 |
38.14 | |
0.00% |
0 / 1 |
| __construct | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| index | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
| area | |
87.50% |
7 / 8 |
|
0.00% |
0 / 1 |
2.01 | |||
| store | |
92.86% |
13 / 14 |
|
0.00% |
0 / 1 |
2.00 | |||
| edit | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
6 | |||
| form | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
1 | |||
| update | |
82.35% |
14 / 17 |
|
0.00% |
0 / 1 |
9.45 | |||
| reorder | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
2 | |||
| toggle | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
6 | |||
| starterLayout | |
94.44% |
17 / 18 |
|
0.00% |
0 / 1 |
3.00 | |||
| destroy | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
| 1 | <?php |
| 2 | |
| 3 | namespace App\Http\Controllers\admin; |
| 4 | |
| 5 | use App\Http\Controllers\Controller; |
| 6 | use App\Models\Widget; |
| 7 | use App\Service\CacheService; |
| 8 | use App\Services\Builder\WidgetService; |
| 9 | use Illuminate\Http\JsonResponse; |
| 10 | use Illuminate\Http\RedirectResponse; |
| 11 | use Illuminate\Http\Request; |
| 12 | use Illuminate\Validation\Rule; |
| 13 | use 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 | */ |
| 20 | class 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 | } |