diff --git a/app/Models/ResourceSubmission.php b/app/Models/ResourceSubmission.php index 62772a0..92df2b0 100644 --- a/app/Models/ResourceSubmission.php +++ b/app/Models/ResourceSubmission.php @@ -18,6 +18,16 @@ 'submitter_name', 'submitter_email', 'submitter_organization', + 'organization_website', + 'counties', + 'phone', + 'language', + 'created_on', + 'target_audience', + 'tags', + 'author_credentials', + 'notes', + 'file_paths', 'external_url', 'locale', 'status', @@ -36,6 +46,10 @@ protected function casts(): array return [ 'type' => ResourceType::class, 'status' => SubmissionStatus::class, + 'counties' => 'array', + 'tags' => 'array', + 'file_paths' => 'array', + 'created_on' => 'date', 'reviewed_at' => 'datetime', ]; } diff --git a/database/migrations/2026_09_22_103358_add_submission_details_to_resource_submissions_table.php b/database/migrations/2026_09_22_103358_add_submission_details_to_resource_submissions_table.php new file mode 100644 index 0000000..11c130e --- /dev/null +++ b/database/migrations/2026_09_22_103358_add_submission_details_to_resource_submissions_table.php @@ -0,0 +1,48 @@ +string('organization_website')->nullable()->after('submitter_organization'); + $table->json('counties')->nullable()->after('organization_website'); + $table->string('phone')->nullable()->after('counties'); + $table->string('language')->nullable()->after('phone'); + $table->date('created_on')->nullable()->after('language'); + $table->string('target_audience')->nullable()->after('created_on'); + $table->json('tags')->nullable()->after('target_audience'); + $table->text('author_credentials')->nullable()->after('tags'); + $table->text('notes')->nullable()->after('author_credentials'); + $table->json('file_paths')->nullable()->after('notes'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('resource_submissions', function (Blueprint $table) { + $table->dropColumn([ + 'organization_website', + 'counties', + 'phone', + 'language', + 'created_on', + 'target_audience', + 'tags', + 'author_credentials', + 'notes', + 'file_paths', + ]); + }); + } +}; diff --git a/lang/en/submit.php b/lang/en/submit.php index c26f69c..a23c7e0 100644 --- a/lang/en/submit.php +++ b/lang/en/submit.php @@ -4,12 +4,44 @@ 'title' => 'Submit a Resource', 'subtitle' => 'Share your organization’s materials with the resource library.', 'guidelines_title' => 'Submission guidelines', - 'guidelines' => 'Materials are reviewed for accuracy, relevance, distribution rights, and accessibility before publication.', - 'organization_section' => 'Contact information', + 'guidelines_items' => [ + 'All materials are reviewed by our team for accuracy and relevance', + 'Resources should be evidence-based and professionally developed', + 'Materials must be appropriate for public distribution', + 'Your organization will be credited for approved resources', + 'Any video material should include subtitles and, where possible, sign language interpretation. If sign language interpretation is not yet available, the submitter commits to providing it at a later date.', + ], + 'organization_section' => 'Organization information', 'contact_name' => 'Contact person', 'email' => 'Email address', - 'organization' => 'Organization', - 'source_url' => 'Resource or reference URL', + 'organization' => 'Organization name', + 'organization_website' => 'Organization website', + 'counties' => 'County / area of activity', + 'phone' => 'Phone number', + 'language' => 'Resource language', + 'created_on' => 'Creation date', + 'target_audience' => 'Target audience', + 'tags' => 'Tags', + 'tags_hint' => 'Add relevant keywords, separated by commas, to help users find this resource.', + 'author_credentials' => 'Author/developer credentials', + 'files' => 'Resource files', + 'files_hint' => 'PDF, DOC, PPT, or video files up to 50MB each', + 'notes' => 'Additional notes or comments', + 'choose_type' => 'Select a type', + 'choose_language' => 'Select language', + 'choose_category' => 'Select a category', + 'languages' => [ + 'ro' => 'Romanian', + 'en' => 'English', + 'fr' => 'French', + 'es' => 'Spanish', + 'de' => 'German', + 'it' => 'Italian', + 'hu' => 'Hungarian', + 'other' => 'Other', + ], + 'source_url' => 'Source/reference', + 'cancel' => 'Cancel', 'resource_section' => 'Resource details', 'resource_title' => 'Resource title', 'resource_type' => 'Resource type', diff --git a/lang/ro/submit.php b/lang/ro/submit.php index d6a51f2..b8a4a73 100644 --- a/lang/ro/submit.php +++ b/lang/ro/submit.php @@ -4,12 +4,44 @@ 'title' => 'Trimite o resursă', 'subtitle' => 'Împărtășiți materialele organizației cu biblioteca de resurse.', 'guidelines_title' => 'Ghid de trimitere', - 'guidelines' => 'Materialele sunt verificate înainte de publicare pentru acuratețe, relevanță, drepturi și accesibilitate.', - 'organization_section' => 'Informații de contact', + 'guidelines_items' => [ + 'Toate materialele sunt revizuite de echipa noastră pentru acuratețe și relevanță', + 'Resursele trebuie să fie bazate pe dovezi și dezvoltate profesional', + 'Materialele trebuie să fie adecvate pentru distribuție publică', + 'Organizația dvs. va fi creditată pentru resursele aprobate', + 'Orice material video trebuie să includă subtitrări și, acolo unde este posibil, interpretare în limbaj semne. Dacă interpretarea în limbaj semne nu este disponibilă, depunătorul se angajează să o furnizeze ulterior.', + ], + 'organization_section' => 'Informații despre organizație', 'contact_name' => 'Persoana de contact', 'email' => 'Adresă de email', - 'organization' => 'Organizație', - 'source_url' => 'URL-ul resursei sau al referinței', + 'organization' => 'Numele organizației', + 'organization_website' => 'Website-ul organizației', + 'counties' => 'Județ / zonă de activitate', + 'phone' => 'Număr de telefon', + 'language' => 'Limba resursei', + 'created_on' => 'Data creării', + 'target_audience' => 'Public țintă', + 'tags' => 'Etichete', + 'tags_hint' => 'Adăugați cuvinte cheie relevante, separate prin virgulă, pentru ca utilizatorii să găsească resursa.', + 'author_credentials' => 'Acreditările autorului sau dezvoltatorului', + 'files' => 'Fișiere resursă', + 'files_hint' => 'PDF, DOC, PPT sau video, până la 50MB fiecare', + 'notes' => 'Note sau comentarii suplimentare', + 'choose_type' => 'Selectați un tip', + 'choose_language' => 'Selectați limba', + 'choose_category' => 'Selectați o categorie', + 'languages' => [ + 'ro' => 'Română', + 'en' => 'Engleză', + 'fr' => 'Franceză', + 'es' => 'Spaniolă', + 'de' => 'Germană', + 'it' => 'Italiană', + 'hu' => 'Maghiară', + 'other' => 'Alta', + ], + 'source_url' => 'Sursă / referință', + 'cancel' => 'Anulează', 'resource_section' => 'Detalii resursă', 'resource_title' => 'Titlul resursei', 'resource_type' => 'Tipul resursei', diff --git "a/resources/views/pages/\342\232\241submit.blade.php" "b/resources/views/pages/\342\232\241submit.blade.php" index e54a399..9c87d0d 100644 --- "a/resources/views/pages/\342\232\241submit.blade.php" +++ "b/resources/views/pages/\342\232\241submit.blade.php" @@ -2,14 +2,20 @@ use App\Enums\ResourceType; use App\Enums\SubmissionStatus; +use App\Models\County; +use App\Models\ResourceCategory; use App\Models\ResourceSubmission; use Illuminate\Validation\Rule; use Livewire\Attributes\Layout; use Livewire\Attributes\Title; use Livewire\Component; +use Livewire\Features\SupportFileUploads\TemporaryUploadedFile; +use Livewire\WithFileUploads; new #[Layout('layouts.app')] #[Title('Submit Resource')] class extends Component { + use WithFileUploads; + public string $title = ''; public string $description = ''; @@ -18,35 +24,105 @@ public string $category = ''; + public string $language = ''; + + public string $createdOn = ''; + + public string $targetAudience = ''; + + public string $tags = ''; + + public string $authorCredentials = ''; + + public string $notes = ''; + public string $submitterName = ''; public string $submitterEmail = ''; public string $submitterOrganization = ''; + public string $organizationWebsite = ''; + + public string $phone = ''; + public string $externalUrl = ''; + /** @var array */ + public array $counties = []; + + /** @var array */ + public array $files = []; + public bool $rightsConfirmed = false; public bool $reviewConfirmed = false; public bool $submitted = false; + /** + * @return list + */ + public function languageOptions(): array + { + return ['ro', 'en', 'fr', 'es', 'de', 'it', 'hu', 'other']; + } + + /** + * @return list + */ + public function countyOptions(): array + { + return County::query()->orderBy('name_ro')->pluck('name_ro')->all(); + } + + /** + * @return list + */ + public function categoryOptions(): array + { + return ResourceCategory::query()->orderBy('sort_order')->get()->pluck('name')->filter()->values()->all(); + } + public function submit(): void { $validated = $this->validate([ - 'title' => ['required', 'string', 'max:255'], - 'description' => ['required', 'string', 'max:5000'], - 'type' => ['required', Rule::enum(ResourceType::class)], - 'category' => ['nullable', 'string', 'max:255'], + 'submitterOrganization' => ['required', 'string', 'max:255'], + 'organizationWebsite' => ['nullable', 'url', 'max:2048'], + 'counties' => ['required', 'array', 'min:1'], + 'counties.*' => ['string', Rule::in($this->countyOptions())], 'submitterName' => ['required', 'string', 'max:255'], 'submitterEmail' => ['required', 'email', 'max:255'], - 'submitterOrganization' => ['nullable', 'string', 'max:255'], + 'phone' => ['nullable', 'string', 'max:50'], + 'title' => ['required', 'string', 'max:255'], + 'type' => ['required', Rule::enum(ResourceType::class)], + 'language' => ['required', Rule::in($this->languageOptions())], + 'createdOn' => ['required', 'date'], + 'category' => ['required', 'string', Rule::in($this->categoryOptions())], + 'description' => ['required', 'string', 'max:5000'], + 'targetAudience' => ['required', 'string', 'max:255'], 'externalUrl' => ['nullable', 'url', 'max:2048'], + 'tags' => ['nullable', 'string', 'max:500'], + 'authorCredentials' => ['nullable', 'string', 'max:2000'], + 'files' => ['required', 'array', 'min:1'], + 'files.*' => ['file', 'max:51200', 'mimes:pdf,doc,docx,ppt,pptx,mp4,mov,webm'], + 'notes' => ['nullable', 'string', 'max:5000'], 'rightsConfirmed' => ['accepted'], 'reviewConfirmed' => ['accepted'], ]); + $paths = []; + + foreach ($this->files as $file) { + $paths[] = $file->store('resource-submissions', 'local'); + } + + $tags = collect(explode(',', $validated['tags'] ?? '')) + ->map(fn (string $tag): string => trim($tag)) + ->filter() + ->values() + ->all(); + ResourceSubmission::create([ 'title' => $validated['title'], 'description' => $validated['description'], @@ -55,7 +131,17 @@ public function submit(): void 'submitter_name' => $validated['submitterName'], 'submitter_email' => $validated['submitterEmail'], 'submitter_organization' => $validated['submitterOrganization'], - 'external_url' => $validated['externalUrl'], + 'organization_website' => $validated['organizationWebsite'] ?: null, + 'counties' => $validated['counties'], + 'phone' => $validated['phone'] ?: null, + 'language' => $validated['language'], + 'created_on' => $validated['createdOn'], + 'target_audience' => $validated['targetAudience'], + 'tags' => $tags, + 'author_credentials' => $validated['authorCredentials'] ?: null, + 'notes' => $validated['notes'] ?: null, + 'file_paths' => $paths, + 'external_url' => $validated['externalUrl'] ?: null, 'locale' => app()->getLocale(), 'status' => SubmissionStatus::Pending, ]); @@ -87,16 +173,32 @@ public function submitAnother(): void @else
- {{ __('submit.guidelines') }} + +
    + @foreach (__('submit.guidelines_items') as $guideline) +
  • {{ $guideline }}
  • + @endforeach +
