Added Feedback section.
This commit is contained in:
parent
491249c8d8
commit
8c353a49b7
10
app/application/app/Dto/Builder/ProjectFeedback.php
Normal file
10
app/application/app/Dto/Builder/ProjectFeedback.php
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Dto\Builder;
|
||||||
|
|
||||||
|
final readonly class ProjectFeedback
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
|
||||||
|
) { }
|
||||||
|
}
|
21
app/application/app/Dto/HttpUserData.php
Normal file
21
app/application/app/Dto/HttpUserData.php
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Dto;
|
||||||
|
|
||||||
|
final readonly class HttpUserData
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private ?string $clientIp = null,
|
||||||
|
private ?string $userAgent = null,
|
||||||
|
) { }
|
||||||
|
|
||||||
|
public function getClientIp(): ?string
|
||||||
|
{
|
||||||
|
return $this->clientIp;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getUserAgent(): ?string
|
||||||
|
{
|
||||||
|
return $this->userAgent;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,21 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Dto\Service\Admin\Project\Feedback;
|
||||||
|
|
||||||
|
use App\Dto\Builder\ProjectFeedback;
|
||||||
|
use App\Dto\Service\Pages;
|
||||||
|
|
||||||
|
final readonly class Index extends Pages
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private ProjectFeedback $projectFeedbackBuilderDto,
|
||||||
|
int $page
|
||||||
|
) {
|
||||||
|
parent::__construct($page);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getProjectFeedbackBuilderDto(): ProjectFeedback
|
||||||
|
{
|
||||||
|
return $this->projectFeedbackBuilderDto;
|
||||||
|
}
|
||||||
|
}
|
@ -30,6 +30,14 @@ public static function getTranslationCodes(): array
|
|||||||
'site.Choose language',
|
'site.Choose language',
|
||||||
'site.Page without translation',
|
'site.Page without translation',
|
||||||
'site.Project',
|
'site.Project',
|
||||||
|
'site.Feedback',
|
||||||
|
'site.Feedback-send',
|
||||||
|
'site.required field',
|
||||||
|
'site.attributes.name',
|
||||||
|
'site.attributes.email',
|
||||||
|
'site.attributes.message',
|
||||||
|
'site.Message sent successfully',
|
||||||
|
'Server Error',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
36
app/application/app/Dto/Service/Site/Feedback/Send.php
Normal file
36
app/application/app/Dto/Service/Site/Feedback/Send.php
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Dto\Service\Site\Feedback;
|
||||||
|
|
||||||
|
use App\Dto\HttpUserData;
|
||||||
|
use App\Dto\Service\Dto;
|
||||||
|
|
||||||
|
final readonly class Send extends Dto
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private string $message,
|
||||||
|
private HttpUserData $httpUserData,
|
||||||
|
private ?string $name,
|
||||||
|
private ?string $email,
|
||||||
|
) { }
|
||||||
|
|
||||||
|
public function getName(): ?string
|
||||||
|
{
|
||||||
|
return $this->name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getEmail(): ?string
|
||||||
|
{
|
||||||
|
return $this->email;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getMessage(): string
|
||||||
|
{
|
||||||
|
return $this->message;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getHttpUserData(): HttpUserData
|
||||||
|
{
|
||||||
|
return $this->httpUserData;
|
||||||
|
}
|
||||||
|
}
|
@ -11,6 +11,7 @@ enum Permission: string
|
|||||||
case ProjectContent = 'project-content';
|
case ProjectContent = 'project-content';
|
||||||
case ProjectLink = 'project-link';
|
case ProjectLink = 'project-link';
|
||||||
case ProjectTranslation = 'project-translation';
|
case ProjectTranslation = 'project-translation';
|
||||||
|
case ProjectFeedback = 'project-feedback';
|
||||||
|
|
||||||
public function getPermissions(): array
|
public function getPermissions(): array
|
||||||
{
|
{
|
||||||
@ -27,6 +28,9 @@ public function getPermissions(): array
|
|||||||
'view' => __('permissions.Allowed to watch'),
|
'view' => __('permissions.Allowed to watch'),
|
||||||
'update' => __('permissions.Allowed to edit'),
|
'update' => __('permissions.Allowed to edit'),
|
||||||
],
|
],
|
||||||
|
self::ProjectFeedback => [
|
||||||
|
'view' => __('permissions.Allowed to watch'),
|
||||||
|
],
|
||||||
default => $this->getBasePermissions()
|
default => $this->getBasePermissions()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -8,6 +8,8 @@
|
|||||||
enum ProjectSection
|
enum ProjectSection
|
||||||
{
|
{
|
||||||
case Home;
|
case Home;
|
||||||
|
case Feedback;
|
||||||
|
case FeedbackSend;
|
||||||
|
|
||||||
public function url(Project $project, ?ProjectLanguage $language = null): string
|
public function url(Project $project, ?ProjectLanguage $language = null): string
|
||||||
{
|
{
|
||||||
@ -26,6 +28,9 @@ public function url(Project $project, ?ProjectLanguage $language = null): string
|
|||||||
|
|
||||||
$route = match ($this) {
|
$route = match ($this) {
|
||||||
self::Home => \route($prefixProject . 'home' . $prefixLanguage, $parameters, false),
|
self::Home => \route($prefixProject . 'home' . $prefixLanguage, $parameters, false),
|
||||||
|
|
||||||
|
self::Feedback => \route($prefixProject . 'feedback' . $prefixLanguage, $parameters, false),
|
||||||
|
self::FeedbackSend => \route($prefixProject . 'feedback.send' . $prefixLanguage, $parameters, false),
|
||||||
};
|
};
|
||||||
|
|
||||||
return $project->http_host . $route;
|
return $project->http_host . $route;
|
||||||
|
@ -0,0 +1,52 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Admin\Projects;
|
||||||
|
|
||||||
|
use App\Dto\QuerySettingsDto;
|
||||||
|
use App\Http\Controllers\Admin\Controller;
|
||||||
|
use App\Http\Requests\Admin\Projects\Feedbacks\IndexRequest;
|
||||||
|
use App\Services\Admin\Project\FeedbackService;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
|
final class FeedbacksController extends Controller
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly FeedbackService $feedbackService,
|
||||||
|
) { }
|
||||||
|
|
||||||
|
public function index(IndexRequest $request): View
|
||||||
|
{
|
||||||
|
$user = $request->user();
|
||||||
|
$data = $request->getDto();
|
||||||
|
$querySettingsDto = new QuerySettingsDto(
|
||||||
|
limit: 20,
|
||||||
|
page: $data->getPage(),
|
||||||
|
queryWith: ['project']
|
||||||
|
);
|
||||||
|
|
||||||
|
$result = $this->feedbackService->index($data->getProjectFeedbackBuilderDto(), $querySettingsDto, $user);
|
||||||
|
if ($result->isError()) {
|
||||||
|
$this->errors($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
return view('admin.projects.feedbacks.index', $result->getData());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function project(int $projectId, IndexRequest $request): View
|
||||||
|
{
|
||||||
|
$user = $request->user();
|
||||||
|
$data = $request->getDto();
|
||||||
|
$querySettingsDto = new QuerySettingsDto(
|
||||||
|
limit: 20,
|
||||||
|
page: $data->getPage(),
|
||||||
|
queryWith: []
|
||||||
|
);
|
||||||
|
|
||||||
|
$result = $this->feedbackService->project($projectId, $data->getProjectFeedbackBuilderDto(), $querySettingsDto, $user);
|
||||||
|
if ($result->isError()) {
|
||||||
|
$this->errors($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
return view('admin.projects.feedbacks.project', $result->getData());
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,41 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Site;
|
||||||
|
|
||||||
|
use App\Http\Requests\Site\Feedback\SendRequest;
|
||||||
|
use App\Models\ProjectFeedback;
|
||||||
|
use App\Services\Site\FeedbackService;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
|
final class FeedbackController extends Controller
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly FeedbackService $feedbackService,
|
||||||
|
) { }
|
||||||
|
|
||||||
|
public function index(Request $request): View
|
||||||
|
{
|
||||||
|
return view('site.feedback.index', [
|
||||||
|
'project' => $request->get('project'),
|
||||||
|
'websiteTranslations' => $request->get('websiteTranslations'),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function send(SendRequest $request): RedirectResponse
|
||||||
|
{
|
||||||
|
$project = $request->get('project');
|
||||||
|
$websiteTranslations = $request->get('websiteTranslations');
|
||||||
|
$user = $request->user();
|
||||||
|
$data = $request->getDto();
|
||||||
|
|
||||||
|
$result = $this->feedbackService->send($data, $project, $websiteTranslations, $user);
|
||||||
|
if ($result->isError()) {
|
||||||
|
return redirect()->back()->withInput()->withErrors($result->getErrorsOrMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
$url = \App\Enums\Site\ProjectSection::Feedback->url($project, $websiteTranslations->getLanguage());
|
||||||
|
return redirect($url)->withSuccess($result->getMessage());
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,29 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Http\Requests\Admin\Projects\Feedbacks;
|
||||||
|
|
||||||
|
use App\Contracts\FormRequestDto;
|
||||||
|
use App\Dto\Builder\ProjectFeedback;
|
||||||
|
use App\Dto\Service\Admin\Project\Feedback\Index;
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
final class IndexRequest extends FormRequest implements FormRequestDto
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Get the validation rules that apply to the request.
|
||||||
|
*/
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'page' => ['nullable', 'numeric', 'min:1']
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDto(): Index
|
||||||
|
{
|
||||||
|
return new Index(
|
||||||
|
projectFeedbackBuilderDto: new ProjectFeedback(),
|
||||||
|
page: (int) $this->input('page', 1),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,57 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Http\Requests\Site\Feedback;
|
||||||
|
|
||||||
|
use App\Contracts\FormRequestDto;
|
||||||
|
use App\Dto\HttpUserData;
|
||||||
|
use App\Dto\Service\Site\Feedback\Send;
|
||||||
|
use App\Services\WebsiteTranslations;
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
final class SendRequest extends FormRequest implements FormRequestDto
|
||||||
|
{
|
||||||
|
public function attributes(): array
|
||||||
|
{
|
||||||
|
$attributes = [];
|
||||||
|
|
||||||
|
$websiteTranslations = $this->request->get('websiteTranslations', null);
|
||||||
|
if ($websiteTranslations) {
|
||||||
|
/** @var WebsiteTranslations $websiteTranslations */
|
||||||
|
$attributes = [
|
||||||
|
'name' => $websiteTranslations->translate('site.attributes.name'),
|
||||||
|
'email' => $websiteTranslations->translate('site.attributes.email'),
|
||||||
|
'message' => $websiteTranslations->translate('site.attributes.message'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $attributes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the validation rules that apply to the request.
|
||||||
|
*/
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'name' => ['nullable', 'string', 'max:255'],
|
||||||
|
'email' => ['nullable', 'string', 'max:255', 'email'],
|
||||||
|
'message' => ['required', 'string', 'max:5000'],
|
||||||
|
'captcha-verified' => ['captcha'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDto(): Send
|
||||||
|
{
|
||||||
|
$httpUserData = new HttpUserData(
|
||||||
|
clientIp: $this->getClientIp(),
|
||||||
|
userAgent: $this->userAgent(),
|
||||||
|
);
|
||||||
|
|
||||||
|
return new Send(
|
||||||
|
message: $this->input('message'),
|
||||||
|
httpUserData: $httpUserData,
|
||||||
|
name: $this->input('name', null),
|
||||||
|
email: $this->input('email', null),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -57,4 +57,9 @@ public function links(): HasMany
|
|||||||
{
|
{
|
||||||
return $this->hasMany(ProjectLink::class);
|
return $this->hasMany(ProjectLink::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function feedbacks(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(ProjectFeedback::class);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
52
app/application/app/Models/ProjectFeedback.php
Normal file
52
app/application/app/Models/ProjectFeedback.php
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use App\Models\Scopes\CreatedScope;
|
||||||
|
use Illuminate\Database\Eloquent\Attributes\ScopedBy;
|
||||||
|
use Illuminate\Database\Eloquent\Concerns\HasUuids;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||||
|
|
||||||
|
#[ScopedBy([CreatedScope::class])]
|
||||||
|
final class ProjectFeedback extends Model
|
||||||
|
{
|
||||||
|
use HasUuids, HasFactory, SoftDeletes;
|
||||||
|
|
||||||
|
protected $table = 'project_feedback';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The attributes that are mass assignable.
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $fillable = [
|
||||||
|
'name',
|
||||||
|
'email',
|
||||||
|
'message',
|
||||||
|
'ip',
|
||||||
|
'user_agent',
|
||||||
|
'user_id',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the attributes that should be cast.
|
||||||
|
*
|
||||||
|
* @return array<string, string>
|
||||||
|
*/
|
||||||
|
protected function casts(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'name' => 'encrypted',
|
||||||
|
'email' => 'encrypted',
|
||||||
|
'message' => 'encrypted',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function project(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Project::class);
|
||||||
|
}
|
||||||
|
}
|
18
app/application/app/Models/Scopes/CreatedScope.php
Normal file
18
app/application/app/Models/Scopes/CreatedScope.php
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Models\Scopes;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Scope;
|
||||||
|
|
||||||
|
class CreatedScope implements Scope
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Apply the scope to a given Eloquent query builder.
|
||||||
|
*/
|
||||||
|
public function apply(Builder $builder, Model $model): void
|
||||||
|
{
|
||||||
|
$builder->orderBy('created_at', 'desc');
|
||||||
|
}
|
||||||
|
}
|
19
app/application/app/Policies/ProjectFeedbackPolicy.php
Normal file
19
app/application/app/Policies/ProjectFeedbackPolicy.php
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Policies;
|
||||||
|
|
||||||
|
use App\Models\ProjectFeedback;
|
||||||
|
use App\Models\User;
|
||||||
|
|
||||||
|
final readonly class ProjectFeedbackPolicy extends Policy
|
||||||
|
{
|
||||||
|
public function viewAny(User $user): bool
|
||||||
|
{
|
||||||
|
return $user->hasPermission('project-feedback.view');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function view(User $user, ProjectFeedback $projectFeedback): bool
|
||||||
|
{
|
||||||
|
return $user->hasPermission('project-feedback.view');
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,38 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Repositories;
|
||||||
|
|
||||||
|
use App\Contracts\Search;
|
||||||
|
use App\Dto\Builder\ProjectFeedback as ProjectFeedbackBuilderDto;
|
||||||
|
use App\Models\Project;
|
||||||
|
use App\Models\ProjectFeedback;
|
||||||
|
use App\Services\ProjectFeedback\BuilderCommand;
|
||||||
|
use App\Services\Search\CreateSearchInstanceCommand;
|
||||||
|
|
||||||
|
final readonly class ProjectFeedbackRepository
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private CreateSearchInstanceCommand $createSearchInstanceCommand,
|
||||||
|
private BuilderCommand $builderCommand
|
||||||
|
) { }
|
||||||
|
|
||||||
|
public function getFeedbacks(ProjectFeedbackBuilderDto $projectFeedbackBuilderDto, array $with = []): Search
|
||||||
|
{
|
||||||
|
$query = $this->builderCommand->execute(
|
||||||
|
query: ProjectFeedback::query()->with($with),
|
||||||
|
projectFeedbackBuilderDto: $projectFeedbackBuilderDto
|
||||||
|
);
|
||||||
|
|
||||||
|
return $this->createSearchInstanceCommand->execute($query);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getFeedbacksByProject(Project $project, ProjectFeedbackBuilderDto $projectFeedbackBuilderDto, array $with = []): Search
|
||||||
|
{
|
||||||
|
$query = $this->builderCommand->execute(
|
||||||
|
query: $project->feedbacks()->with($with),
|
||||||
|
projectFeedbackBuilderDto: $projectFeedbackBuilderDto
|
||||||
|
);
|
||||||
|
|
||||||
|
return $this->createSearchInstanceCommand->execute($query);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,66 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Services\Admin\Project;
|
||||||
|
|
||||||
|
use App\Dto\Builder\ProjectFeedback as ProjectFeedbackBuilderDto;
|
||||||
|
use App\Dto\QuerySettingsDto;
|
||||||
|
use App\Models\ProjectFeedback;
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Repositories\ProjectFeedbackRepository;
|
||||||
|
use App\Repositories\ProjectRepository;
|
||||||
|
use App\ServiceResults\ServiceResultArray;
|
||||||
|
use App\ServiceResults\ServiceResultError;
|
||||||
|
use App\Services\Service;
|
||||||
|
|
||||||
|
final class FeedbackService extends Service
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly ProjectFeedbackRepository $projectFeedbackRepository,
|
||||||
|
private readonly ProjectRepository $projectRepository,
|
||||||
|
) { }
|
||||||
|
|
||||||
|
public function index(ProjectFeedbackBuilderDto $projectFeedbackBuilderDto, QuerySettingsDto $querySettingsDto, User $user): ServiceResultError | ServiceResultArray
|
||||||
|
{
|
||||||
|
if ($user->cannot('viewAny', ProjectFeedback::class)) {
|
||||||
|
return $this->errFobidden(__('Access is denied'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$feedbacks = $this->projectFeedbackRepository->getFeedbacks(
|
||||||
|
$projectFeedbackBuilderDto,
|
||||||
|
$querySettingsDto->getQueryWith()
|
||||||
|
)->pagination(
|
||||||
|
$querySettingsDto->getLimit(),
|
||||||
|
$querySettingsDto->getPage()
|
||||||
|
);
|
||||||
|
|
||||||
|
return $this->result([
|
||||||
|
'feedbacks' => $feedbacks,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function project(int $projectId, ProjectFeedbackBuilderDto $projectFeedbackBuilderDto, QuerySettingsDto $querySettingsDto, User $user): ServiceResultError | ServiceResultArray
|
||||||
|
{
|
||||||
|
$project = $this->projectRepository->getProjectById($projectId);
|
||||||
|
if (\is_null($project)) {
|
||||||
|
return $this->errNotFound(__('Not Found'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($user->cannot('viewAny', ProjectFeedback::class)) {
|
||||||
|
return $this->errFobidden(__('Access is denied'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$feedbacks = $this->projectFeedbackRepository->getFeedbacksByProject(
|
||||||
|
$project,
|
||||||
|
$projectFeedbackBuilderDto,
|
||||||
|
$querySettingsDto->getQueryWith()
|
||||||
|
)->pagination(
|
||||||
|
$querySettingsDto->getLimit(),
|
||||||
|
$querySettingsDto->getPage()
|
||||||
|
);
|
||||||
|
|
||||||
|
return $this->result([
|
||||||
|
'project' => $project,
|
||||||
|
'feedbacks' => $feedbacks,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,15 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Services\ProjectFeedback;
|
||||||
|
|
||||||
|
use App\Dto\Builder\ProjectFeedback as ProjectFeedbackBuilderDto;
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\Relation;
|
||||||
|
|
||||||
|
final readonly class BuilderCommand
|
||||||
|
{
|
||||||
|
public function execute(Relation | Builder $query, ProjectFeedbackBuilderDto $projectFeedbackBuilderDto): Relation | Builder
|
||||||
|
{
|
||||||
|
return $query;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,15 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Services\ProjectFeedback;
|
||||||
|
|
||||||
|
use App\Models\Project;
|
||||||
|
use App\Models\ProjectFeedback;
|
||||||
|
|
||||||
|
final readonly class ProjectFeedbackCommandHandler
|
||||||
|
{
|
||||||
|
public function handleStore(Project $project, array $data): ProjectFeedback
|
||||||
|
{
|
||||||
|
$feedback = $project->feedbacks()->create($data);
|
||||||
|
return $feedback;
|
||||||
|
}
|
||||||
|
}
|
53
app/application/app/Services/Site/FeedbackService.php
Normal file
53
app/application/app/Services/Site/FeedbackService.php
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Services\Site;
|
||||||
|
|
||||||
|
use App\Dto\Service\Site\Feedback\Send;
|
||||||
|
use App\Models\Project;
|
||||||
|
use App\Models\User;
|
||||||
|
use App\ServiceResults\ServiceResultError;
|
||||||
|
use App\ServiceResults\ServiceResultSuccess;
|
||||||
|
use App\Services\ProjectFeedback\ProjectFeedbackCommandHandler;
|
||||||
|
use App\Services\Service;
|
||||||
|
use App\Services\WebsiteTranslations;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
final class FeedbackService extends Service
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly ProjectFeedbackCommandHandler $feedbackCommandHandler,
|
||||||
|
) { }
|
||||||
|
|
||||||
|
public function send(Send $send, Project $project, WebsiteTranslations $websiteTranslations, ?User $user = null): ServiceResultError | ServiceResultSuccess
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
DB::transaction(function () use ($send, $project, $user) {
|
||||||
|
$data = $this->getDataFeedback($send);
|
||||||
|
$data['user_id'] = $user?->id;
|
||||||
|
$this->feedbackCommandHandler->handleStore($project, $data);
|
||||||
|
});
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
report($e);
|
||||||
|
return $this->errService($websiteTranslations->translate('Server Error'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->ok($websiteTranslations->translate('site.Message sent successfully'));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getDataFeedback(Send $send): array
|
||||||
|
{
|
||||||
|
$name = $send->getName();
|
||||||
|
if ($name !== null) {
|
||||||
|
$name = strip_tags($name);
|
||||||
|
}
|
||||||
|
$message = $send->getMessage();
|
||||||
|
$message = strip_tags($message, ['<b>', '<strong>', '<p>', '<br>', '<ul>', '<li>', '<ol>', '<table>', '<tr>', '<td>', '<th>', '<i>',]);
|
||||||
|
return [
|
||||||
|
'name' => $name,
|
||||||
|
'email' => $send->getEmail(),
|
||||||
|
'message' => $message,
|
||||||
|
'ip' => $send->getHttpUserData()->getClientIp(),
|
||||||
|
'user_agent' => $send->getHttpUserData()->getUserAgent(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
26
app/application/app/View/Components/Site/Forms/Form.php
Normal file
26
app/application/app/View/Components/Site/Forms/Form.php
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\View\Components\Site\Forms;
|
||||||
|
|
||||||
|
use App\Helpers\Helpers;
|
||||||
|
use Illuminate\View\Component;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
|
abstract class Form extends Component
|
||||||
|
{
|
||||||
|
private ?string $requestName = null;
|
||||||
|
|
||||||
|
abstract protected function getName(): string;
|
||||||
|
abstract public function render(): View;
|
||||||
|
|
||||||
|
protected function getRequestName(): string
|
||||||
|
{
|
||||||
|
if (!is_null($this->requestName)) {
|
||||||
|
return $this->requestName;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->requestName = Helpers::formatAttributeNameToRequestName($this->getName());
|
||||||
|
|
||||||
|
return $this->requestName;
|
||||||
|
}
|
||||||
|
}
|
58
app/application/app/View/Components/Site/Forms/Input.php
Normal file
58
app/application/app/View/Components/Site/Forms/Input.php
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\View\Components\Site\Forms;
|
||||||
|
|
||||||
|
use App\Services\WebsiteTranslations;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
|
final class Input extends Form
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly WebsiteTranslations $websiteTranslations,
|
||||||
|
private readonly string $title,
|
||||||
|
private readonly string $name,
|
||||||
|
private readonly string $type = 'text',
|
||||||
|
private readonly ?string $value = '',
|
||||||
|
) { }
|
||||||
|
|
||||||
|
protected function getName(): string
|
||||||
|
{
|
||||||
|
return $this->name;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getTitle(): string
|
||||||
|
{
|
||||||
|
return Str::ucfirst($this->title);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getType(): string
|
||||||
|
{
|
||||||
|
return $this->type;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getValue(): string
|
||||||
|
{
|
||||||
|
return (string) old($this->getRequestName(), $this->value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getWebsiteTranslations(): WebsiteTranslations
|
||||||
|
{
|
||||||
|
return $this->websiteTranslations;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritDoc
|
||||||
|
*/
|
||||||
|
public function render(): View
|
||||||
|
{
|
||||||
|
return view('components.site.forms.input', [
|
||||||
|
'websiteTranslations' => $this->getWebsiteTranslations(),
|
||||||
|
'title' => $this->getTitle(),
|
||||||
|
'name' => $this->getName(),
|
||||||
|
'requestName' => $this->getRequestName(),
|
||||||
|
'type' => $this->getType(),
|
||||||
|
'value' => $this->getValue(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,54 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\View\Components\Site\Forms;
|
||||||
|
|
||||||
|
use App\Services\WebsiteTranslations;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
|
final class TextareaWysiwyg extends Form
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly WebsiteTranslations $websiteTranslations,
|
||||||
|
private readonly string $title,
|
||||||
|
private readonly string $name,
|
||||||
|
private readonly ?string $value = '',
|
||||||
|
) { }
|
||||||
|
|
||||||
|
protected function getName(): string
|
||||||
|
{
|
||||||
|
return $this->name;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getTitle(): string
|
||||||
|
{
|
||||||
|
return Str::ucfirst($this->title);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getValue(): string
|
||||||
|
{
|
||||||
|
return (string) old($this->getRequestName(), $this->value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getWebsiteTranslations(): WebsiteTranslations
|
||||||
|
{
|
||||||
|
return $this->websiteTranslations;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritDoc
|
||||||
|
*/
|
||||||
|
public function render(): View
|
||||||
|
{
|
||||||
|
$tinymceLicenseKey = config('tinymce.license_key');
|
||||||
|
|
||||||
|
return view('components.site.forms.textarea-wysiwyg', [
|
||||||
|
'websiteTranslations' => $this->getWebsiteTranslations(),
|
||||||
|
'tinymceLicenseKey' => $tinymceLicenseKey,
|
||||||
|
'title' => $this->getTitle(),
|
||||||
|
'name' => $this->getName(),
|
||||||
|
'requestName' => $this->getRequestName(),
|
||||||
|
'value' => $this->getValue(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,40 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('project_feedback', function (Blueprint $table) {
|
||||||
|
$table->uuid('id')->primary();
|
||||||
|
$table->unsignedBigInteger('project_id')->index();
|
||||||
|
$table->foreign('project_id')->references('id')->on('projects');
|
||||||
|
$table->unsignedBigInteger('user_id')->nullable()->index();
|
||||||
|
$table->foreign('user_id')->references('id')->on('users');
|
||||||
|
$table->text('name')->nullable();
|
||||||
|
$table->text('email')->nullable();
|
||||||
|
$table->text('message');
|
||||||
|
$table->ipAddress('ip')->nullable();
|
||||||
|
$table->string('user_agent')->nullable();
|
||||||
|
$table->timestamps();
|
||||||
|
$table->softDeletes();
|
||||||
|
$table->index(['created_at']);
|
||||||
|
$table->index(['project_id', 'deleted_at']);
|
||||||
|
$table->index(['user_id', 'deleted_at']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('project_feedback');
|
||||||
|
}
|
||||||
|
};
|
@ -12,4 +12,5 @@
|
|||||||
'Last update' => 'Last update',
|
'Last update' => 'Last update',
|
||||||
'Links project' => 'Links from the project',
|
'Links project' => 'Links from the project',
|
||||||
'Translations' => 'Translations',
|
'Translations' => 'Translations',
|
||||||
|
'Feedback' => 'Feedback',
|
||||||
];
|
];
|
||||||
|
@ -16,4 +16,5 @@
|
|||||||
'ProjectContent' => 'About the project',
|
'ProjectContent' => 'About the project',
|
||||||
'ProjectLink' => 'Links from the project',
|
'ProjectLink' => 'Links from the project',
|
||||||
'ProjectTranslation' => 'Translations',
|
'ProjectTranslation' => 'Translations',
|
||||||
|
'ProjectFeedback' => 'Feedback',
|
||||||
];
|
];
|
||||||
|
@ -7,4 +7,13 @@
|
|||||||
'Page without translation' => 'Page without translation',
|
'Page without translation' => 'Page without translation',
|
||||||
'Powered by service' => 'Powered by the My Projects website engine',
|
'Powered by service' => 'Powered by the My Projects website engine',
|
||||||
'Choose language' => 'Choose language',
|
'Choose language' => 'Choose language',
|
||||||
|
'Feedback' => 'Feedback',
|
||||||
|
'Feedback-send' => 'Send review',
|
||||||
|
'required field' => 'required field',
|
||||||
|
'Message sent successfully' => 'Message sent successfully',
|
||||||
|
'attributes' => [
|
||||||
|
'name' => 'name',
|
||||||
|
'email' => 'email',
|
||||||
|
'message' => 'message',
|
||||||
|
],
|
||||||
];
|
];
|
||||||
|
@ -12,4 +12,5 @@
|
|||||||
'Last update' => 'Последнее обновление',
|
'Last update' => 'Последнее обновление',
|
||||||
'Links project' => 'Ссылки от проекта',
|
'Links project' => 'Ссылки от проекта',
|
||||||
'Translations' => 'Переводы',
|
'Translations' => 'Переводы',
|
||||||
|
'Feedback' => 'Обратная связь',
|
||||||
];
|
];
|
||||||
|
@ -16,4 +16,5 @@
|
|||||||
'ProjectContent' => 'О проекте',
|
'ProjectContent' => 'О проекте',
|
||||||
'ProjectLink' => 'Ссылки от проекта',
|
'ProjectLink' => 'Ссылки от проекта',
|
||||||
'ProjectTranslation' => 'Переводы',
|
'ProjectTranslation' => 'Переводы',
|
||||||
|
'ProjectFeedback' => 'Обратная связь',
|
||||||
];
|
];
|
||||||
|
@ -7,4 +7,13 @@
|
|||||||
'Page without translation' => 'Страница без перевода',
|
'Page without translation' => 'Страница без перевода',
|
||||||
'Powered by service' => 'Работает на движке сайта «Мои проекты»',
|
'Powered by service' => 'Работает на движке сайта «Мои проекты»',
|
||||||
'Choose language' => 'Выберите язык',
|
'Choose language' => 'Выберите язык',
|
||||||
|
'Feedback' => 'Обратная связь',
|
||||||
|
'Feedback-send' => 'Отправить отзыв',
|
||||||
|
'required field' => 'обязательное поле',
|
||||||
|
'Message sent successfully' => 'Сообщение успешно отправлено',
|
||||||
|
'attributes' => [
|
||||||
|
'name' => 'имя',
|
||||||
|
'email' => 'email',
|
||||||
|
'message' => 'сообщение',
|
||||||
|
],
|
||||||
];
|
];
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
@import url('https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,400;0,700;1,400;1,700&display=swap');
|
@import url('https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,400;0,700;1,400;1,700&display=swap');
|
||||||
@import "reset";
|
@import "reset";
|
||||||
|
@import "forms";
|
||||||
|
|
||||||
html,
|
html,
|
||||||
body {
|
body {
|
||||||
|
86
app/application/resources/site/scss/forms.scss
Normal file
86
app/application/resources/site/scss/forms.scss
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
.form-block {
|
||||||
|
padding: 10px 0;
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
padding-bottom: 7px;
|
||||||
|
color: #000;
|
||||||
|
|
||||||
|
span {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 400;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control.is-invalid {
|
||||||
|
border-color: #dc3545;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control:focus,
|
||||||
|
.form-control:focus-visible {
|
||||||
|
outline: 2px solid #006ce7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invalid-feedback {
|
||||||
|
display: block;
|
||||||
|
padding: 7px 10px;
|
||||||
|
color: #dc3545;
|
||||||
|
font-size: 14px;
|
||||||
|
border: 1px solid #dc3545;
|
||||||
|
border-radius: 6px;
|
||||||
|
margin-top: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
background: #006ce7;
|
||||||
|
border: 1px solid #002452;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 16px;
|
||||||
|
padding: 14px 24px;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.button:hover {
|
||||||
|
opacity: 0.8;
|
||||||
|
border-color: #006ce7;
|
||||||
|
color: #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.captcha-service-kor-elf {
|
||||||
|
position: relative;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert {
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 16px;
|
||||||
|
padding: 14px 24px;
|
||||||
|
|
||||||
|
ul {
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.alert.alert-success {
|
||||||
|
border: 1px solid #0a6f4d;
|
||||||
|
color: #0a6f4d;
|
||||||
|
}
|
||||||
|
.alert.alert-danger {
|
||||||
|
border: 1px solid #dc3545;
|
||||||
|
color: #dc3545;
|
||||||
|
}
|
@ -26,6 +26,22 @@
|
|||||||
</li>
|
</li>
|
||||||
@endcan
|
@endcan
|
||||||
|
|
||||||
|
@can('viewAny', \App\Models\ProjectFeedback::class)
|
||||||
|
<li @class([
|
||||||
|
'nav-item',
|
||||||
|
'active' => request()->route()->named('admin.feedbacks.*'),
|
||||||
|
])>
|
||||||
|
<a href="{{ route('admin.feedbacks.index') }}" class="nav-link">
|
||||||
|
<span class="sidebar-icon">
|
||||||
|
<svg class="icon icon-xs me-2" data-slot="icon" fill="none" stroke-width="1.5" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M8.625 12a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Zm0 0H8.25m4.125 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Zm0 0H12m4.125 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Zm0 0h-.375M21 12c0 4.556-4.03 8.25-9 8.25a9.764 9.764 0 0 1-2.555-.337A5.972 5.972 0 0 1 5.41 20.97a5.969 5.969 0 0 1-.474-.065 4.48 4.48 0 0 0 .978-2.025c.09-.457-.133-.901-.467-1.226C3.93 16.178 3 14.189 3 12c0-4.556 4.03-8.25 9-8.25s9 3.694 9 8.25Z"></path>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
<span class="sidebar-text">{{ __('admin-sections.Feedback') }}</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
@endcan
|
||||||
|
|
||||||
@can('viewAny', \App\Models\User::class)
|
@can('viewAny', \App\Models\User::class)
|
||||||
<li @class([
|
<li @class([
|
||||||
'nav-item',
|
'nav-item',
|
||||||
|
@ -0,0 +1,37 @@
|
|||||||
|
@section('meta_title', __('admin-sections.Feedback'))
|
||||||
|
@section('h1', __('admin-sections.Feedback'))
|
||||||
|
<x-admin.layout>
|
||||||
|
<div class="card border-0 shadow mb-4">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-centered table-nowrap mb-0 rounded">
|
||||||
|
<thead class="thead-light">
|
||||||
|
<tr>
|
||||||
|
<th class="border-0">{{ __('admin-sections.Project') }}</th>
|
||||||
|
<th class="border-0" style="width: 100%;">{{ __('site.attributes.message') }}</th>
|
||||||
|
<th class="border-0">{{ __('site.attributes.name') }}</th>
|
||||||
|
<th class="border-0">{{ __('site.attributes.email') }}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@foreach($feedbacks as $feedback)
|
||||||
|
<tr>
|
||||||
|
<td>{{ $feedback->project->name }}</td>
|
||||||
|
<td>{!! $feedback->message !!}</td>
|
||||||
|
<td>
|
||||||
|
<p>{{ $feedback->name }}</p>
|
||||||
|
<p><strong>IP:</strong> {{ $feedback->ip }}</p>
|
||||||
|
<p><strong>UserAgent:</strong> {{ $feedback->user_agent }}</p>
|
||||||
|
</td>
|
||||||
|
<td>{{ $feedback->email }}</td>
|
||||||
|
</tr>
|
||||||
|
@endforeach
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<div class="card-footer border-0">
|
||||||
|
{{ $feedbacks->links() }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</x-admin.layout>
|
@ -0,0 +1,36 @@
|
|||||||
|
@section('meta_title', __('admin-sections.Project') . ': ' . $project->name . ' - ' . __('admin-sections.Feedback'))
|
||||||
|
@section('h1', __('admin-sections.Project') . ': ' . $project->name)
|
||||||
|
<x-admin.layout>
|
||||||
|
<div class="card border-0 shadow mb-4">
|
||||||
|
<div class="card-body">
|
||||||
|
<h3 id="category" class="mb-4">{{ __('admin-sections.Feedback') }}</h3>
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-centered table-nowrap mb-0 rounded">
|
||||||
|
<thead class="thead-light">
|
||||||
|
<tr>
|
||||||
|
<th class="border-0" style="width: 100%;">{{ __('site.attributes.message') }}</th>
|
||||||
|
<th class="border-0">{{ __('site.attributes.name') }}</th>
|
||||||
|
<th class="border-0">{{ __('site.attributes.email') }}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@foreach($feedbacks as $feedback)
|
||||||
|
<tr>
|
||||||
|
<td>{!! $feedback->message !!}</td>
|
||||||
|
<td>
|
||||||
|
<p>{{ $feedback->name }}</p>
|
||||||
|
<p><strong>IP:</strong> {{ $feedback->ip }}</p>
|
||||||
|
<p><strong>UserAgent:</strong> {{ $feedback->user_agent }}</p>
|
||||||
|
</td>
|
||||||
|
<td>{{ $feedback->email }}</td>
|
||||||
|
</tr>
|
||||||
|
@endforeach
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<div class="card-footer border-0">
|
||||||
|
{{ $feedbacks->links() }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</x-admin.layout>
|
@ -48,6 +48,18 @@
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@endcan
|
@endcan
|
||||||
|
@can('viewAny', \App\Models\ProjectFeedback::class)
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<a href="{{ route('admin.projects.feedbacks.index', ['project' => $project->id]) }}" class="fw-bold">
|
||||||
|
<svg width="16" height="16" class="align-text-top" data-slot="icon" fill="none" stroke-width="1.5" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M8.625 12a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Zm0 0H8.25m4.125 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Zm0 0H12m4.125 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Zm0 0h-.375M21 12c0 4.556-4.03 8.25-9 8.25a9.764 9.764 0 0 1-2.555-.337A5.972 5.972 0 0 1 5.41 20.97a5.969 5.969 0 0 1-.474-.065 4.48 4.48 0 0 0 .978-2.025c.09-.457-.133-.901-.467-1.226C3.93 16.178 3 14.189 3 12c0-4.556 4.03-8.25 9-8.25s9 3.694 9 8.25Z"></path>
|
||||||
|
</svg>
|
||||||
|
{{ __('admin-sections.Feedback') }}
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@endcan
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
@ -0,0 +1,12 @@
|
|||||||
|
<div class="form-block">
|
||||||
|
<label for="form-input-{{ $requestName }}">
|
||||||
|
{{ $title }}
|
||||||
|
@if($attributes->get('required'))
|
||||||
|
<span>{{ $websiteTranslations->translate('site.required field') }}</span>
|
||||||
|
@endif
|
||||||
|
</label>
|
||||||
|
<input id="form-input-{{ $requestName }}" class="form-control @error($requestName) is-invalid @enderror" name="{{ $name }}" type="{{ $type }}" @if($type !== 'password') value="{{ $value }}" @endif {{ $attributes }}>
|
||||||
|
@error($requestName)
|
||||||
|
<span class="invalid-feedback">{{ $message }}</span>
|
||||||
|
@enderror
|
||||||
|
</div>
|
@ -0,0 +1,36 @@
|
|||||||
|
<div class="form-block">
|
||||||
|
<label for="form-textarea-wysiwyg-{{ $requestName }}">
|
||||||
|
{{ $title }}
|
||||||
|
@if($attributes->get('required'))
|
||||||
|
<span>{{ $websiteTranslations->translate('site.required field') }}</span>
|
||||||
|
@endif
|
||||||
|
</label>
|
||||||
|
<textarea class="form-control textarea-tinymce @error($requestName) is-invalid @enderror" name="{{ $name }}" id="form-textarea-wysiwyg-{{ $requestName }}" rows="3">{{ $value }}</textarea>
|
||||||
|
@error($requestName)
|
||||||
|
<span class="invalid-feedback">{{ $message }}</span>
|
||||||
|
@enderror
|
||||||
|
</div>
|
||||||
|
@pushOnce('scripts')
|
||||||
|
<script src="{{ asset('/build/tinymce/tinymce.min.js') }}" referrerpolicy="origin"></script>
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
tinymce.init({
|
||||||
|
selector: '.textarea-tinymce',
|
||||||
|
@if(in_array(app()->getLocale(), ['ru'], true))
|
||||||
|
language: '{{ app()->getLocale() }}',
|
||||||
|
@endif
|
||||||
|
license_key: '{{ $tinymceLicenseKey }}',
|
||||||
|
plugins: 'emoticons lists table',
|
||||||
|
toolbar: 'bold italic | bullist numlist table | emoticons',
|
||||||
|
menubar: false,
|
||||||
|
mobile: {
|
||||||
|
menubar: false,
|
||||||
|
plugins: 'emoticons lists table',
|
||||||
|
toolbar: 'bold italic bullist numlist table emoticons',
|
||||||
|
},
|
||||||
|
referrer_policy: 'origin',
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
@endpushonce
|
@ -37,11 +37,16 @@
|
|||||||
<div class="menu__title">{{ __('site.Menu') }}</div>
|
<div class="menu__title">{{ __('site.Menu') }}</div>
|
||||||
<ul>
|
<ul>
|
||||||
<li><a href="{{ \App\Enums\Site\ProjectSection::Home->url($project, $websiteTranslations->getLanguage()) }}" @class(['active' => request()->route()->named(['home', 'language.home', 'project.home', 'project.language.home'])])>{{ $websiteTranslations->translate('site.About project') }}</a></li>
|
<li><a href="{{ \App\Enums\Site\ProjectSection::Home->url($project, $websiteTranslations->getLanguage()) }}" @class(['active' => request()->route()->named(['home', 'language.home', 'project.home', 'project.language.home'])])>{{ $websiteTranslations->translate('site.About project') }}</a></li>
|
||||||
|
<li><a href="{{ \App\Enums\Site\ProjectSection::Feedback->url($project, $websiteTranslations->getLanguage()) }}" @class(['active' => request()->route()->named(['feedback', 'language.feedback', 'project.feedback', 'project.language.feedback'])])>{{ $websiteTranslations->translate('site.Feedback') }}</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
<div class="section-container">
|
<div class="section-container">
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<h1>@yield('h1', '')</h1>
|
<h1>@yield('h1', '')</h1>
|
||||||
|
|
||||||
|
@includeWhen($errors->any(), 'layout.site._errors', ['errors' => $errors->all()])
|
||||||
|
@includeWhen(Session::has('success'), 'layout.site._success', ['success' => Session::get('success')])
|
||||||
|
|
||||||
{{ $slot }}
|
{{ $slot }}
|
||||||
</div>
|
</div>
|
||||||
<footer class="footer">
|
<footer class="footer">
|
||||||
@ -51,5 +56,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@vite('resources/site/js/app.js')
|
@vite('resources/site/js/app.js')
|
||||||
|
@stack('scripts')
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@ -0,0 +1,7 @@
|
|||||||
|
<div class="alert alert-danger">
|
||||||
|
<ul>
|
||||||
|
@foreach($errors as $error)
|
||||||
|
<li>{{ $error }}</li>
|
||||||
|
@endforeach
|
||||||
|
</ul>
|
||||||
|
</div>
|
@ -0,0 +1,3 @@
|
|||||||
|
<div class="alert alert-success">
|
||||||
|
{{ $success }}
|
||||||
|
</div>
|
@ -0,0 +1,22 @@
|
|||||||
|
@section('meta_title', $websiteTranslations->translate('site.Feedback'))
|
||||||
|
@section('h1', $websiteTranslations->translate('site.Feedback'))
|
||||||
|
|
||||||
|
<x-site.layout :project="$project" :websiteTranslations="$websiteTranslations">
|
||||||
|
@if(Session::has('success') !== true)
|
||||||
|
<form method="post" action="{{ \App\Enums\Site\ProjectSection::FeedbackSend->url($project, $websiteTranslations->getLanguage()) }}">
|
||||||
|
@csrf
|
||||||
|
<x-site.forms.input :title="$websiteTranslations->translate('site.attributes.name')" :websiteTranslations="$websiteTranslations" name="name" type="text" value="" autofocus />
|
||||||
|
<x-site.forms.input :title="$websiteTranslations->translate('site.attributes.email')" :websiteTranslations="$websiteTranslations" name="email" type="text" value="" />
|
||||||
|
<x-site.forms.textarea-wysiwyg :title="$websiteTranslations->translate('site.attributes.message')" :websiteTranslations="$websiteTranslations" required name="message" value="" />
|
||||||
|
<div class="form-block">
|
||||||
|
@captcha
|
||||||
|
@error('captcha-verified')
|
||||||
|
<span class="invalid-feedback">{{ $message }}</span>
|
||||||
|
@enderror
|
||||||
|
</div>
|
||||||
|
<div class="form-block">
|
||||||
|
<button class="button" type="submit">{{ $websiteTranslations->translate('site.Feedback-send') }}</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
@endif
|
||||||
|
</x-site.layout>
|
@ -4,3 +4,10 @@
|
|||||||
|
|
||||||
Route::get('/', [\App\Http\Controllers\Site\ProjectsController::class, 'index'])->name('home');
|
Route::get('/', [\App\Http\Controllers\Site\ProjectsController::class, 'index'])->name('home');
|
||||||
Route::get('/language/{language}', [\App\Http\Controllers\Site\ProjectsController::class, 'index'])->name('home-language');
|
Route::get('/language/{language}', [\App\Http\Controllers\Site\ProjectsController::class, 'index'])->name('home-language');
|
||||||
|
|
||||||
|
Route::get('feedback', [\App\Http\Controllers\Site\FeedbackController::class, 'index'])->name('feedback');
|
||||||
|
Route::get('feedback/language/{language}', [\App\Http\Controllers\Site\FeedbackController::class, 'index'])->name('feedback-language');
|
||||||
|
Route::get('feedback/success', [\App\Http\Controllers\Site\FeedbackController::class, 'index'])->name('feedback.success');
|
||||||
|
Route::get('feedback/success/language/{language}', [\App\Http\Controllers\Site\FeedbackController::class, 'index'])->name('feedback.success-language');
|
||||||
|
Route::post('feedback', [\App\Http\Controllers\Site\FeedbackController::class, 'send'])->name('feedback.send');
|
||||||
|
Route::post('feedback/language/{language}', [\App\Http\Controllers\Site\FeedbackController::class, 'send'])->name('feedback.send-language');
|
||||||
|
@ -31,8 +31,12 @@
|
|||||||
Route::get('translations/{language}', [\App\Http\Controllers\Admin\Projects\TranslationsController::class, 'edit'])->name('translations.edit')->where(['language' => '[0-9]+']);
|
Route::get('translations/{language}', [\App\Http\Controllers\Admin\Projects\TranslationsController::class, 'edit'])->name('translations.edit')->where(['language' => '[0-9]+']);
|
||||||
Route::post('translations/{language}', [\App\Http\Controllers\Admin\Projects\TranslationsController::class, 'update'])->name('translations.update')->where(['language' => '[0-9]+']);
|
Route::post('translations/{language}', [\App\Http\Controllers\Admin\Projects\TranslationsController::class, 'update'])->name('translations.update')->where(['language' => '[0-9]+']);
|
||||||
|
|
||||||
|
Route::get('feedbacks', [\App\Http\Controllers\Admin\Projects\FeedbacksController::class, 'project'])->name('feedbacks.index');
|
||||||
|
|
||||||
})->where(['project' => '[0-9]+']);
|
})->where(['project' => '[0-9]+']);
|
||||||
|
|
||||||
|
Route::get('feedbacks', [\App\Http\Controllers\Admin\Projects\FeedbacksController::class, 'index'])->name('feedbacks.index');
|
||||||
|
|
||||||
Route::post('languages/new-language', [\App\Http\Controllers\Admin\LanguagesController::class, 'newLanguage'])->name('new-language');
|
Route::post('languages/new-language', [\App\Http\Controllers\Admin\LanguagesController::class, 'newLanguage'])->name('new-language');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user