diff --git a/app/Http/Controllers/Dashboard/SettingController.php b/app/Http/Controllers/Dashboard/SettingController.php index b838e54d..1fb1db0a 100644 --- a/app/Http/Controllers/Dashboard/SettingController.php +++ b/app/Http/Controllers/Dashboard/SettingController.php @@ -8,6 +8,7 @@ use App\Http\Requests\SettingRequest; use App\Models\Setting; use Illuminate\Http\RedirectResponse; +use Illuminate\Support\Facades\Storage; use Inertia\Response; class SettingController extends Controller @@ -21,11 +22,42 @@ public function edit(): Response public function update(SettingRequest $request): RedirectResponse { - Setting::query()->first() - ->update($request->validated()); + $settings = Setting::query()->firstOrFail(); + $settings->fill($request->getData()); + + if ($request->file("logo")) { + if ($settings->logo) { + Storage::disk("public")->delete($settings->logo); + } + $file = $request->file("logo"); + $fileName = $file->getClientOriginalName(); + $path = "/logo"; + + $fullPath = Storage::disk("public")->putFileAs($path, $file, $fileName); + $settings->logo = $fullPath; + } + + $settings->save(); return redirect() ->back() ->with("success", "Zaktualizowano ustawienia"); } + + public function removeLogo(): RedirectResponse + { + $settings = Setting::query()->firstOrFail(); + + if ($settings->logo) { + $res = Storage::disk("public")->delete($settings->logo); + $settings->logo = null; + $settings->save(); + + return redirect() + ->back() + ->with("success", "Usunięto logo"); + } + + abort(404); + } } diff --git a/app/Http/Requests/SettingRequest.php b/app/Http/Requests/SettingRequest.php index 3a4f2232..ae103e17 100644 --- a/app/Http/Requests/SettingRequest.php +++ b/app/Http/Requests/SettingRequest.php @@ -16,6 +16,22 @@ public function rules(): array "teacher_titles" => ["required", "max:255"], "university_name" => ["required", "max:255"], "department_name" => ["required", "max:255"], + "primary_color" => ["required", "regex:/^#([A-Fa-f0-9]{6})$/"], + "secondary_color" => ["required", "regex:/^#([A-Fa-f0-9]{6})$/"], + "logo" => ["nullable", "image", "max:1024"], + ]; + } + + public function getData(): array + { + return [ + "teacher_name" => $this->input("teacher_name"), + "teacher_email" => $this->input("teacher_email"), + "teacher_titles" => $this->input("teacher_titles"), + "university_name" => $this->input("university_name"), + "department_name" => $this->input("department_name"), + "primary_color" => $this->input("primary_color"), + "secondary_color" => $this->input("secondary_color"), ]; } } diff --git a/app/Models/Setting.php b/app/Models/Setting.php index ecb6a981..bb4dc614 100644 --- a/app/Models/Setting.php +++ b/app/Models/Setting.php @@ -30,5 +30,8 @@ class Setting extends Model "teacher_titles", "university_name", "department_name", + "primary_color", + "secondary_color", + "logo", ]; } diff --git a/database/factories/SettingFactory.php b/database/factories/SettingFactory.php index 876e117d..89664ef0 100644 --- a/database/factories/SettingFactory.php +++ b/database/factories/SettingFactory.php @@ -16,6 +16,8 @@ public function definition(): array "teacher_email" => fake()->email, "department_name" => "Zakład Informatyki, Wydział Nauk Technicznych i Ekonomicznych", "university_name" => "Collegium Witelona Uczelnia Państwowa", + "primary_color" => "#000000", + "secondary_color" => "#ffffff", ]; } } diff --git a/database/migrations/2024_08_07_075139_add_fields_to_settings_table.php b/database/migrations/2024_08_07_075139_add_fields_to_settings_table.php new file mode 100644 index 00000000..edc1f8d4 --- /dev/null +++ b/database/migrations/2024_08_07_075139_add_fields_to_settings_table.php @@ -0,0 +1,27 @@ +<?php + +declare(strict_types=1); + +use Illuminate\Database\Migrations\Migration; +use Illuminate\Database\Schema\Blueprint; +use Illuminate\Support\Facades\Schema; + +return new class() extends Migration { + public function up(): void + { + Schema::table("settings", function (Blueprint $table): void { + $table->string("primary_color")->nullable(); + $table->string("secondary_color")->nullable(); + $table->string("logo")->nullable(); + }); + } + + public function down(): void + { + Schema::table("settings", function (Blueprint $table): void { + $table->dropColumn("primary_color"); + $table->dropColumn("secondary_color"); + $table->dropColumn("logo"); + }); + } +}; diff --git a/environment/dev/app/Dockerfile b/environment/dev/app/Dockerfile index 1024bc85..a45f0a09 100644 --- a/environment/dev/app/Dockerfile +++ b/environment/dev/app/Dockerfile @@ -41,7 +41,9 @@ RUN apt-get update \ && echo "deb https://nginx.org/packages/mainline/debian bullseye nginx" | tee /etc/apt/sources.list.d/nginx.list \ && apt-get update && apt-get install --assume-yes \ nginx=${NGINX_VERSION} \ + gnupg \ libzip-dev \ + libpng-dev \ libpq-dev \ supervisor \ cron \ @@ -49,6 +51,7 @@ RUN apt-get update \ && docker-php-ext-install \ zip \ pdo_pgsql \ + gd \ && docker-php-ext-enable \ redis diff --git a/environment/dev/app/nginx.conf b/environment/dev/app/nginx.conf index 0350a9f7..ddeabbe3 100644 --- a/environment/dev/app/nginx.conf +++ b/environment/dev/app/nginx.conf @@ -26,6 +26,8 @@ http { listen 80 default; server_name keating-nginx; + client_max_body_size 20M; + access_log /dev/stdout; error_log /dev/stderr; diff --git a/environment/dev/app/php.ini b/environment/dev/app/php.ini index 8ffeea58..1d16a3ee 100644 --- a/environment/dev/app/php.ini +++ b/environment/dev/app/php.ini @@ -1,5 +1,7 @@ [PHP] memory_limit = 256M +upload_max_filesize = 20m +post_max_size = 20m [xdebug] xdebug.client_host=xdebug://gateway diff --git a/resources/js/Pages/Dashboard/Setting/Edit.vue b/resources/js/Pages/Dashboard/Setting/Edit.vue index bebafa1e..0666a819 100644 --- a/resources/js/Pages/Dashboard/Setting/Edit.vue +++ b/resources/js/Pages/Dashboard/Setting/Edit.vue @@ -9,6 +9,9 @@ import { useForm } from '@inertiajs/inertia-vue3' import FormError from '@/Shared/Forms/FormError.vue' import ManagementHeader from '@/Shared/Components/ManagementHeader.vue' import ManagementHeaderItem from '@/Shared/Components/ManagementHeaderItem.vue' +import ColorInput from '../../../Shared/Forms/ColorInput.vue' +import { ref } from 'vue' +import { Method } from '@inertiajs/inertia' const props = defineProps({ settings: Object, @@ -20,10 +23,29 @@ const form = useForm({ teacher_titles: props.settings.teacher_titles, university_name: props.settings.university_name, department_name: props.settings.department_name, + primary_color: props.settings.primary_color, + secondary_color: props.settings.secondary_color, + logo: null, }) +const imageUrl = ref('') + function updateSettings() { - form.patch('/dashboard/settings') + form.post('/dashboard/settings') +} + +function onFileSelected(event) { + const file = event.target?.files[0] + + if (file.size > 1024 * 1024) { + form.errors.logo = 'Plik nie może być większy niż 1MB' + + return + } + + form.logo = file + imageUrl.value = URL.createObjectURL(event.target?.files[0]) + form.errors.logo = '' } </script> @@ -40,7 +62,7 @@ function updateSettings() { </ManagementHeaderItem> </template> </ManagementHeader> - <form class="grid grid-cols-2" @submit.prevent="updateSettings"> + <form class="grid grid-cols-2" enctype="multipart/form-data" @submit.prevent="updateSettings"> <Section> <div class="flex flex-col justify-between gap-4"> <FormGroup> @@ -78,6 +100,55 @@ function updateSettings() { <TextInput id="department_name" v-model="form.department_name" :error="form.errors.department_name" autocomplete="off" /> <FormError :error="form.errors.department_name" /> </FormGroup> + <FormGroup> + <FormLabel for="primary_color"> + Kolor główny + </FormLabel> + <ColorInput id="primary_color" v-model="form.primary_color" :error="form.errors.primary_color" autocomplete="off" /> + <FormError :error="form.errors.primary_color" /> + </FormGroup> + <FormGroup> + <FormLabel for="secondary_color"> + Kolor dodatkowy + </FormLabel> + <ColorInput id="secondary_color" v-model="form.secondary_color" :error="form.errors.secondary_color" autocomplete="off" /> + <FormError :error="form.errors.secondary_color" /> + </FormGroup> + <FormGroup> + <FormLabel for="title"> + Logo + </FormLabel> + <input + class="border-brand-light-gray text-brand-black hover:border-brand-black focus:border-brand-black !mb-px block w-full border-0 border-b p-2 text-sm font-medium hover:!mb-px hover:border-b-2 focus:!mb-px focus:border-b-2 focus:ring-0 focus:ring-offset-0" + type="file" max="1" @input="onFileSelected" + > + <FormError :error="form.errors.logo" class="mt-2" /> + </FormGroup> + <FormGroup v-if="settings.logo || imageUrl"> + <div v-if="settings.logo && !imageUrl"> + <FormLabel class="mb-3 flex justify-between"> + Aktualne logo + <InertiaLink + href="/dashboard/settings/remove-logo" + :method="Method.DELETE" + class="text-sm text-red-500 hover:text-red-700" + @click="form.logo = ''" + > + Usuń + </InertiaLink> + </FormLabel> + <img :alt="'alt text'" + :src="`/storage/${settings.logo}`" + class="m-auto shadow-lg" + > + </div> + <div v-else> + <FormLabel class="mb-3"> + Przesłane logo + </FormLabel> + <img :src="imageUrl" alt="Image preview" class="m-auto shadow-lg"> + </div> + </FormGroup> <div class="mt-4 flex justify-end"> <SubmitButton> Zapisz diff --git a/resources/js/Pages/Public/Home.vue b/resources/js/Pages/Public/Home.vue index 94dc2d04..526b6ab1 100644 --- a/resources/js/Pages/Public/Home.vue +++ b/resources/js/Pages/Public/Home.vue @@ -24,7 +24,7 @@ defineProps({ <PublicLayout> <div v-if="sectionSettings.banner_enabled" class="relative isolate bg-white pt-14"> <BackgroundGrid /> - <img src="/cwup.png" alt="" class="absolute right-0 hidden w-[50%] opacity-10 lg:mt-16 lg:block xl:mt-10 2xl:mt-0"> + <img src="/cwup.png" alt="" class="absolute right-0 hidden w-1/2 opacity-10 lg:mt-16 lg:block xl:mt-10 2xl:mt-0"> <div class="mx-auto max-w-7xl px-6 py-24 sm:py-32 lg:flex lg:items-center lg:gap-x-10 lg:px-8 lg:py-32"> <div class="mx-auto max-w-7xl text-center lg:mx-0 lg:flex-auto"> <h1 class="mx-auto mt-10 max-w-4xl text-4xl font-bold tracking-tight text-gray-900 sm:text-6xl"> diff --git a/resources/js/Pages/Public/Login.vue b/resources/js/Pages/Public/Login.vue index eb831e0b..cb2f8c7d 100644 --- a/resources/js/Pages/Public/Login.vue +++ b/resources/js/Pages/Public/Login.vue @@ -22,7 +22,7 @@ function attemptLogin() { <PublicLayout> <div class="relative isolate bg-white pt-14"> <BackgroundGrid /> - <img src="/cwup.png" alt="" class="absolute right-0 z-0 hidden w-[50%] opacity-10 lg:mt-16 lg:block xl:mt-10 2xl:mt-0"> + <img src="/cwup.png" alt="" class="absolute right-0 z-0 hidden w-1/2 opacity-10 lg:mt-16 lg:block xl:mt-10 2xl:mt-0"> <div class="mx-auto max-w-7xl px-6 py-24 sm:py-32 lg:flex lg:items-center lg:gap-x-10 lg:px-8 lg:py-40"> <div class="mx-auto max-w-7xl text-center lg:mx-0 lg:flex-auto"> <img :src="universityLogo" :alt="university" class="mx-auto w-[360px]"> diff --git a/resources/js/Shared/Forms/ColorInput.vue b/resources/js/Shared/Forms/ColorInput.vue new file mode 100644 index 00000000..eb4f74a5 --- /dev/null +++ b/resources/js/Shared/Forms/ColorInput.vue @@ -0,0 +1,32 @@ +<script setup> +import { computed } from 'vue' + +const props = defineProps({ + modelValue: { + type: [String, Number, null], + default: null, + }, + error: { + type: String, + default: null, + }, +}) +const emit = defineEmits(['update:modelValue']) +const value = computed({ + get: () => props.modelValue, + set: (value) => { + emit('update:modelValue', value) + }, +}) +</script> + +<template> + <input v-bind="$attrs" v-model="value" + type="color" + :class="[props.error + ? 'text-red-900 ring-red-300 placeholder:text-red-300' + : 'text-gray-900 shadow-sm ring-gray-300 placeholder:text-gray-400', + 'block h-10 w-14 cursor-pointer rounded-lg border border-gray-200 bg-white p-1 disabled:pointer-events-none disabled:opacity-50 dark:border-neutral-700 dark:bg-neutral-900' + ]" + > +</template> diff --git a/routes/web.php b/routes/web.php index 782d1530..c21b994d 100644 --- a/routes/web.php +++ b/routes/web.php @@ -124,7 +124,8 @@ }); Route::controller(SettingController::class)->group(function (): void { Route::get("/settings", "edit")->name("settings.edit"); - Route::patch("/settings", "update")->name("settings.update"); + Route::post("/settings", "update")->name("settings.update"); + Route::delete("/settings/remove-logo", "removeLogo")->name("settings.remove.logo"); }); Route::controller(SectionController::class)->group(function (): void { Route::get("/sections", "show")->name("sections.show"); diff --git a/tests/Feature/ExampleTest.php b/tests/Feature/ExampleTest.php index 940210ce..0a01957c 100644 --- a/tests/Feature/ExampleTest.php +++ b/tests/Feature/ExampleTest.php @@ -10,6 +10,13 @@ class ExampleTest extends TestCase { + protected function tearDown(): void + { + Setting::query()->delete(); + + parent::tearDown(); + } + public function testTheApplicationReturnsASuccessfulResponse(): void { Setting::factory()->create(); diff --git a/tests/Feature/SettingsTest.php b/tests/Feature/SettingsTest.php index f86add0b..f252b8cd 100644 --- a/tests/Feature/SettingsTest.php +++ b/tests/Feature/SettingsTest.php @@ -2,11 +2,13 @@ declare(strict_types=1); -namespace Feature; +namespace Tests\Feature; use App\Models\Setting; use App\Models\User; use Illuminate\Foundation\Testing\RefreshDatabase; +use Illuminate\Http\UploadedFile; +use Illuminate\Support\Facades\Storage; use Illuminate\Support\Str; use Tests\TestCase; @@ -25,6 +27,8 @@ protected function setUp(): void "teacher_titles" => "dr inż.", "university_name" => "CWUP", "department_name" => "IT department", + "primary_color" => "#000000", + "secondary_color" => "#ffffff", ]); $this->actingAs($this->user); } @@ -37,14 +41,19 @@ public function testSettingsCanBeUpdated(): void "teacher_titles" => "dr inż.", "university_name" => "CWUP", "department_name" => "IT department", + "primary_color" => "#000000", + "secondary_color" => "#ffffff", + "logo" => null, ]); - $this->patch("/dashboard/settings", [ + $this->post("/dashboard/settings", [ "teacher_name" => "John Doe", "teacher_email" => "john.doe@exmple.com", "teacher_titles" => "dr", "university_name" => "SWPS", "department_name" => "Psychology department", + "primary_color" => "#11ffff", + "secondary_color" => "#110000", ])->assertSessionHasNoErrors(); $this->assertDatabaseHas("settings", [ @@ -53,6 +62,9 @@ public function testSettingsCanBeUpdated(): void "teacher_titles" => "dr", "university_name" => "SWPS", "department_name" => "Psychology department", + "primary_color" => "#11ffff", + "secondary_color" => "#110000", + "logo" => null, ]); } @@ -64,14 +76,19 @@ public function testSettingsCannotBeUpdatedWithInvalidData(): void "teacher_titles" => "dr inż.", "university_name" => "CWUP", "department_name" => "IT department", + "primary_color" => "#000000", + "secondary_color" => "#ffffff", + "logo" => null, ]); - $this->patch("/dashboard/settings", [ + $this->post("/dashboard/settings", [ "teacher_name" => Str::random(256), "teacher_email" => "john.doe", "teacher_titles" => Str::random(256), "university_name" => Str::random(256), "department_name" => Str::random(256), + "primary_color" => "000000", + "secondary_color" => "ffffff", ])->assertSessionHasErrors() ->assertInvalid([ "teacher_name", @@ -79,6 +96,8 @@ public function testSettingsCannotBeUpdatedWithInvalidData(): void "teacher_titles", "university_name", "department_name", + "primary_color", + "secondary_color", ]); $this->assertDatabaseHas("settings", [ @@ -87,6 +106,9 @@ public function testSettingsCannotBeUpdatedWithInvalidData(): void "teacher_titles" => "dr inż.", "university_name" => "CWUP", "department_name" => "IT department", + "primary_color" => "#000000", + "secondary_color" => "#ffffff", + "logo" => null, ]); } @@ -98,14 +120,19 @@ public function testSettingsCannotBeUpdatedWithEmptyData(): void "teacher_titles" => "dr inż.", "university_name" => "CWUP", "department_name" => "IT department", + "primary_color" => "#000000", + "secondary_color" => "#ffffff", + "logo" => null, ]); - $this->patch("/dashboard/settings", [ + $this->post("/dashboard/settings", [ "teacher_name" => "", "teacher_email" => "", "teacher_titles" => "", "university_name" => "", "department_name" => "", + "primary_color" => "", + "secondary_color" => "", ])->assertSessionHasErrors() ->assertInvalid([ "teacher_name", @@ -113,6 +140,8 @@ public function testSettingsCannotBeUpdatedWithEmptyData(): void "teacher_titles", "university_name", "department_name", + "primary_color", + "secondary_color", ]); $this->assertDatabaseHas("settings", [ @@ -121,6 +150,104 @@ public function testSettingsCannotBeUpdatedWithEmptyData(): void "teacher_titles" => "dr inż.", "university_name" => "CWUP", "department_name" => "IT department", + "primary_color" => "#000000", + "secondary_color" => "#ffffff", + "logo" => null, + ]); + } + + public function testLogoCanBeUploadedAndRemoved(): void + { + Storage::fake("public"); + $this->assertDatabaseHas("settings", [ + "teacher_name" => "Ty Doe", + "teacher_email" => "ty.doe@exmple.com", + "teacher_titles" => "dr inż.", + "university_name" => "CWUP", + "department_name" => "IT department", + "primary_color" => "#000000", + "secondary_color" => "#ffffff", + "logo" => null, + ]); + $logo = UploadedFile::fake()->image("logo.png"); + + $this->post("/dashboard/settings", [ + "teacher_name" => "John Doe", + "teacher_email" => "john.doe@exmple.com", + "teacher_titles" => "dr", + "university_name" => "SWPS", + "department_name" => "Psychology department", + "primary_color" => "#11ffff", + "secondary_color" => "#110000", + "logo" => $logo, + ])->assertSessionHasNoErrors(); + + $this->assertDatabaseHas("settings", [ + "teacher_name" => "John Doe", + "teacher_email" => "john.doe@exmple.com", + "teacher_titles" => "dr", + "university_name" => "SWPS", + "department_name" => "Psychology department", + "primary_color" => "#11ffff", + "secondary_color" => "#110000", + "logo" => "logo/logo.png", + ]); + + Storage::disk("public")->assertExists("logo/logo.png"); + + $this->delete("/dashboard/settings/remove-logo")->assertSessionHasNoErrors(); + + $this->assertDatabaseHas("settings", [ + "teacher_name" => "John Doe", + "teacher_email" => "john.doe@exmple.com", + "teacher_titles" => "dr", + "university_name" => "SWPS", + "department_name" => "Psychology department", + "primary_color" => "#11ffff", + "secondary_color" => "#110000", + "logo" => null, + ]); + + Storage::disk("public")->assertMissing("logo/logo.png"); + } + + public function testSettingsCannotBeUpdatedWithTooBigLogoFile(): void + { + $this->assertDatabaseHas("settings", [ + "teacher_name" => "Ty Doe", + "teacher_email" => "ty.doe@exmple.com", + "teacher_titles" => "dr inż.", + "university_name" => "CWUP", + "department_name" => "IT department", + "primary_color" => "#000000", + "secondary_color" => "#ffffff", + "logo" => null, + ]); + $logo = UploadedFile::fake()->image("logo.png")->size(2049); + + $this->post("/dashboard/settings", [ + "teacher_name" => "John Doe", + "teacher_email" => "john.doe@exmple.com", + "teacher_titles" => "dr", + "university_name" => "SWPS", + "department_name" => "Psychology department", + "primary_color" => "#11ffff", + "secondary_color" => "#110000", + "logo" => $logo, + ])->assertSessionHasErrors() + ->assertInvalid([ + "logo", + ]); + + $this->assertDatabaseHas("settings", [ + "teacher_name" => "Ty Doe", + "teacher_email" => "ty.doe@exmple.com", + "teacher_titles" => "dr inż.", + "university_name" => "CWUP", + "department_name" => "IT department", + "primary_color" => "#000000", + "secondary_color" => "#ffffff", + "logo" => null, ]); } }