+

{{ __('submit.organization_section') }}

+ + +
+ + + @error('counties')

{{ $message }}

@enderror +
- - +
@@ -106,14 +208,38 @@ public function submitAnother(): void
- + @foreach (ResourceType::cases() as $resourceType) @endforeach - + + + @foreach ($this->languageOptions() as $languageOption) + + @endforeach + + + + + @foreach ($this->categoryOptions() as $categoryOption) + + @endforeach +
+ + + + +
+ + +

{{ __('submit.files_hint') }}

+ @error('files')

{{ $message }}

@enderror + @error('files.*')

{{ $message }}

@enderror +
+ @@ -124,7 +250,10 @@ public function submitAnother(): void @error('reviewConfirmed')

{{ $message }}

@enderror - {{ __('submit.button') }} +
+ {{ __('submit.button') }} + {{ __('submit.cancel') }} +
@endif diff --git a/tests/Feature/SubmitResourceTest.php b/tests/Feature/SubmitResourceTest.php index 4445783..09d8ac8 100644 --- a/tests/Feature/SubmitResourceTest.php +++ b/tests/Feature/SubmitResourceTest.php @@ -1,25 +1,58 @@ create([ + 'name_ro' => 'Cluj', + 'name_en' => 'Cluj', + ]); + $category = ResourceCategory::factory()->create([ + 'name_ro' => 'Prevenire și educație', + 'name_en' => 'Prevention and education', + ]); + Livewire::test('pages::submit') ->set('title', 'Professional safety guide') ->set('description', 'A practical guide for child protection teams.') ->set('type', 'guide') - ->set('category', 'Prevention') + ->set('category', $category->name_en) + ->set('language', 'en') + ->set('createdOn', '2026-03-01') + ->set('targetAudience', 'Social workers') + ->set('tags', 'safety, reporting') ->set('submitterName', 'Ana Pop') ->set('submitterEmail', 'ana@example.com') ->set('submitterOrganization', 'Example NGO') + ->set('organizationWebsite', 'https://example.org') + ->set('counties', [$county->name_ro]) + ->set('files', [UploadedFile::fake()->create('guide.pdf', 100, 'application/pdf')]) ->set('rightsConfirmed', true) ->set('reviewConfirmed', true) ->call('submit') ->assertHasNoErrors() ->assertSet('submitted', true); - expect(ResourceSubmission::query()->where('submitter_email', 'ana@example.com')->exists())->toBeTrue(); + $submission = ResourceSubmission::query()->where('submitter_email', 'ana@example.com')->first(); + + expect($submission)->not->toBeNull() + ->and($submission->counties)->toBe(['Cluj']) + ->and($submission->tags)->toBe(['safety', 'reporting']) + ->and($submission->file_paths)->toHaveCount(1); + + Storage::disk('local')->assertExists($submission->file_paths[0]); +}); + +it('shows the submission guidelines', function () { + $this->get('/ro/submit') + ->assertSuccessful() + ->assertSee('interpretare în limbaj semne', false) + ->assertSee('Materiale Online/Social-media', false); });