diff --git a/app/Http/Controllers/Api/PresignedUrlController.php b/app/Http/Controllers/Api/PresignedUrlController.php index d23af73..cf9e98b 100644 --- a/app/Http/Controllers/Api/PresignedUrlController.php +++ b/app/Http/Controllers/Api/PresignedUrlController.php @@ -6,6 +6,7 @@ use App\Http\Requests\Api\PresignedUrlRequest; use App\Services\S3PresignedService; use Illuminate\Http\JsonResponse; +use Illuminate\Http\RedirectResponse; class PresignedUrlController extends Controller { @@ -25,4 +26,12 @@ public function store(PresignedUrlRequest $request): JsonResponse return response()->json($result); } + + public function show(string $key): RedirectResponse + { + $minutes = (int) request('minutes', 60); + $url = $this->service->getTemporaryUrl(urldecode($key), $minutes); + + return redirect($url); + } } diff --git a/app/Http/Requests/Admin/Settings/UpdateHRRequest.php b/app/Http/Requests/Admin/Settings/UpdateHRRequest.php index d675547..5daeaa2 100644 --- a/app/Http/Requests/Admin/Settings/UpdateHRRequest.php +++ b/app/Http/Requests/Admin/Settings/UpdateHRRequest.php @@ -3,6 +3,7 @@ namespace App\Http\Requests\Admin\Settings; use Illuminate\Foundation\Http\FormRequest; +use Override; class UpdateHRRequest extends FormRequest { @@ -11,6 +12,22 @@ public function authorize(): bool return true; } + #[Override] + public function prepareForValidation() + { + if ($this->has('late_penalty_amount')) { + $this->merge([ + 'late_penalty_amount' => str_replace('.', '', $this->late_penalty_amount), + ]); + } + + if ($this->has('absent_penalty_amount')) { + $this->merge([ + 'absent_penalty_amount' => str_replace('.', '', $this->absent_penalty_amount), + ]); + } + } + public function rules(): array { return [ diff --git a/app/Http/Requests/Admin/Settings/UpdateHomepageRequest.php b/app/Http/Requests/Admin/Settings/UpdateHomepageRequest.php index 38a2543..4d77fbf 100644 --- a/app/Http/Requests/Admin/Settings/UpdateHomepageRequest.php +++ b/app/Http/Requests/Admin/Settings/UpdateHomepageRequest.php @@ -14,19 +14,19 @@ public function authorize(): bool public function rules(): array { return [ - 'hero_image_url' => ['nullable', 'string', 'max:2000'], - 'about_image_url' => ['nullable', 'string', 'max:2000'], - 'gallery_images' => ['nullable', 'array'], - 'gallery_images.*' => ['string', 'max:2000'], + 'hero_image_key' => ['nullable', 'string', 'max:2000'], + 'about_image_key' => ['nullable', 'string', 'max:2000'], + 'gallery_image_keys' => ['nullable', 'array'], + 'gallery_image_keys.*' => ['string', 'max:2000'], ]; } public function attributes(): array { return [ - 'hero_image_url' => 'url foto hero', - 'about_image_url' => 'url foto tentang kami', - 'gallery_images' => 'gambar galeri', + 'hero_image_key' => 'foto hero', + 'about_image_key' => 'foto tentang kami', + 'gallery_image_keys' => 'gambar galeri', ]; } } diff --git a/app/Services/Admin/AdminSettingsService.php b/app/Services/Admin/AdminSettingsService.php index 51b80a5..311f6b2 100644 --- a/app/Services/Admin/AdminSettingsService.php +++ b/app/Services/Admin/AdminSettingsService.php @@ -2,6 +2,7 @@ namespace App\Services\Admin; +use App\Services\S3PresignedService; use App\Settings\HRSettings; use App\Settings\HomepageSettings; use App\Settings\MarketplaceSettings; @@ -11,6 +12,10 @@ class AdminSettingsService { + public function __construct( + private S3PresignedService $s3Service = new S3PresignedService, + ) {} + public function getSystemData(): array { $settings = app(SystemSettings::class); @@ -29,12 +34,27 @@ public function getHomepageData(): array $settings = app(HomepageSettings::class); return [ - 'hero_image_url' => $settings->hero_image_url, - 'about_image_url' => $settings->about_image_url, - 'gallery_images' => $settings->gallery_images, + 'hero_image_url' => $settings->hero_image_url ? $this->getTemporaryUrl($settings->hero_image_url) : null, + 'hero_image_key' => $settings->hero_image_url, + 'about_image_url' => $settings->about_image_url ? $this->getTemporaryUrl($settings->about_image_url) : null, + 'about_image_key' => $settings->about_image_url, + 'gallery_images' => array_map( + fn ($key) => $this->getTemporaryUrl($key), + $settings->gallery_images ?? [], + ), + 'gallery_image_keys' => $settings->gallery_images ?? [], ]; } + private function getTemporaryUrl(string $key): string + { + if (str_starts_with($key, 'http')) { + return $key; + } + + return $this->s3Service->getTemporaryUrl($key, 60); + } + public function getSocialMediaData(): array { $settings = app(SocialMediaSettings::class); @@ -95,7 +115,11 @@ public function updateSystem(array $data): void public function updateHomepage(array $data): void { $settings = app(HomepageSettings::class); - $settings->fill($data); + + $settings->hero_image_url = $data['hero_image_key'] ?? null; + $settings->about_image_url = $data['about_image_key'] ?? null; + $settings->gallery_images = $data['gallery_image_keys'] ?? []; + $settings->save(); } diff --git a/resources/js/components/file-upload.tsx b/resources/js/components/file-upload.tsx index 629fd87..4501261 100644 --- a/resources/js/components/file-upload.tsx +++ b/resources/js/components/file-upload.tsx @@ -1,5 +1,5 @@ import { FileImage, Upload, X } from 'lucide-react'; -import { useEffect, useRef, useState } from 'react'; +import { useEffect, useRef, useState, useId } from 'react'; import { Attachment, AttachmentAction, @@ -36,6 +36,7 @@ function formatFileSize(bytes: number): string { export function FileUpload({ value, onChange, folder, accept = 'image/jpeg,image/png,image/webp,image/gif', onUploadingChange, existingUrl, onFileMeta }: FileUploadProps) { const inputRef = useRef(null); + const uploadId = useId(); const [uploading, setUploading] = useState(false); const [error, setError] = useState(null); const [fileName, setFileName] = useState(null); @@ -106,7 +107,7 @@ export function FileUpload({ value, onChange, folder, accept = 'image/jpeg,image accept={accept} onChange={handleFileChange} className="hidden" - id="file-upload" + id={uploadId} /> diff --git a/resources/js/pages/admin/settings/index.tsx b/resources/js/pages/admin/settings/index.tsx index 1755d7f..0369e0c 100644 --- a/resources/js/pages/admin/settings/index.tsx +++ b/resources/js/pages/admin/settings/index.tsx @@ -1,3 +1,4 @@ +import { FileUpload } from '@/components/file-upload'; import InputError from '@/components/input-error'; import { PhoneNumberInput } from '@/components/phone-number-input'; import { RupiahInput } from '@/components/rupiah-input'; @@ -10,8 +11,9 @@ import { Separator } from '@/components/ui/separator'; import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Textarea } from '@/components/ui/textarea'; import { cn } from '@/lib/utils'; -import { updateHomepage, updateMarketplace, updateSocialMedia, updateSystem } from '@/routes/admin/settings'; +import { updateHomepage, updateHr, updateMarketplace, updateSocialMedia, updateSystem } from '@/routes/admin/settings'; import { Form, Head } from '@inertiajs/react'; +import { Plus, Trash2 } from 'lucide-react'; import { useState } from 'react'; type MarketplaceFeeRule = { @@ -30,8 +32,11 @@ type Props = { }; homepage: { hero_image_url: string; + hero_image_key: string; about_image_url: string; + about_image_key: string; gallery_images: string[]; + gallery_image_keys: string[]; }; socialMedia: { instagram_url: string; @@ -50,6 +55,124 @@ type Props = { }; }; +type HomepageSettingsTabProps = { + homepage: Props['homepage']; +}; + +function HomepageSettingsTab({ homepage }: HomepageSettingsTabProps) { + const [heroKey, setHeroKey] = useState(homepage.hero_image_key ?? null); + const [heroUploading, setHeroUploading] = useState(false); + const [aboutKey, setAboutKey] = useState(homepage.about_image_key ?? null); + const [aboutUploading, setAboutUploading] = useState(false); + const [galleryKeys, setGalleryKeys] = useState(homepage.gallery_image_keys ?? []); + const [galleryUploading, setGalleryUploading] = useState(false); + + function addGalleryImage() { + setGalleryKeys((prev) => [...prev, '']); + } + + function removeGalleryImage(index: number) { + setGalleryKeys((prev) => prev.filter((_, i) => i !== index)); + } + + function updateGalleryKey(index: number, key: string | null) { + setGalleryKeys((prev) => prev.map((k, i) => (i === index ? (key ?? '') : k))); + } + + return ( +
+ {({ processing, errors }) => ( +
+ + + Homepage + + + + + {galleryKeys.map((key, index) => ( + + ))} + +
+ + + +
+ +
+ + + +
+ +
+
+ + +
+
+ {galleryKeys.map((key, index) => ( +
+
+ updateGalleryKey(index, k)} + folder="homepage/gallery" + existingUrl={homepage.gallery_images[index] ?? null} + onUploadingChange={setGalleryUploading} + /> +
+ +
+ ))} + {galleryKeys.length === 0 && ( +

Belum ada gambar. Klik "Tambah" untuk menambahkan gambar galeri.

+ )} +
+ +
+
+
+
+ +
+
+ )} +
+ ); +} + const sidebarTabs = [ { key: 'sistem', label: 'Sistem' }, { key: 'homepage', label: 'Homepage' }, @@ -249,32 +372,7 @@ export default function AdminSettings({ system, homepage, socialMedia, marketpla )} {activeTab === 'homepage' && ( -
- {({ processing, errors }) => ( -
- - - Homepage - - -
- - - -
-
- - - -
-
-
-
- -
-
- )} -
+ )} {activeTab === 'media-sosial' && ( @@ -336,7 +434,7 @@ export default function AdminSettings({ system, homepage, socialMedia, marketpla )} {activeTab === 'hr' && ( -
+ {({ processing, errors }) => (
diff --git a/routes/api.php b/routes/api.php index d983e74..43c2a22 100644 --- a/routes/api.php +++ b/routes/api.php @@ -6,3 +6,7 @@ Route::post('/presigned-url', [PresignedUrlController::class, 'store']) ->middleware(['web', 'auth', 'verified']) ->name('presigned-url.store'); + +Route::get('/presigned-url/{key}', [PresignedUrlController::class, 'show']) + ->middleware(['web', 'auth', 'verified']) + ->name('presigned-url.show'); diff --git a/tests/Feature/Admin/Settings/AdminSettingsTest.php b/tests/Feature/Admin/Settings/AdminSettingsTest.php index ea173d8..d1dcf37 100644 --- a/tests/Feature/Admin/Settings/AdminSettingsTest.php +++ b/tests/Feature/Admin/Settings/AdminSettingsTest.php @@ -32,7 +32,7 @@ test('guest cannot update homepage settings', function () { $response = $this->put(route('admin.settings.update-homepage'), [ - 'hero_image_url' => 'https://unauthorized.com/image.jpg', + 'hero_image_key' => 'unauthorized/image.jpg', ]); $response->assertRedirect(route('login')); }); @@ -257,9 +257,9 @@ $this->actingAs($user); $response = $this->put(route('admin.settings.update-homepage'), [ - 'hero_image_url' => 'https://example.com/hero.jpg', - 'about_image_url' => 'https://example.com/about.jpg', - 'gallery_images' => ['https://example.com/g1.jpg', 'https://example.com/g2.jpg'], + 'hero_image_key' => 'homepage/hero/2026/07/31/abc123/hero.jpg', + 'about_image_key' => 'homepage/about/2026/07/31/def456/about.jpg', + 'gallery_image_keys' => ['homepage/gallery/2026/07/31/g1/img.jpg', 'homepage/gallery/2026/07/31/g2/img.jpg'], ]); $response @@ -267,9 +267,9 @@ ->assertRedirect(route('admin.settings.index')); $settings = app(HomepageSettings::class); - expect($settings->hero_image_url)->toBe('https://example.com/hero.jpg'); - expect($settings->about_image_url)->toBe('https://example.com/about.jpg'); - expect($settings->gallery_images)->toBe(['https://example.com/g1.jpg', 'https://example.com/g2.jpg']); + expect($settings->hero_image_url)->toBe('homepage/hero/2026/07/31/abc123/hero.jpg'); + expect($settings->about_image_url)->toBe('homepage/about/2026/07/31/def456/about.jpg'); + expect($settings->gallery_images)->toBe(['homepage/gallery/2026/07/31/g1/img.jpg', 'homepage/gallery/2026/07/31/g2/img.jpg']); }); test('homepage fields are nullable', function () { @@ -277,44 +277,44 @@ $this->actingAs($user); $response = $this->put(route('admin.settings.update-homepage'), [ - 'hero_image_url' => null, - 'about_image_url' => null, + 'hero_image_key' => null, + 'about_image_key' => null, ]); $response->assertSessionHasNoErrors(); }); -test('homepage hero_image_url must not exceed 2000 characters', function () { +test('homepage hero_image_key must not exceed 2000 characters', function () { $user = User::factory()->create(); $this->actingAs($user); $response = $this->put(route('admin.settings.update-homepage'), [ - 'hero_image_url' => str_repeat('a', 2001), + 'hero_image_key' => str_repeat('a', 2001), ]); - $response->assertSessionHasErrors('hero_image_url'); + $response->assertSessionHasErrors('hero_image_key'); }); -test('homepage gallery_images must be array', function () { +test('homepage gallery_image_keys must be array', function () { $user = User::factory()->create(); $this->actingAs($user); $response = $this->put(route('admin.settings.update-homepage'), [ - 'gallery_images' => 'not-an-array', + 'gallery_image_keys' => 'not-an-array', ]); - $response->assertSessionHasErrors('gallery_images'); + $response->assertSessionHasErrors('gallery_image_keys'); }); -test('homepage gallery_images items must not exceed 2000 characters', function () { +test('homepage gallery_image_keys items must not exceed 2000 characters', function () { $user = User::factory()->create(); $this->actingAs($user); $response = $this->put(route('admin.settings.update-homepage'), [ - 'gallery_images' => [str_repeat('a', 2001)], + 'gallery_image_keys' => [str_repeat('a', 2001)], ]); - $response->assertSessionHasErrors('gallery_images.0'); + $response->assertSessionHasErrors('gallery_image_keys.0'); }); /* @@ -851,8 +851,8 @@ // Update homepage $this->put(route('admin.settings.update-homepage'), [ - 'hero_image_url' => 'https://example.com/new-hero.jpg', - 'about_image_url' => 'https://example.com/new-about.jpg', + 'hero_image_key' => 'homepage/hero/2026/07/31/new-hero.jpg', + 'about_image_key' => 'homepage/about/2026/07/31/new-about.jpg', ])->assertSessionHasNoErrors(); // Update social media @@ -898,7 +898,7 @@ $hr = app(HRSettings::class); expect($system->app_name)->toBe('DST Updated'); - expect($homepage->hero_image_url)->toBe('https://example.com/new-hero.jpg'); + expect($homepage->hero_image_url)->toBe('homepage/hero/2026/07/31/new-hero.jpg'); expect($socialMedia->instagram_url)->toBe('https://instagram.com/newdst'); expect($marketplace->tiktok_shop_platform_commission->value)->toBe(3.0); expect($hr->scheduled_check_in_time)->toBe('08:30');