Browse Source

project thumbnail

Andrew 1 day ago
parent
commit
d340925e94

+ 8
- 0
.gitignore View File

@@ -22,3 +22,11 @@
22 22
 Homestead.json
23 23
 Homestead.yaml
24 24
 Thumbs.db
25
+.mcp.json
26
+CLAUDE.md
27
+AGENTS.md
28
+junie/
29
+boost.json
30
+/.claude
31
+/.cursor
32
+/.junie

+ 124
- 123
app/Filament/Resources/ProjectResource.php View File

@@ -2,13 +2,11 @@
2 2
 
3 3
 namespace App\Filament\Resources;
4 4
 
5
-use AbdulmajeedJamaan\FilamentTranslatableTabs\TranslatableTabs;
6 5
 use App\Filament\Resources\ProjectResource\Pages;
7
-use App\Filament\Resources\ProjectResource\RelationManagers;
8 6
 use App\Models\Badge;
9 7
 use App\Models\Project;
10 8
 use App\Models\Region;
11
-use Filament\Forms;
9
+use App\Service\DeepLService;
12 10
 use Filament\Forms\Components\DatePicker;
13 11
 use Filament\Forms\Components\FileUpload;
14 12
 use Filament\Forms\Components\Group;
@@ -30,72 +28,75 @@ use Filament\Tables\Columns\TextColumn;
30 28
 use Filament\Tables\Filters\SelectFilter;
31 29
 use Filament\Tables\Table;
32 30
 use Illuminate\Database\Eloquent\Builder;
33
-use Illuminate\Database\Eloquent\SoftDeletingScope;
34 31
 use Illuminate\Support\Facades\Storage;
35 32
 use Illuminate\Support\Str;
36 33
 use SolutionForest\FilamentTranslateField\Forms\Component\Translate;
37
-use App\Service\DeepLService;
38 34
 
39 35
 class ProjectResource extends Resource
