Added Feedback section.

This commit is contained in:
2024-04-23 19:30:56 +05:00
parent 491249c8d8
commit 8c353a49b7
44 changed files with 1050 additions and 0 deletions

View File

@@ -0,0 +1,10 @@
<?php declare(strict_types=1);
namespace App\Dto\Builder;
final readonly class ProjectFeedback
{
public function __construct(
) { }
}

View 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;
}
}

View File

@@ -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;
}
}

View File

@@ -30,6 +30,14 @@ final class Translations
'site.Choose language',
'site.Page without translation',
'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',
];
}
}

View 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;
}
}

View File

@@ -11,6 +11,7 @@ enum Permission: string
case ProjectContent = 'project-content';
case ProjectLink = 'project-link';
case ProjectTranslation = 'project-translation';
case ProjectFeedback = 'project-feedback';
public function getPermissions(): array
{
@@ -27,6 +28,9 @@ enum Permission: string
'view' => __('permissions.Allowed to watch'),
'update' => __('permissions.Allowed to edit'),
],
self::ProjectFeedback => [
'view' => __('permissions.Allowed to watch'),
],
default => $this->getBasePermissions()
};

View File

@@ -8,6 +8,8 @@ use App\Models\ProjectLanguage;
enum ProjectSection
{
case Home;
case Feedback;
case FeedbackSend;
public function url(Project $project, ?ProjectLanguage $language = null): string
{
@@ -26,6 +28,9 @@ enum ProjectSection
$route = match ($this) {
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;

View File

@@ -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());
}
}

View File

@@ -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());
}
}

View File

@@ -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),
);
}
}

View File

@@ -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),
);
}
}

View File

@@ -57,4 +57,9 @@ final class Project extends Model implements StorageContract
{
return $this->hasMany(ProjectLink::class);
}
public function feedbacks(): HasMany
{
return $this->hasMany(ProjectFeedback::class);
}
}

View 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);
}
}

View 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');
}
}

View 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');
}
}

View File

@@ -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);
}
}

View File

@@ -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,
]);
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View 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(),
];
}
}

View 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;
}
}

View 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(),
]);
}
}

View File

@@ -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(),
]);
}
}