40 36
 {
41 37
     protected static ?string $model = Project::class;
42 38
 
43 39
     protected static ?string $navigationIcon = 'heroicon-o-rectangle-stack';
40
+
44 41
     protected static ?string $navigationGroup = '專案項目管理';
45
-    protected static ?string $navigationLabel = "專案項目管理";
46
-    protected static ?string $modelLabel = "專案項目管理";
42
+
43
+    protected static ?string $navigationLabel = '專案項目管理';
44
+
45
+    protected static ?string $modelLabel = '專案項目管理';
46
+
47 47
     protected static ?int $navigationSort = 5;
48 48
 
49 49
     public static function form(Form $form): Form
50 50
     {
51 51
         return $form
52 52
             ->schema([
53
-                Section::make("")->schema([
53
+                Section::make('')->schema([
54 54
                     Translate::make()->schema(fn (string $locale) => [
55
-                        TextInput::make("name")->label("項目名稱"),
56
-                        TextInput::make("sub_name")->label("項目子名稱"),
55
+                        TextInput::make('name')->label('項目名稱'),
56
+                        TextInput::make('sub_name')->label('項目子名稱'),
57 57
                     ])
58
-                    ->locales(["zh_TW", "en"])
59
-                    ->actions([
60
-                        app(DeepLService::class)->createTranslationAction("Main", ["name", "sub_name"])
61
-                    ])->columnSpanFull()->id("main"),
58
+                        ->locales(['zh_TW', 'en'])
59
+                        ->actions([
60
+                            app(DeepLService::class)->createTranslationAction('Main', ['name', 'sub_name']),
61
+                        ])->columnSpanFull()->id('main'),
62 62
                     Select::make('tags')
63 63
                         ->multiple()
64 64
                         ->relationship('tags', 'name')
65 65
                         ->preload()
66 66
                         ->label('標籤'),
67
-                    FileUpload::make("img_url")->label("圖片")->directory("project")->multiple()->maxFiles(5),
68
-                    TextInput::make('order')->label("排序")->default("0")
67
+                    FileUpload::make('thumbnail')->label('縮圖')->directory('project')->image(),
68
+                    FileUpload::make('img_url')->label('圖片')->directory('project')->multiple()->maxFiles(5),
69
+                    TextInput::make('order')->label('排序')->default('0'),
69 70
                 ]),
70 71
                 Tabs::make()->schema([
71
-                    Tab::make("專案概要")->schema([
72
-                        Select::make("region_id")->label("地區")->options(function (){
73
-                            return Region::where("visible",true)->pluck("name", "id");
72
+                    Tab::make('專案概要')->schema([
73
+                        Select::make('region_id')->label('地區')->options(function () {
74
+                            return Region::where('visible', true)->pluck('name', 'id');
74 75
                         }),
75 76
                         Translate::make()->schema(fn (string $locale) => [
76
-                            Textarea::make("summaries")->label("簡述"),
77
-                            TextInput::make("district")->label("區域"),
78
-                            TextInput::make("address")->label("地址"),
79
-                            Textarea::make("floor_plan")->label("樓層規劃"),
80
-                            Textarea::make("building_structure")->label("建築結構"),
81
-                            Textarea::make("design_unit")->label("設計單位")
77
+                            Textarea::make('summaries')->label('簡述'),
78
+                            TextInput::make('district')->label('區域'),
79
+                            TextInput::make('address')->label('地址'),
80
+                            Textarea::make('floor_plan')->label('樓層規劃'),
81
+                            Textarea::make('building_structure')->label('建築結構'),
82
+                            Textarea::make('design_unit')->label('設計單位'),
82 83
                         ])
83
-                        ->locales(["zh_TW", "en"])
84
-                        ->actions([
85
-                            app(DeepLService::class)->createTranslationAction("summaries", ["summaries","address",
86
-                            "floor_plan","building_structure","design_unit"])
87
-                        ])->columnSpanFull()->id("summaries"),
88
-                        Radio::make("badge_type")->label("")->options([1 => "永續目標", 2 => "取得標章"])->default(1)->inline(),
89
-                        Repeater::make("badgesTarget")->label("永續目標")->schema([
90
-                            Hidden::make("award_type")->default(1),
84
+                            ->locales(['zh_TW', 'en'])
85
+                            ->actions([
86
+                                app(DeepLService::class)->createTranslationAction('summaries', ['summaries', 'address',
87
+                                    'floor_plan', 'building_structure', 'design_unit']),
88
+                            ])->columnSpanFull()->id('summaries'),
89
+                        Radio::make('badge_type')->label('')->options([1 => '永續目標', 2 => '取得標章'])->default(1)->inline(),
90
+                        Repeater::make('badgesTarget')->label('永續目標')->schema([
91
+                            Hidden::make('award_type')->default(1),
91 92
                             Select::make('badge_id')
92 93
                                 ->options(function () {
93 94
                                     return Badge::all()->mapWithKeys(function ($badge) {
94 95
                                         return [
95 96
                                             $badge->id => '<div class="flex items-center gap-2">
96
-                                                <img src="' . Storage::url($badge->img_url) . '" class="w-6 h-6 rounded-full">
97
-                                                <span>' . $badge->title . '</span>
98
-                                            </div>'
97
+                                                <img src="'.Storage::url($badge->img_url).'" class="w-6 h-6 rounded-full">
98
+                                                <span>'.$badge->title.'</span>
99
+                                            </div>',
99 100
                                         ];
100 101
                                     });
101 102
                                 })
@@ -105,16 +106,16 @@ class ProjectResource extends Resource
105 106
                                 ->label('')
106 107
                                 ->required(),
107 108
                         ])->reorderable(false),
108
-                        Repeater::make("badgesAward")->label("取得標章")->schema([
109
-                            Hidden::make("award_type")->default(2),
109
+                        Repeater::make('badgesAward')->label('取得標章')->schema([
110
+                            Hidden::make('award_type')->default(2),
110 111
                             Select::make('badge_id')
111 112
                                 ->options(function () {
112 113
                                     return Badge::all()->mapWithKeys(function ($badge) {
113 114
                                         return [
114 115
                                             $badge->id => '<div class="flex items-center gap-2">
115
-                                                <img src="' . Storage::url($badge->img_url) . '" class="w-6 h-6 rounded-full">
116
-                                                <span>' . $badge->title . '</span>
117
-                                            </div>'
116
+                                                <img src="'.Storage::url($badge->img_url).'" class="w-6 h-6 rounded-full">
117
+                                                <span>'.$badge->title.'</span>
118
+                                            </div>',
118 119
                                         ];
119 120
                                     });
120 121
                                 })
@@ -128,67 +129,67 @@ class ProjectResource extends Resource
128 129
                                 ->format('Y-m')
129 130
                                 ->displayFormat('Y年m月')
130 131
                                 ->native(false)
131
-                                ->closeOnDateSelection()
132
+                                ->closeOnDateSelection(),
132 133
                         ])->reorderable(false),
133 134
                     ]),
134
-                    Tab::make("開發歷程")->schema([
135
-                        Repeater::make("histories")->label("")->schema([
135
+                    Tab::make('開發歷程')->schema([
136
+                        Repeater::make('histories')->label('')->schema([
136 137
                             TextInput::make('histories_item_key')
137
-                            ->default(fn () => Str::random())
138
-                            ->hidden()
139
-                            ->afterStateHydrated(function (TextInput $component, $state) {
140
-                                if (empty($state)) {
141
-                                    $component->state(Str::random());
142
-                                }
143
-                            }),
144
-                            DatePicker::make("operate_date")->label("歷程日期")->columnSpan(1),
138
+                                ->default(fn () => Str::random())
139
+                                ->hidden()
140
+                                ->afterStateHydrated(function (TextInput $component, $state) {
141
+                                    if (empty($state)) {
142
+                                        $component->state(Str::random());
143
+                                    }
144
+                                }),
145
+                            DatePicker::make('operate_date')->label('歷程日期')->columnSpan(1),
145 146
                             Translate::make()->schema(fn (string $locale) => [
146
-                                TextInput::make("title")->label("標題")->columnSpanFull()
147
+                                TextInput::make('title')->label('標題')->columnSpanFull(),
147 148
                             ])
148
-                            ->locales(["zh_TW", "en"])
149
-                            ->actions([
150
-                                app(DeepLService::class)->createTranslationAction("histories", ["title"])
151
-                            ])->columnSpanFull()
152
-                            ->id(fn ($get) => "histories_" . $get('histories_item_key')),
149
+                                ->locales(['zh_TW', 'en'])
150
+                                ->actions([
151
+                                    app(DeepLService::class)->createTranslationAction('histories', ['title']),
152
+                                ])->columnSpanFull()
153
+                                ->id(fn ($get) => 'histories_'.$get('histories_item_key')),
153 154
                         ])
154
-                        ->relationship("histories", function ($query,Get $get, $livewire) {
155
-                            return $query->orderby("operate_date","desc");
156
-                        })
157
-                        ->collapsible()
158
-                        ->cloneable()
155
+                            ->relationship('histories', function ($query, Get $get, $livewire) {
156
+                                return $query->orderby('operate_date', 'desc');
157
+                            })
158
+                            ->collapsible()
159
+                            ->cloneable(),
159 160
                     ]),
160
-                    Tab::make("空間資訊")->schema([
161
+                    Tab::make('空間資訊')->schema([
161 162
                         Group::make()->schema([
162 163
                             Translate::make()->schema(fn (string $locale) => [
163
-                                TextInput::make("contact_unit")->label("物管單位"),
164
-                                TextInput::make("contact_phone")->label("物管電話"),
165
-                                TextInput::make("inversment_phone")->label("招商電話"),
164
+                                TextInput::make('contact_unit')->label('物管單位'),
165
+                                TextInput::make('contact_phone')->label('物管電話'),
166
+                                TextInput::make('inversment_phone')->label('招商電話'),
166 167
                             ])
167
-                            ->locales(["zh_TW", "en"])
168
-                            ->actions([
169
-                                app(DeepLService::class)->createTranslationAction("contact", ["contact_unit", "contact_phone", "inversment_phone"])
170
-                            ])->columnSpanFull()->columns(3)
171
-                            ->id("contact"),
172
-                            TextInput::make("offical_link")->label("官網連結"),
168
+                                ->locales(['zh_TW', 'en'])
169
+                                ->actions([
170
+                                    app(DeepLService::class)->createTranslationAction('contact', ['contact_unit', 'contact_phone', 'inversment_phone']),
171
+                                ])->columnSpanFull()->columns(3)
172
+                                ->id('contact'),
173
+                            TextInput::make('offical_link')->label('官網連結'),
173 174
                         ])->columnSpanFull(),
174
-                        Repeater::make("spaceInfos")->label("")->schema([
175
+                        Repeater::make('spaceInfos')->label('')->schema([
175 176
                             Translate::make()->schema(fn (string $locale) => [
176
-                                TextInput::make("title")->label("標題")->columnSpanFull(),
177
-                                Textarea::make("content")->label("內文")->columnSpanFull()
177
+                                TextInput::make('title')->label('標題')->columnSpanFull(),
178
+                                Textarea::make('content')->label('內文')->columnSpanFull(),
178 179
                             ])
179
-                            ->locales(["zh_TW", "en"])
180
-                            ->actions([
181
-                                app(DeepLService::class)->createTranslationAction("spaceInfos", ["title", "content"])
182
-                            ])->columnSpanFull()
183
-                            ->id(fn ($get) => "spaceInfos_" . $get('item_key')),
180
+                                ->locales(['zh_TW', 'en'])
181
+                                ->actions([
182
+                                    app(DeepLService::class)->createTranslationAction('spaceInfos', ['title', 'content']),
183
+                                ])->columnSpanFull()
184
+                                ->id(fn ($get) => 'spaceInfos_'.$get('item_key')),
184 185
                         ])
185
-                        ->relationship("spaceInfos")
186
-                        ->label("")
187
-                        ->collapsible()
188
-                        ->reorderableWithButtons()
189
-                        ->orderColumn('order')
190
-                        ->cloneable(),
191
-                    ])
186
+                            ->relationship('spaceInfos')
187
+                            ->label('')
188
+                            ->collapsible()
189
+                            ->reorderableWithButtons()
190
+                            ->orderColumn('order')
191
+                            ->cloneable(),
192
+                    ]),
192 193
                 ])->columnSpanFull(),
193 194
             ]);
194 195
     }
@@ -198,47 +199,47 @@ class ProjectResource extends Resource
198 199
         return $table
199 200
             ->columns([
200 201
                 //
201
-                TextColumn::make("name")->label("項目名稱")->alignCenter(),
202
-                ImageColumn::make("first_list_img_url")->label("列表圖")->alignCenter(),
203
-                TextColumn::make("region.name")->label("地址")->alignCenter()
204
-                ->formatStateUsing(fn ($record) => $record->region->getTranslation("name", "zh_TW") . ' | ' . $record->getTranslation("address", "zh_TW")),
202
+                TextColumn::make('name')->label('項目名稱')->alignCenter(),
203
+                ImageColumn::make('thumbnail_url')->label('縮圖')->alignCenter(),
204
+                ImageColumn::make('first_list_img_url')->label('列表圖')->alignCenter(),
205
+                TextColumn::make('region.name')->label('地址')->alignCenter()
206
+                    ->formatStateUsing(fn ($record) => $record->region->getTranslation('name', 'zh_TW').' | '.$record->getTranslation('address', 'zh_TW')),
205 207
             ])
206 208
             ->filters([
207
-                SelectFilter::make('visible')->label("上/下架")
208
-                ->options([
209
-                    0 => "下架",
210
-                    1 => "上架",
211
-                ])
212
-                ->query(
213
-                    fn (array $data, Builder $query): Builder =>
214
-                    $query->when(
215
-                        $data['value'],
216
-                        fn (Builder $query, $value): Builder => $query->where('visible', $data['value'])
217
-                    )
218
-                ),
209
+                SelectFilter::make('visible')->label('上/下架')
210
+                    ->options([
211
+                        0 => '下架',
212
+                        1 => '上架',
213
+                    ])
214
+                    ->query(
215
+                        fn (array $data, Builder $query): Builder => $query->when(
216
+                            $data['value'],
217
+                            fn (Builder $query, $value): Builder => $query->where('visible', $data['value'])
218
+                        )
219
+                    ),
219 220
             ])
220 221
             ->actions([
221 222
                 Tables\Actions\EditAction::make(),
222 223
                 Tables\Actions\DeleteAction::make(),
223
-                \Filament\Tables\Actions\Action::make("audit")
224
-                ->label(fn ($record) => match ($record->visible) {
225
-                    0 => '上架',
226
-                    1 => '下架',
227
-                })
228
-                ->color(fn ($record) => match ($record->visible) {
229
-                    0 => 'warning',
230
-                    1 => 'gray',
231
-                })
232
-                ->icon(fn ($record) => match ($record->visible) {
233
-                    0 => 'heroicon-m-chevron-double-up',
234
-                    1 => 'heroicon-m-chevron-double-down',
235
-                })
236
-                ->action(function ($record): void {
237
-                    $record->visible = !$record->visible;
238
-                    $record->save();
239
-                })
240
-                ->outlined()
241
-                ->requiresConfirmation(),
224
+                \Filament\Tables\Actions\Action::make('audit')
225
+                    ->label(fn ($record) => match ($record->visible) {
226
+                        0 => '上架',
227
+                        1 => '下架',
228
+                    })
229
+                    ->color(fn ($record) => match ($record->visible) {
230
+                        0 => 'warning',
231
+                        1 => 'gray',
232
+                    })
233
+                    ->icon(fn ($record) => match ($record->visible) {
234
+                        0 => 'heroicon-m-chevron-double-up',
235
+                        1 => 'heroicon-m-chevron-double-down',
236
+                    })
237
+                    ->action(function ($record): void {
238
+                        $record->visible = ! $record->visible;
239
+                        $record->save();
240
+                    })
241
+                    ->outlined()
242
+                    ->requiresConfirmation(),
242 243
             ])
243 244
             ->bulkActions([
244 245
                 Tables\Actions\BulkActionGroup::make([

+ 79
- 80
app/Http/Controllers/Api/ProjectController.php View File

@@ -3,17 +3,12 @@
3 3
 namespace App\Http\Controllers\Api;
4 4
 
5 5
 use App\Http\Controllers\Controller;
6
-use App\Models\News;
7
-use App\Models\NewsCategory;
8 6
 use App\Models\Project;
9 7
 use App\Models\Region;
10 8
 use App\Models\Tag;
11 9
 use App\Supports\Response;
12 10
 use Carbon\Carbon;
13 11
 use Illuminate\Http\Request;
14
-use Illuminate\Support\Facades\DB;
15
-use Illuminate\Support\Facades\Log;
16
-use App\Http\Helper\Helper;
17 12
 
18 13
 /**
19 14
  * @group Lottery Prize
@@ -21,137 +16,141 @@ use App\Http\Helper\Helper;
21 16
 class ProjectController extends Controller
22 17
 {
23 18
     public function __construct(
24
-    )
25
-    {
26
-    }
19
+    ) {}
27 20
 
28 21
     public function list(Request $request, $locale = 'tw')
29 22
     {
30
-        $locale = $locale == "tw" ? "zh_TW" : $locale;
23
+        $locale = $locale == 'tw' ? 'zh_TW' : $locale;
31 24
         $result = [];
32 25
 
33
-        //年份清單
34
-        $regions = Region::select(['id', 'name'])->where("visible", 1)->get()->map(function ($record) use ($locale){
26
+        // 年份清單
27
+        $regions = Region::select(['id', 'name'])->where('visible', 1)->get()->map(function ($record) use ($locale) {
35 28
             return [
36
-                "id" => $record->id,
37
-                "name" => $record->getTranslation("name", $locale)
29
+                'id' => $record->id,
30
+                'name' => $record->getTranslation('name', $locale),
38 31
             ];
39 32
         });
40
-        $tags = Tag::select(['id', 'name'])->where("visible", 1)->get()->map(function ($record) use ($locale){
33
+        $tags = Tag::select(['id', 'name'])->where('visible', 1)->get()->map(function ($record) use ($locale) {
41 34
             return [
42
-                "id" => $record->id,
43
-                "name" => $record->getTranslation("name", $locale)
35
+                'id' => $record->id,
36
+                'name' => $record->getTranslation('name', $locale),
44 37
             ];
45 38
         });
46 39
 
47
-        $result["regions"] = $regions;
48
-        $result["tags"] = $tags;
40
+        $result['regions'] = $regions;
41
+        $result['tags'] = $tags;
49 42
 
50
-        //文章列表
51
-        $projects = Project::where("visible", true);
52
-        if($request->has('region')){
53
-            $projects->where("region_id", $request->input('region'));
43
+        // 文章列表
44
+        $projects = Project::where('visible', true);
45
+        if ($request->has('region')) {
46
+            $projects->where('region_id', $request->input('region'));
54 47
         }
55
-        if($request->has('tags')){
48
+        if ($request->has('tags')) {
56 49
             $request_tags = $request->input('tags');
57
-            $projects->whereHas("tags", function ($query) use ($request_tags) {
58
-                return $query->whereIn("tags.id", $request_tags);
50
+            $projects->whereHas('tags', function ($query) use ($request_tags) {
51
+                return $query->whereIn('tags.id', $request_tags);
59 52
             });
60 53
         }
61 54
 
62
-        $projects = $projects->with(['tags', 'region'])->orderByDesc("order")->get();
63
-        foreach($projects as $project){
64
-            $result["list"][] = [
65
-                "id" => $project->id,
66
-                "region_id" => $project->region_id,
67
-                "region" => $project->region->getTranslation("name", $locale),
68
-                "district" => $project->getTranslation("district", $locale),
69
-                "address" => $project->getTranslation("address", $locale),
70
-                "tags" => $project->tags->map(function ($record) use ($locale){
55
+        $projects = $projects->with(['tags', 'region'])->orderByDesc('order')->get();
56
+        foreach ($projects as $project) {
57
+            $result['list'][] = [
58
+                'id' => $project->id,
59
+                'region_id' => $project->region_id,
60
+                'region' => $project->region->getTranslation('name', $locale),
61
+                'district' => $project->getTranslation('district', $locale),
62
+                'address' => $project->getTranslation('address', $locale),
63
+                'tags' => $project->tags->map(function ($record) use ($locale) {
71 64
                     return [
72
-                        "id" => $record->id,
73
-                        "name" => $record->getTranslation("name", $locale)
65
+                        'id' => $record->id,
66
+                        'name' => $record->getTranslation('name', $locale),
74 67
                     ];
75 68
                 }),
76
-                "name" => $project->getTranslation("name", $locale),
77
-                "imgUrl" => $project->first_list_img_url
69
+                'name' => $project->getTranslation('name', $locale),
70
+                'thumbnail' => $project->thumbnail_url,
71
+                'imgUrl' => $project->first_list_img_url,
78 72
             ];
79 73
         }
74
+
80 75
         return Response::ok($result);
81 76
     }
82 77
 
83
-    public function detail($locale = 'tw', $id){
78
+    public function detail($locale, $id)
79
+    {
84 80
 
85
-        $locale = $locale == "tw" ? "zh_TW" : $locale;
81
+        $locale = $locale == 'tw' ? 'zh_TW' : $locale;
86 82
         $project = Project::find($id);
87 83
 
88 84
         $projectHistories = [];
89 85
         $projectSpaceInfo = [];
90
-        foreach($project->histories as $history){
86
+        foreach ($project->histories as $history) {
91 87
             $operateDate = Carbon::parse($history->operate_date);
92
-            $projectHistories[$operateDate->format("Y")][] = [
93
-                "operateDate" => $operateDate->format("m/d"),
94
-                "title" => $history->getTranslation("title", $locale)
88
+            $projectHistories[$operateDate->format('Y')][] = [
89
+                'operateDate' => $operateDate->format('m/d'),
90
+                'title' => $history->getTranslation('title', $locale),
95 91
             ];
96 92
         }
97
-        foreach($project->spaceInfos as $spaceInfo){
93
+        foreach ($project->spaceInfos as $spaceInfo) {
98 94
             $projectSpaceInfo[] = [
99
-                "title" => $spaceInfo->getTranslation("title", $locale),
100
-                "content" => $spaceInfo->getTranslation("content", $locale)
95
+                'title' => $spaceInfo->getTranslation('title', $locale),
96
+                'content' => $spaceInfo->getTranslation('content', $locale),
101 97
             ];
102 98
         }
103 99
 
104 100
         $result = [
105
-            "name" => $project->getTranslation("name", $locale),
106
-            "subName" => $project->getTranslation("sub_name", $locale),
107
-            "description" => $project->getTranslation("summaries", $locale),
108
-            "images" => $project->img_list,
109
-            "region_id" => $project->region_id,
110
-            "region" => $project->region->getTranslation("name", $locale),
111
-            "address" => $project->getTranslation("address", $locale),
112
-            "district" => $project->getTranslation("district", $locale),
113
-            "tags" => $project->tags->map(function ($record) use ($locale){
101
+            'name' => $project->getTranslation('name', $locale),
102
+            'subName' => $project->getTranslation('sub_name', $locale),
103
+            'description' => $project->getTranslation('summaries', $locale),
104
+            'thumbnail' => $project->thumbnail_url,
105
+            'images' => $project->img_list,
106
+            'region_id' => $project->region_id,
107
+            'region' => $project->region->getTranslation('name', $locale),
108
+            'address' => $project->getTranslation('address', $locale),
109
+            'district' => $project->getTranslation('district', $locale),
110
+            'tags' => $project->tags->map(function ($record) use ($locale) {
114 111
                 return [
115
-                    "id" => $record->id,
116
-                    "name" => $record->getTranslation("name", $locale)
112
+                    'id' => $record->id,
113
+                    'name' => $record->getTranslation('name', $locale),
117 114
                 ];
118 115
             }),
119
-            "floor_plan" => $project->getTranslation("floor_plan", $locale),
120
-            "building_structure" => $project->getTranslation("building_structure", $locale),
121
-            "design_unit" => $project->getTranslation("design_unit", $locale),
122
-            "badge_type" => $project->badge_type,
123
-            "badges" => $project->getBadgesByType($locale, $project->badge_type),
124
-            "contact_info" => [
125
-                "unitName" => $project->getTranslation("contact_unit", $locale),
126
-                "unitPhone" => $project->getTranslation("contact_phone", $locale),
127
-                "inversmentPhone" => $project->getTranslation("inversment_phone", $locale),
128
-                "officalLink" => $project->offical_link,
116
+            'floor_plan' => $project->getTranslation('floor_plan', $locale),
117
+            'building_structure' => $project->getTranslation('building_structure', $locale),
118
+            'design_unit' => $project->getTranslation('design_unit', $locale),
119
+            'badge_type' => $project->badge_type,
120
+            'badges' => $project->getBadgesByType($locale, $project->badge_type),
121
+            'contact_info' => [
122
+                'unitName' => $project->getTranslation('contact_unit', $locale),
123
+                'unitPhone' => $project->getTranslation('contact_phone', $locale),
124
+                'inversmentPhone' => $project->getTranslation('inversment_phone', $locale),
125
+                'officalLink' => $project->offical_link,
129 126
             ],
130
-            "spaceInfo" => $projectSpaceInfo,
131
-            "historyList" => $projectHistories
127
+            'spaceInfo' => $projectSpaceInfo,
128
+            'historyList' => $projectHistories,
132 129
         ];
130
+
133 131
         return Response::ok($result);
134 132
     }
135 133
 
136
-    public function badges($locale = 'tw') {
137
-        $locale = $locale == "tw" ? "zh_TW" : $locale;
134
+    public function badges($locale = 'tw')
135
+    {
136
+        $locale = $locale == 'tw' ? 'zh_TW' : $locale;
138 137
 
139
-        $projects = Project::where("badge_type", 2)->get();
138
+        $projects = Project::where('badge_type', 2)->get();
140 139
         $result = [];
141 140
 
142
-        foreach($projects as $project){
141
+        foreach ($projects as $project) {
143 142
             $badgesData = $project->awardBadges;
144 143
             $badges = [];
145
-            foreach($badgesData as $badge){
144
+            foreach ($badgesData as $badge) {
146 145
                 $badges[] = [
147
-                    "imgUrl" => $badge->imgUrlLink,
148
-                    "name" => $badge->getTranslation("title", $locale),
149
-                    "rewardYear" => "取得時間 : " . date("Y.m", strtotime($badge->pivot->award_date))
146
+                    'imgUrl' => $badge->imgUrlLink,
147
+                    'name' => $badge->getTranslation('title', $locale),
148
+                    'rewardYear' => '取得時間 : '.date('Y.m', strtotime($badge->pivot->award_date)),
150 149
                 ];
151 150
             }
152 151
             $result[] = [
153
-                "name" => $project->getTranslation("name", $locale),
154
-                "badges" => $badges
152
+                'name' => $project->getTranslation('name', $locale),
153
+                'badges' => $badges,
155 154
             ];
156 155
         }
157 156
 

+ 43
- 32
app/Models/Project.php View File

@@ -11,14 +11,16 @@ use Spatie\Translatable\HasTranslations;
11 11
 class Project extends Model
12 12
 {
13 13
     use HasTranslations, SoftDeletes;
14
+
14 15
     protected $guarded = ['id'];
15 16
 
16
-    protected $translatable = ["name", "sub_name", "summaries", "img_alt", "address", "floor_plan",
17
-        "building_structure", "design_unit", "contact_unit", "contact_phone", "inversment_phone", "district"
17
+    protected $translatable = ['name', 'sub_name', 'summaries', 'img_alt', 'address', 'floor_plan',
18
+        'building_structure', 'design_unit', 'contact_unit', 'contact_phone', 'inversment_phone', 'district',
18 19
     ];
19
-    protected $casts = ["img_url" => "array"];
20 20
 
21
-    protected $appends = ["first_list_img_url", "img_list"];
21
+    protected $casts = ['img_url' => 'array'];
22
+
23
+    protected $appends = ['first_list_img_url', 'img_list', 'thumbnail_url'];
22 24
 
23 25
     public function region()
24 26
     {
@@ -33,8 +35,8 @@ class Project extends Model
33 35
     public function badges()
34 36
     {
35 37
         return $this->morphToMany(Badge::class, 'badgeable')
36
-        ->withPivot('award_date', 'award_type')  // ✅ 關鍵
37
-        ->withTimestamps();
38
+            ->withPivot('award_date', 'award_type')  // ✅ 關鍵
39
+            ->withTimestamps();
38 40
     }
39 41
 
40 42
     /**
@@ -72,32 +74,40 @@ class Project extends Model
72 74
     public function firstListImgUrl(): Attribute
73 75
     {
74 76
         return Attribute::make(
75
-            get: fn ($value) => !empty($this->img_url) ? Storage::url($this->img_url[0]) : null,
77
+            get: fn ($value) => ! empty($this->img_url) ? Storage::url($this->img_url[0]) : null,
76 78
         );
77 79
     }
78 80
 
79 81
     public function imgList(): Attribute
80 82
     {
81 83
         $imgList = [];
82
-        if(!is_null($this->img_url) && count($this->img_url) > 0){
83
-            foreach($this->img_url as $img){
84
+        if (! is_null($this->img_url) && count($this->img_url) > 0) {
85
+            foreach ($this->img_url as $img) {
84 86
                 $imgList[] = Storage::url($img);
85 87
             }
86 88
         }
89
+
87 90
         return Attribute::make(
88 91
             get: fn ($value) => $imgList,
89 92
         );
90 93
     }
91 94
 
92
-    public function getBadges($locale) : array
95
+    public function thumbnailUrl(): Attribute
96
+    {
97
+        return Attribute::make(
98
+            get: fn ($value) => ! empty($this->thumbnail) ? Storage::url($this->thumbnail) : null,
99
+        );
100
+    }
101
+
102
+    public function getBadges($locale): array
93 103
     {
94 104
         $badges = [];
95
-        if($this->badges->count() > 0){
96
-            foreach($this->badges as $badge){
105
+        if ($this->badges->count() > 0) {
106
+            foreach ($this->badges as $badge) {
97 107
                 $badges[] = [
98
-                    "imgUrl" => $badge->imgUrlLink,
99
-                    "name" => $badge->getTranslation("title", $locale),
100
-                    "awardDate" => "取得時間 : " . date("Y.m", strtotime($badge->pivot->award_date))
108
+                    'imgUrl' => $badge->imgUrlLink,
109
+                    'name' => $badge->getTranslation('title', $locale),
110
+                    'awardDate' => '取得時間 : '.date('Y.m', strtotime($badge->pivot->award_date)),
101 111
                 ];
102 112
             }
103 113
         }
@@ -105,42 +115,43 @@ class Project extends Model
105 115
         return $badges;
106 116
     }
107 117
 
108
-    public function getBadgesByType($locale, $badgeType) : array
118
+    public function getBadgesByType($locale, $badgeType): array
109 119
     {
110 120
         $badges = [];
111
-        switch($badgeType){
112
-            case "1":
121
+        switch ($badgeType) {
122
+            case '1':
113 123
                 $badgesData = $this->targetBadges;
114
-                foreach($badgesData as $badge){
124
+                foreach ($badgesData as $badge) {
115 125
                     $badges[] = [
116
-                        "imgUrl" => $badge->imgUrlLink,
117
-                        "name" => $badge->getTranslation("title", $locale),
118
-                        "awardDate" => null
126
+                        'imgUrl' => $badge->imgUrlLink,
127
+                        'name' => $badge->getTranslation('title', $locale),
128
+                        'awardDate' => null,
119 129
                     ];
120 130
                 }
121 131
                 break;
122
-            case "2":
132
+            case '2':
123 133
                 $badgesData = $this->awardBadges;
124
-                foreach($badgesData as $badge){
134
+                foreach ($badgesData as $badge) {
125 135
                     $badges[] = [
126
-                        "imgUrl" => $badge->imgUrlLink,
127
-                        "name" => $badge->getTranslation("title", $locale),
128
-                        "awardDate" => "取得時間 : " . date("Y.m", strtotime($badge->pivot->award_date))
136
+                        'imgUrl' => $badge->imgUrlLink,
137
+                        'name' => $badge->getTranslation('title', $locale),
138
+                        'awardDate' => '取得時間 : '.date('Y.m', strtotime($badge->pivot->award_date)),
129 139
                     ];
130 140
                 }
131 141
                 break;
132 142
         }
143
+
133 144
         return $badges;
134 145
     }
135 146
 
136
-    public function getTags($locale) : array
147
+    public function getTags($locale): array
137 148
     {
138 149
         $tags = [];
139
-        if($this->tags->count() > 0){
140
-            foreach($this->tags as $tag){
150
+        if ($this->tags->count() > 0) {
151
+            foreach ($this->tags as $tag) {
141 152
                 $tags[] = [
142
-                    "id" => $tag->id,
143
-                    "name" => $tag->getTranslation("name", $locale),
153
+                    'id' => $tag->id,
154
+                    'name' => $tag->getTranslation('name', $locale),
144 155
                 ];
145 156
             }
146 157
         }

+ 1
- 0
composer.json View File

@@ -21,6 +21,7 @@
21 21
     },
22 22
     "require-dev": {
23 23
         "fakerphp/faker": "^1.23",
24
+        "laravel/boost": "^1.1",
24 25
         "laravel/pail": "^1.2.2",
25 26
         "laravel/pint": "^1.24",
26 27
         "laravel/sail": "^1.41",

+ 192
- 2
composer.lock View File

@@ -4,7 +4,7 @@
4 4
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
5 5
         "This file is @generated automatically"
6 6
     ],
7
-    "content-hash": "e904da9638c3c3831ee33c8e624ce412",
7
+    "content-hash": "44b40d372827055e89cb5b1d76a38986",
8 8
     "packages": [
9 9
         {
10 10
             "name": "abdulmajeed-jamaan/filament-translatable-tabs",
@@ -8759,6 +8759,135 @@
8759 8759
             "time": "2025-04-30T06:54:44+00:00"
8760 8760
         },
8761 8761
         {
8762
+            "name": "laravel/boost",
8763
+            "version": "v1.1.5",
8764
+            "source": {
8765
+                "type": "git",
8766
+                "url": "https://github.com/laravel/boost.git",
8767
+                "reference": "4bd1692c064b2135eb2f8f7bd8bcb710e5e75f86"
8768
+            },
8769
+            "dist": {
8770
+                "type": "zip",
8771
+                "url": "https://api.github.com/repos/laravel/boost/zipball/4bd1692c064b2135eb2f8f7bd8bcb710e5e75f86",
8772
+                "reference": "4bd1692c064b2135eb2f8f7bd8bcb710e5e75f86",
8773
+                "shasum": ""
8774
+            },
8775
+            "require": {
8776
+                "guzzlehttp/guzzle": "^7.9",
8777
+                "illuminate/console": "^10.0|^11.0|^12.0",
8778
+                "illuminate/contracts": "^10.0|^11.0|^12.0",
8779
+                "illuminate/routing": "^10.0|^11.0|^12.0",
8780
+                "illuminate/support": "^10.0|^11.0|^12.0",
8781
+                "laravel/mcp": "^0.1.1",
8782
+                "laravel/prompts": "^0.1.9|^0.3",
8783
+                "laravel/roster": "^0.2.5",
8784
+                "php": "^8.1"
8785
+            },
8786
+            "require-dev": {
8787
+                "laravel/pint": "^1.14",
8788
+                "mockery/mockery": "^1.6",
8789
+                "orchestra/testbench": "^8.22.0|^9.0|^10.0",
8790
+                "pestphp/pest": "^2.0|^3.0",
8791
+                "phpstan/phpstan": "^2.0"
8792
+            },
8793
+            "type": "library",
8794
+            "extra": {
8795
+                "laravel": {
8796
+                    "providers": [
8797
+                        "Laravel\\Boost\\BoostServiceProvider"
8798
+                    ]
8799
+                },
8800
+                "branch-alias": {
8801
+                    "dev-master": "1.x-dev"
8802
+                }
8803
+            },
8804
+            "autoload": {
8805
+                "psr-4": {
8806
+                    "Laravel\\Boost\\": "src/"
8807
+                }
8808
+            },
8809
+            "notification-url": "https://packagist.org/downloads/",
8810
+            "license": [
8811
+                "MIT"
8812
+            ],
8813
+            "description": "Laravel Boost accelerates AI-assisted development to generate high-quality, Laravel-specific code.",
8814
+            "homepage": "https://github.com/laravel/boost",
8815
+            "keywords": [
8816
+                "ai",
8817
+                "dev",
8818
+                "laravel"
8819
+            ],
8820
+            "support": {
8821
+                "issues": "https://github.com/laravel/boost/issues",
8822
+                "source": "https://github.com/laravel/boost"
8823
+            },
8824
+            "time": "2025-09-18T07:33:27+00:00"
8825
+        },
8826
+        {
8827
+            "name": "laravel/mcp",
8828
+            "version": "v0.1.1",
8829
+            "source": {
8830
+                "type": "git",
8831
+                "url": "https://github.com/laravel/mcp.git",
8832
+                "reference": "6d6284a491f07c74d34f48dfd999ed52c567c713"
8833
+            },
8834
+            "dist": {
8835
+                "type": "zip",
8836
+                "url": "https://api.github.com/repos/laravel/mcp/zipball/6d6284a491f07c74d34f48dfd999ed52c567c713",
8837
+                "reference": "6d6284a491f07c74d34f48dfd999ed52c567c713",
8838
+                "shasum": ""
8839
+            },
8840
+            "require": {
8841
+                "illuminate/console": "^10.0|^11.0|^12.0",
8842
+                "illuminate/contracts": "^10.0|^11.0|^12.0",
8843
+                "illuminate/http": "^10.0|^11.0|^12.0",
8844
+                "illuminate/routing": "^10.0|^11.0|^12.0",
8845
+                "illuminate/support": "^10.0|^11.0|^12.0",
8846
+                "illuminate/validation": "^10.0|^11.0|^12.0",
8847
+                "php": "^8.1|^8.2"
8848
+            },
8849
+            "require-dev": {
8850
+                "laravel/pint": "^1.14",
8851
+                "orchestra/testbench": "^8.22.0|^9.0|^10.0",
8852
+                "phpstan/phpstan": "^2.0"
8853
+            },
8854
+            "type": "library",
8855
+            "extra": {
8856
+                "laravel": {
8857
+                    "aliases": {
8858
+                        "Mcp": "Laravel\\Mcp\\Server\\Facades\\Mcp"
8859
+                    },
8860
+                    "providers": [
8861
+                        "Laravel\\Mcp\\Server\\McpServiceProvider"
8862
+                    ]
8863
+                }
8864
+            },
8865
+            "autoload": {
8866
+                "psr-4": {
8867
+                    "Laravel\\Mcp\\": "src/",
8868
+                    "Workbench\\App\\": "workbench/app/",
8869
+                    "Laravel\\Mcp\\Tests\\": "tests/",
8870
+                    "Laravel\\Mcp\\Server\\": "src/Server/"
8871
+                }
8872
+            },
8873
+            "notification-url": "https://packagist.org/downloads/",
8874
+            "license": [
8875
+                "MIT"
8876
+            ],
8877
+            "description": "The easiest way to add MCP servers to your Laravel app.",
8878
+            "homepage": "https://github.com/laravel/mcp",
8879
+            "keywords": [
8880
+                "dev",
8881
+                "laravel",
8882
+                "mcp"
8883
+            ],
8884
+            "support": {
8885
+                "issues": "https://github.com/laravel/mcp/issues",
8886
+                "source": "https://github.com/laravel/mcp"
8887
+            },
8888
+            "time": "2025-08-16T09:50:43+00:00"
8889
+        },
8890
+        {
8762 8891
             "name": "laravel/pail",
8763 8892
             "version": "v1.2.3",
8764 8893
             "source": {
@@ -8907,6 +9036,67 @@
8907 9036
             "time": "2025-07-10T18:09:32+00:00"
8908 9037
         },
8909 9038
         {
9039
+            "name": "laravel/roster",
9040
+            "version": "v0.2.9",
9041
+            "source": {
9042
+                "type": "git",
9043
+                "url": "https://github.com/laravel/roster.git",
9044
+                "reference": "82bbd0e2de614906811aebdf16b4305956816fa6"
9045
+            },
9046
+            "dist": {
9047
+                "type": "zip",
9048
+                "url": "https://api.github.com/repos/laravel/roster/zipball/82bbd0e2de614906811aebdf16b4305956816fa6",
9049
+                "reference": "82bbd0e2de614906811aebdf16b4305956816fa6",
9050
+                "shasum": ""
9051
+            },
9052
+            "require": {
9053
+                "illuminate/console": "^10.0|^11.0|^12.0",
9054
+                "illuminate/contracts": "^10.0|^11.0|^12.0",
9055
+                "illuminate/routing": "^10.0|^11.0|^12.0",
9056
+                "illuminate/support": "^10.0|^11.0|^12.0",
9057
+                "php": "^8.1|^8.2",
9058
+                "symfony/yaml": "^6.4|^7.2"
9059
+            },
9060
+            "require-dev": {
9061
+                "laravel/pint": "^1.14",
9062
+                "mockery/mockery": "^1.6",
9063
+                "orchestra/testbench": "^8.22.0|^9.0|^10.0",
9064
+                "pestphp/pest": "^2.0|^3.0",
9065
+                "phpstan/phpstan": "^2.0"
9066
+            },
9067
+            "type": "library",
9068
+            "extra": {
9069
+                "laravel": {
9070
+                    "providers": [
9071
+                        "Laravel\\Roster\\RosterServiceProvider"
9072
+                    ]
9073
+                },
9074
+                "branch-alias": {
9075
+                    "dev-master": "1.x-dev"
9076
+                }
9077
+            },
9078
+            "autoload": {
9079
+                "psr-4": {
9080
+                    "Laravel\\Roster\\": "src/"
9081
+                }
9082
+            },
9083
+            "notification-url": "https://packagist.org/downloads/",
9084
+            "license": [
9085
+                "MIT"
9086
+            ],
9087
+            "description": "Detect packages & approaches in use within a Laravel project",
9088
+            "homepage": "https://github.com/laravel/roster",
9089
+            "keywords": [
9090
+                "dev",
9091
+                "laravel"
9092
+            ],
9093
+            "support": {
9094
+                "issues": "https://github.com/laravel/roster/issues",
9095
+                "source": "https://github.com/laravel/roster"
9096
+            },
9097
+            "time": "2025-10-20T09:56:46+00:00"
9098
+        },
9099
+        {
8910 9100
             "name": "laravel/sail",
8911 9101
             "version": "v1.45.0",
8912 9102
             "source": {
@@ -10935,5 +11125,5 @@
10935 11125
         "php": "^8.2"
10936 11126
     },
10937 11127
     "platform-dev": [],
10938
-    "plugin-api-version": "2.6.0"
11128
+    "plugin-api-version": "2.3.0"
10939 11129
 }

+ 28
- 0
database/migrations/2025_11_18_082640_add_thumbnail_to_projects_table.php View File

@@ -0,0 +1,28 @@
1
+<?php
2
+
3
+use Illuminate\Database\Migrations\Migration;
4
+use Illuminate\Database\Schema\Blueprint;
5
+use Illuminate\Support\Facades\Schema;
6
+
7
+return new class extends Migration
8
+{
9
+    /**
10
+     * Run the migrations.
11
+     */
12
+    public function up(): void
13
+    {
14
+        Schema::table('projects', function (Blueprint $table) {
15
+            $table->string('thumbnail')->nullable()->after('img_url')->comment('縮圖');
16
+        });
17
+    }
18
+
19
+    /**
20
+     * Reverse the migrations.
21
+     */
22
+    public function down(): void
23
+    {
24
+        Schema::table('projects', function (Blueprint $table) {
25
+            $table->dropColumn('thumbnail');
26
+        });
27
+    }
28
+};