add: full multi-tenancy control

This commit is contained in:
Cauê Faleiros
2026-02-02 15:31:15 -03:00
commit c6ec92802b
1711 changed files with 258106 additions and 0 deletions

3
packages/Webkul/WebForm/.gitignore vendored Executable file
View File

@@ -0,0 +1,3 @@
/node_modules
/package-lock.json
npm-debug.log

View File

@@ -0,0 +1,25 @@
{
"name": "krayin/laravel-webform",
"license": "MIT",
"authors": [
{
"name": "Jitendra Singh",
"email": "jitendra@webkul.com"
}
],
"require": {},
"autoload": {
"psr-4": {
"Webkul\\WebForm\\": "src/"
}
},
"extra": {
"laravel": {
"providers": [
"Webkul\\WebForm\\Providers\\WebFormServiceProvider"
],
"aliases": {}
}
},
"minimum-stability": "dev"
}

View File

@@ -0,0 +1,24 @@
{
"private": true,
"scripts": {
"dev": "vite",
"build": "vite build"
},
"devDependencies": {
"autoprefixer": "^10.4.16",
"axios": "^1.6.4",
"laravel-vite-plugin": "^0.7.2",
"postcss": "^8.4.23",
"tailwindcss": "^3.3.2",
"vite": "^4.0.0",
"vue": "^3.4.19"
},
"dependencies": {
"@vee-validate/i18n": "^4.9.1",
"@vee-validate/rules": "^4.9.1",
"@vitejs/plugin-vue": "^4.2.3",
"mitt": "^3.0.0",
"vee-validate": "^4.9.1",
"vue-flatpickr": "^2.3.0"
}
}

View File

@@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

View File

@@ -0,0 +1,30 @@
<?php
return [
[
'key' => 'settings.other_settings.web_forms',
'name' => 'web_form::app.acl.title',
'route' => 'admin.settings.web_forms.index',
'sort' => 1,
], [
'key' => 'settings.other_settings.web_forms.view',
'name' => 'web_form::app.acl.view',
'route' => 'admin.settings.web_forms.view',
'sort' => 1,
], [
'key' => 'settings.other_settings.web_forms.create',
'name' => 'web_form::app.acl.create',
'route' => ['admin.settings.web_forms.create', 'admin.settings.web_forms.store'],
'sort' => 2,
], [
'key' => 'settings.other_settings.web_forms.edit',
'name' => 'web_form::app.acl.edit',
'route' => ['admin.settings.web_forms.edit', 'admin.settings.web_forms.update'],
'sort' => 3,
], [
'key' => 'settings.other_settings.web_forms.delete',
'name' => 'web_form::app.acl.delete',
'route' => 'admin.settings.web_forms.delete',
'sort' => 4,
],
];

View File

@@ -0,0 +1,12 @@
<?php
return [
[
'key' => 'settings.other_settings.web_forms',
'name' => 'web_form::app.menu.title',
'info' => 'web_form::app.menu.title-info',
'route' => 'admin.settings.web_forms.index',
'sort' => 1,
'icon-class' => 'icon-settings-webforms',
],
];

View File

@@ -0,0 +1,5 @@
<?php
namespace Webkul\WebForm\Contracts;
interface WebForm {}

View File

@@ -0,0 +1,5 @@
<?php
namespace Webkul\WebForm\Contracts;
interface WebFormAttribute {}

View File

@@ -0,0 +1,84 @@
<?php
namespace Webkul\WebForm\DataGrids;
use Illuminate\Contracts\Database\Query\Builder;
use Illuminate\Support\Facades\DB;
use Webkul\DataGrid\DataGrid;
class WebFormDataGrid extends DataGrid
{
/**
* Prepare query builder.
*/
public function prepareQueryBuilder(): Builder
{
$queryBuilder = DB::table('web_forms')
->addSelect(
'web_forms.id',
'web_forms.title',
);
$this->addFilter('id', 'web_forms.id');
return $queryBuilder;
}
/**
* Prepare columns.
*/
public function prepareColumns(): void
{
$this->addColumn([
'index' => 'id',
'label' => trans('admin::app.settings.webforms.index.datagrid.id'),
'type' => 'string',
'sortable' => true,
]);
$this->addColumn([
'index' => 'title',
'label' => trans('admin::app.settings.webforms.index.datagrid.title'),
'type' => 'string',
'sortable' => true,
'searchable' => true,
'filterable' => true,
]);
}
/**
* Prepare actions.
*/
public function prepareActions(): void
{
if (bouncer()->hasPermission('settings.other_settings.web_forms.view')) {
$this->addAction([
'index' => 'view',
'icon' => 'icon-eye',
'title' => trans('admin::app.settings.webforms.index.datagrid.view'),
'method' => 'GET',
'url' => fn ($row) => route('admin.settings.web_forms.view', $row->id),
]);
}
if (bouncer()->hasPermission('settings.other_settings.web_forms.edit')) {
$this->addAction([
'index' => 'edit',
'icon' => 'icon-edit',
'title' => trans('admin::app.settings.webforms.index.datagrid.edit'),
'method' => 'GET',
'url' => fn ($row) => route('admin.settings.web_forms.edit', $row->id),
]);
}
if (bouncer()->hasPermission('settings.other_settings.web_forms.delete')) {
$this->addAction([
'index' => 'delete',
'icon' => 'icon-delete',
'title' => trans('admin::app.settings.webforms.index.datagrid.delete'),
'method' => 'DELETE',
'url' => fn ($row) => route('admin.settings.web_forms.delete', $row->id),
]);
}
}
}

View File

@@ -0,0 +1,43 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('web_forms', function (Blueprint $table) {
$table->increments('id');
$table->string('form_id')->unique();
$table->string('title');
$table->text('description')->nullable();
$table->text('submit_button_label');
$table->string('submit_success_action');
$table->string('submit_success_content');
$table->boolean('create_lead')->default(0);
$table->string('background_color')->nullable();
$table->string('form_background_color')->nullable();
$table->string('form_title_color')->nullable();
$table->string('form_submit_button_color')->nullable();
$table->string('attribute_label_color')->nullable();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('web_forms');
}
};

View File

@@ -0,0 +1,41 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('web_form_attributes', function (Blueprint $table) {
$table->increments('id');
$table->string('name')->nullable();
$table->string('placeholder')->nullable();
$table->boolean('is_required')->default(0);
$table->boolean('is_hidden')->default(0);
$table->integer('sort_order')->nullable();
$table->integer('attribute_id')->unsigned();
$table->foreign('attribute_id')->references('id')->on('attributes')->onDelete('cascade');
$table->integer('web_form_id')->unsigned();
$table->foreign('web_form_id')->references('id')->on('web_forms')->onDelete('cascade');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('web_form_attributes');
}
};

View File

@@ -0,0 +1,23 @@
<?php
namespace Webkul\WebForm\Http\Controllers;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Foundation\Bus\DispatchesJobs;
use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Routing\Controller as BaseController;
class Controller extends BaseController
{
use AuthorizesRequests, DispatchesJobs, ValidatesRequests;
/**
* Display a listing of the resource.
*
* @return \Illuminate\Http\Response
*/
public function redirectToLogin()
{
return redirect()->route('admin.session.create');
}
}

View File

@@ -0,0 +1,160 @@
<?php
namespace Webkul\WebForm\Http\Controllers;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Event;
use Illuminate\View\View;
use Webkul\Attribute\Repositories\AttributeRepository;
use Webkul\Contact\Repositories\PersonRepository;
use Webkul\Lead\Repositories\LeadRepository;
use Webkul\Lead\Repositories\PipelineRepository;
use Webkul\Lead\Repositories\SourceRepository;
use Webkul\Lead\Repositories\TypeRepository;
use Webkul\WebForm\Http\Requests\WebForm;
use Webkul\WebForm\Repositories\WebFormRepository;
class WebFormController extends Controller
{
/**
* Create a new controller instance.
*
* @return void
*/
public function __construct(
protected AttributeRepository $attributeRepository,
protected WebFormRepository $webFormRepository,
protected PersonRepository $personRepository,
protected LeadRepository $leadRepository,
protected PipelineRepository $pipelineRepository,
protected SourceRepository $sourceRepository,
protected TypeRepository $typeRepository,
) {}
/**
* Remove the specified email template from storage.
*/
public function formJS(string $formId): Response
{
$webForm = $this->webFormRepository->findOneByField('form_id', $formId);
return response()->view('web_form::settings.web-forms.embed', compact('webForm'))
->header('Content-Type', 'text/javascript');
}
/**
* Remove the specified email template from storage.
*/
public function formStore(int $id): JsonResponse
{
$person = $this->personRepository
->getModel()
->where('emails', 'like', '%'.request('persons.emails.0.value').'%')
->first();
if ($person) {
request()->request->add(['persons' => array_merge(request('persons'), ['id' => $person->id])]);
}
app(WebForm::class);
$webForm = $this->webFormRepository->findOrFail($id);
if ($webForm->create_lead) {
request()->request->add(['entity_type' => 'leads']);
Event::dispatch('lead.create.before');
$data = request('leads');
$data['entity_type'] = 'leads';
$data['person'] = request('persons');
$data['status'] = 1;
$pipeline = $this->pipelineRepository->getDefaultPipeline();
$stage = $pipeline->stages()->first();
$data['lead_pipeline_id'] = $pipeline->id;
$data['lead_pipeline_stage_id'] = $stage->id;
$data['title'] = request('leads.title') ?: 'Lead From Web Form';
$data['lead_value'] = request('leads.lead_value') ?: 0;
if (! request('leads.lead_source_id')) {
$source = $this->sourceRepository->findOneByField('name', 'Web Form');
if (! $source) {
$source = $this->sourceRepository->first();
}
$data['lead_source_id'] = $source->id;
}
$data['lead_type_id'] = request('leads.lead_type_id') ?: $this->typeRepository->first()->id;
$lead = $this->leadRepository->create($data);
Event::dispatch('lead.create.after', $lead);
} else {
if (! $person) {
Event::dispatch('contacts.person.create.before');
$data = request('persons');
request()->request->add(['entity_type' => 'persons']);
$data['entity_type'] = 'persons';
$person = $this->personRepository->create($data);
Event::dispatch('contacts.person.create.after', $person);
}
}
if ($webForm->submit_success_action == 'message') {
return response()->json([
'message' => $webForm->submit_success_content,
], 200);
} else {
return response()->json([
'redirect' => $webForm->submit_success_content,
], 301);
}
}
/**
* Remove the specified email template from storage.
*/
public function preview(string $id): View
{
$webForm = $this->webFormRepository->findOneByField('form_id', $id);
if (is_null($webForm)) {
abort(404);
}
return view('web_form::settings.web-forms.preview', compact('webForm'));
}
/**
* Preview the web form from datagrid.
*/
public function view(int $id): View
{
$webForm = $this->webFormRepository->findOneByField('id', $id);
request()->merge(['id' => $webForm->form_id]);
if (is_null($webForm)) {
abort(404);
}
return view('web_form::settings.web-forms.preview', compact('webForm'));
}
}

View File

@@ -0,0 +1,127 @@
<?php
namespace Webkul\WebForm\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Webkul\Attribute\Repositories\AttributeRepository;
use Webkul\Attribute\Repositories\AttributeValueRepository;
use Webkul\Core\Contracts\Validations\Decimal;
use Webkul\WebForm\Rules\PhoneNumber;
class WebForm extends FormRequest
{
/**
* @var array
*/
protected $rules = [];
/**
* Create a new form request instance.
*
* @return void
*/
public function __construct(
protected AttributeRepository $attributeRepository,
protected AttributeValueRepository $attributeValueRepository
) {}
/**
* Determine if the product is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
foreach (['leads', 'persons'] as $key => $entityType) {
$attributes = $this->attributeRepository->scopeQuery(function ($query) use ($entityType) {
$attributeCodes = $entityType == 'persons'
? array_keys(request('persons') ?? [])
: array_keys(request('leads') ?? []);
$query = $query->whereIn('code', $attributeCodes)
->where('entity_type', $entityType);
return $query;
})->get();
foreach ($attributes as $attribute) {
$attribute->code = $entityType.'.'.$attribute->code;
$validations = [];
if ($attribute->type == 'boolean') {
continue;
} elseif ($attribute->type == 'address') {
if (! $attribute->is_required) {
continue;
}
$validations = [
$attribute->code.'.address' => 'required',
$attribute->code.'.country' => 'required',
$attribute->code.'.state' => 'required',
$attribute->code.'.city' => 'required',
$attribute->code.'.postcode' => 'required',
];
} elseif ($attribute->type == 'email') {
$validations = [
$attribute->code => [$attribute->is_required ? 'required' : 'nullable'],
$attribute->code.'.*.value' => [$attribute->is_required ? 'required' : 'nullable', 'email'],
$attribute->code.'.*.label' => $attribute->is_required ? 'required' : 'nullable',
];
} elseif ($attribute->type == 'phone') {
$validations = [
$attribute->code => [$attribute->is_required ? 'required' : 'nullable'],
$attribute->code.'.*.value' => [$attribute->is_required ? 'required' : 'nullable', new PhoneNumber],
$attribute->code.'.*.label' => $attribute->is_required ? 'required' : 'nullable',
];
} else {
$validations[$attribute->code] = [$attribute->is_required ? 'required' : 'nullable'];
if ($attribute->type == 'text' && $attribute->validation) {
array_push($validations[$attribute->code],
$attribute->validation == 'decimal'
? new Decimal
: $attribute->validation
);
}
if ($attribute->type == 'price') {
array_push($validations[$attribute->code], new Decimal);
}
}
if ($attribute->is_unique) {
array_push($validations[in_array($attribute->type, ['email', 'phone'])
? $attribute->code.'.*.value'
: $attribute->code
], function ($field, $value, $fail) use ($attribute, $entityType) {
if (! $this->attributeValueRepository->isValueUnique(
$entityType == 'persons' ? request('persons.id') : null,
$attribute->entity_type,
$attribute,
request($field)
)
) {
$fail('The value has already been taken.');
}
});
}
$this->rules = array_merge($this->rules, $validations);
}
}
return $this->rules;
}
}

View File

@@ -0,0 +1,32 @@
<?php
namespace Webkul\WebForm\Models;
use Illuminate\Database\Eloquent\Model;
use Webkul\WebForm\Contracts\WebForm as WebFormContract;
class WebForm extends Model implements WebFormContract
{
protected $fillable = [
'form_id',
'title',
'description',
'submit_button_label',
'submit_success_action',
'submit_success_content',
'create_lead',
'background_color',
'form_background_color',
'form_title_color',
'form_submit_button_color',
'attribute_label_color',
];
/**
* The attributes that belong to the activity.
*/
public function attributes()
{
return $this->hasMany(WebFormAttributeProxy::modelClass());
}
}

View File

@@ -0,0 +1,48 @@
<?php
namespace Webkul\WebForm\Models;
use Illuminate\Database\Eloquent\Model;
use Webkul\Attribute\Models\AttributeProxy;
use Webkul\WebForm\Contracts\WebFormAttribute as WebFormAttributeContract;
class WebFormAttribute extends Model implements WebFormAttributeContract
{
/**
* Indicates if the model should be timestamped.
*
* @var string
*/
public $timestamps = false;
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'name',
'placeholder',
'is_required',
'is_hidden',
'sort_order',
'attribute_id',
'web_form_id',
];
/**
* Get the attribute that owns the attribute.
*/
public function attribute()
{
return $this->belongsTo(AttributeProxy::modelClass());
}
/**
* Get the web_form that owns the attribute.
*/
public function web_form()
{
return $this->belongsTo(WebFormProxy::modelClass());
}
}

View File

@@ -0,0 +1,7 @@
<?php
namespace Webkul\WebForm\Models;
use Konekt\Concord\Proxies\ModelProxy;
class WebFormAttributeProxy extends ModelProxy {}

View File

@@ -0,0 +1,7 @@
<?php
namespace Webkul\WebForm\Models;
use Konekt\Concord\Proxies\ModelProxy;
class WebFormProxy extends ModelProxy {}

View File

@@ -0,0 +1,13 @@
<?php
namespace Webkul\WebForm\Providers;
use Webkul\Core\Providers\BaseModuleServiceProvider;
class ModuleServiceProvider extends BaseModuleServiceProvider
{
protected $models = [
\Webkul\WebForm\Models\WebForm::class,
\Webkul\WebForm\Models\WebFormAttribute::class,
];
}

View File

@@ -0,0 +1,45 @@
<?php
namespace Webkul\WebForm\Providers;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\ServiceProvider;
class WebFormServiceProvider extends ServiceProvider
{
/**
* Bootstrap services.
*/
public function boot(): void
{
$this->loadRoutesFrom(__DIR__.'/../Routes/routes.php');
$this->loadTranslationsFrom(__DIR__.'/../Resources/lang', 'web_form');
$this->loadViewsFrom(__DIR__.'/../Resources/views', 'web_form');
Blade::anonymousComponentPath(__DIR__.'/../Resources/views/components');
$this->loadMigrationsFrom(__DIR__.'/../Database/Migrations');
$this->app->register(ModuleServiceProvider::class);
}
/**
* Register services.
*/
public function register(): void
{
$this->registerConfig();
}
/**
* Register package config.
*/
protected function registerConfig(): void
{
$this->mergeConfigFrom(dirname(__DIR__).'/Config/menu.php', 'menu.admin');
$this->mergeConfigFrom(dirname(__DIR__).'/Config/acl.php', 'acl');
}
}

View File

@@ -0,0 +1,18 @@
<?php
namespace Webkul\WebForm\Repositories;
use Webkul\Core\Eloquent\Repository;
class WebFormAttributeRepository extends Repository
{
/**
* Specify Model class name
*
* @return mixed
*/
public function model()
{
return 'Webkul\WebForm\Contracts\WebFormAttribute';
}
}

View File

@@ -0,0 +1,87 @@
<?php
namespace Webkul\WebForm\Repositories;
use Illuminate\Container\Container;
use Illuminate\Support\Str;
use Webkul\Core\Eloquent\Repository;
use Webkul\WebForm\Contracts\WebForm;
class WebFormRepository extends Repository
{
/**
* Create a new repository instance.
*
* @return void
*/
public function __construct(
protected WebFormAttributeRepository $webFormAttributeRepository,
Container $container
) {
parent::__construct($container);
}
/**
* Specify model class name.
*
* @return mixed
*/
public function model()
{
return WebForm::class;
}
/**
* Create Web Form.
*
* @return \Webkul\WebForm\Contracts\WebForm
*/
public function create(array $data)
{
$webForm = $this->model->create(array_merge($data, [
'form_id' => Str::random(50),
]));
foreach ($data['attributes'] as $attributeData) {
$this->webFormAttributeRepository->create(array_merge([
'web_form_id' => $webForm->id,
], $attributeData));
}
return $webForm;
}
/**
* Update Web Form.
*
* @param int $id
* @param string $attribute
* @return \Webkul\WebForm\Contracts\WebForm
*/
public function update(array $data, $id, $attribute = 'id')
{
$webForm = parent::update($data, $id);
$previousAttributeIds = $webForm->attributes()->pluck('id');
foreach ($data['attributes'] as $attributeId => $attributeData) {
if (Str::contains($attributeId, 'attribute_')) {
$this->webFormAttributeRepository->create(array_merge([
'web_form_id' => $webForm->id,
], $attributeData));
} else {
if (is_numeric($index = $previousAttributeIds->search($attributeId))) {
$previousAttributeIds->forget($index);
}
$this->webFormAttributeRepository->update($attributeData, $attributeId);
}
}
foreach ($previousAttributeIds as $attributeId) {
$this->webFormAttributeRepository->delete($attributeId);
}
return $webForm;
}
}

View File

@@ -0,0 +1,128 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
/* -------------------------------- new css -------------------------------- */
@font-face {
font-family: "icomoon";
src: url("../fonts/icomoon.woff?w2trdd") format("woff");
font-weight: normal;
font-style: normal;
font-display: block;
}
[class^="icon-"],
[class*=" icon-"] {
/* use !important to prevent issues with browser extensions that change fonts */
font-family: "icomoon" !important;
speak: never;
font-style: normal;
font-weight: normal;
font-variant: normal;
text-transform: none;
line-height: 1;
/* Better Font Rendering =========== */
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
@layer components {
.icon-cross-large:before {
content: "\e91c";
}
[dir="rtl"] .stage::before {
content: "";
position: absolute;
top: 50%;
left: -10px;
width: 24px;
height: 24px;
z-index: 1;
border-radius: 0 0 0 25px;
transform: translateY(-50%) rotate(225deg);
border-right: 4px solid #f3f4f6;
border-top: 4px solid #f3f4f6;
}
[dir="rtl"] .stage::after {
display: none;
}
.primary-button {
@apply bg-brandColor border border-brandColor cursor-pointer flex focus:opacity-[0.9] font-semibold gap-x-1 hover:opacity-[0.9] items-center place-content-center px-3 py-1.5 rounded-md text-gray-50 transition-all;
}
.secondary-button {
@apply flex cursor-pointer place-content-center items-center gap-x-1 whitespace-nowrap rounded-md border-2 border-brandColor bg-white px-3 py-1.5 font-semibold text-brandColor transition-all hover:bg-[#eff6ff61] focus:bg-[#eff6ff61] dark:border-gray-400 dark:bg-gray-800 dark:text-white dark:hover:opacity-80;
}
.transparent-button {
@apply flex cursor-pointer appearance-none place-content-center items-center gap-x-1 whitespace-nowrap rounded-md border-2 border-transparent px-3 py-1.5 font-semibold text-gray-600 transition-all marker:shadow hover:bg-gray-100 focus:bg-gray-100 dark:hover:bg-gray-950;
}
::-webkit-scrollbar {
width: 12px;
}
::-webkit-scrollbar-track {
background: #f1f1f1;
}
::-webkit-scrollbar-thumb {
background: #888;
border-radius: 6px;
}
::-webkit-scrollbar-thumb:hover {
background: #555;
}
/* Firefox */
* {
scrollbar-width: thin;
scrollbar-color: #888 #f1f1f1;
}
::selection {
background-color: rgba(0, 68, 242, 0.2);
}
body {
@apply bg-gray-100 text-sm text-gray-800;
}
button:disabled {
@apply cursor-not-allowed opacity-50;
}
button:disabled:hover {
@apply cursor-not-allowed opacity-50;
}
.draggable-ghost {
opacity: 0.5;
background: #e0e7ff;
}
html.dark [class^="icon-"],
html.dark [class*=" icon-"] {
color: #d1d5db;
}
p {
@apply text-[14px] !leading-[17px];
}
input,
textarea,
select {
@apply outline-none;
}
.required:after {
@apply content-['*'];
}
}

View File

@@ -0,0 +1,57 @@
/**
* This will track all the images and fonts for publishing.
*/
import.meta.glob(["../images/**", "../fonts/**"]);
/**
* Main vue bundler.
*/
import { createApp } from "vue/dist/vue.esm-bundler";
/**
* Main root application registry.
*/
window.app = createApp({
data() {
return {
isMenuActive: false,
hoveringMenu: '',
};
},
methods: {
onSubmit() {},
onInvalidSubmit({ values, errors, results }) {
setTimeout(() => {
const errorKeys = Object.entries(errors)
.map(([key, value]) => ({ key, value }))
.filter(error => error["value"].length);
let firstErrorElement = document.querySelector('[name="' + errorKeys[0]["key"] + '"]');
firstErrorElement.scrollIntoView({
behavior: "smooth",
block: "center"
});
}, 100);
},
},
});
/**
* Global plugins registration.
*/
import Axios from "./plugins/axios";
import Emitter from "./plugins/emitter";
import Flatpickr from "./plugins/flatpickr";
import VeeValidate from "./plugins/vee-validate";
[
Axios,
Emitter,
Flatpickr,
VeeValidate,
].forEach((plugin) => app.use(plugin));
export default app;

View File

@@ -0,0 +1,14 @@
/**
* We'll load the axios HTTP library which allows us to easily issue requests
* to our Laravel back-end. This library automatically handles sending the
* CSRF token as a header based on the value of the "XSRF" token cookie.
*/
import axios from "axios";
window.axios = axios;
window.axios.defaults.headers.common["X-Requested-With"] = "XMLHttpRequest";
export default {
install(app) {
app.config.globalProperties.$axios = axios;
},
};

View File

@@ -0,0 +1,11 @@
import mitt from "mitt";
const emitter = mitt();
window.emitter = emitter;
export default {
install: (app, options) => {
app.config.globalProperties.$emitter = emitter;
},
};

View File

@@ -0,0 +1,35 @@
import Flatpickr from "flatpickr";
import "flatpickr/dist/flatpickr.css";
export default {
install: (app) => {
window.Flatpickr = Flatpickr;
const changeTheme = (theme) => {
document.getElementById('flatpickr')?.remove();
if (theme === 'light') {
return;
}
const linkElement = document.createElement("link");
linkElement.rel = "stylesheet";
linkElement.type = "text/css";
linkElement.href = `https://npmcdn.com/flatpickr/dist/themes/${theme}.css`;
linkElement.id = 'flatpickr';
document.head.appendChild(linkElement);
};
const currentTheme = document.documentElement.classList.contains("dark")
? "dark"
: "light";
changeTheme(currentTheme);
app.config.globalProperties.$emitter.on("change-theme", (theme) => {
changeTheme(theme);
});
},
};

View File

@@ -0,0 +1,285 @@
/**
* We are defining all the global rules here and configuring
* all the `vee-validate` settings.
*/
import { configure, defineRule, Field, Form, ErrorMessage } from "vee-validate";
import { localize, setLocale } from "@vee-validate/i18n";
import ar from "@vee-validate/i18n/dist/locale/ar.json";
import bn from "@vee-validate/i18n/dist/locale/bn.json";
import de from "@vee-validate/i18n/dist/locale/de.json";
import en from "@vee-validate/i18n/dist/locale/en.json";
import es from "@vee-validate/i18n/dist/locale/es.json";
import fa from "@vee-validate/i18n/dist/locale/fa.json";
import fr from "@vee-validate/i18n/dist/locale/fr.json";
import he from "@vee-validate/i18n/dist/locale/he.json";
import hi_IN from "../../locales/hi_IN.json";
import it from "@vee-validate/i18n/dist/locale/it.json";
import ja from "@vee-validate/i18n/dist/locale/ja.json";
import nl from "@vee-validate/i18n/dist/locale/nl.json";
import pl from "@vee-validate/i18n/dist/locale/pl.json";
import pt_BR from "@vee-validate/i18n/dist/locale/pt_BR.json";
import ru from "@vee-validate/i18n/dist/locale/ru.json";
import sin from "../../locales/sin.json";
import tr from "@vee-validate/i18n/dist/locale/tr.json";
import uk from "@vee-validate/i18n/dist/locale/uk.json";
import zh_CN from "@vee-validate/i18n/dist/locale/zh_CN.json";
import { all } from '@vee-validate/rules';
window.defineRule = defineRule;
export default {
install: (app) => {
/**
* Global components registration;
*/
app.component("VForm", Form);
app.component("VField", Field);
app.component("VErrorMessage", ErrorMessage);
window.addEventListener("load", () => setLocale(document.documentElement.attributes.lang.value));
/**
* Registration of all global validators.
*/
Object.entries(all).forEach(([name, rule]) => defineRule(name, rule));
/**
* This regular expression allows phone numbers with the following conditions:
* - The phone number can start with an optional "+" sign.
* - After the "+" sign, there should be one or more digits.
*
* This validation is sufficient for global-level phone number validation. If
* someone wants to customize it, they can override this rule.
*/
defineRule("phone", (value) => {
if (! value || ! value.length) {
return true;
}
if (! /^\+?\d+$/.test(value)) {
return false;
}
return true;
});
defineRule("address", (value) => {
if (!value || !value.length) {
return true;
}
if (
!/^[a-zA-Z0-9\s.\/*'\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF\u0590-\u05FF\u3040-\u309F\u30A0-\u30FF\u0400-\u04FF\u0D80-\u0DFF\u3400-\u4DBF\u2000-\u2A6D\u00C0-\u017F\u0980-\u09FF\u0900-\u097F\u4E00-\u9FFF,\(\)-]{1,60}$/iu.test(
value
)
) {
return false;
}
return true;
});
defineRule("decimal", (value, { decimals = '*', separator = '.' } = {}) => {
if (value === null || value === undefined || value === '') {
return true;
}
if (Number(decimals) === 0) {
return /^-?\d*$/.test(value);
}
const regexPart = decimals === '*' ? '+' : `{1,${decimals}}`;
const regex = new RegExp(`^[-+]?\\d*(\\${separator}\\d${regexPart})?([eE]{1}[-]?\\d+)?$`);
return regex.test(value);
});
defineRule("required_if", (value, { condition = true } = {}) => {
if (condition) {
if (value === null || value === undefined || value === '') {
return false;
}
}
return true;
});
defineRule("", () => true);
// @TODO handle this
// @suraj-webkul
defineRule("date_format", (value) => {
return true;
});
// @TODO handle this
// @suraj-webkul
defineRule("after", (value) => {
return true;
});
configure({
/**
* Built-in error messages and custom error messages are available. Multiple
* locales can be added in the same way.
*/
generateMessage: localize({
ar: {
...ar,
messages: {
...ar.messages,
phone: "يجب أن يكون هذا {field} رقم هاتف صالحًا",
},
},
bn: {
...bn,
messages: {
...bn.messages,
phone: "এই {field} একটি বৈধ ফোন নম্বর হতে হবে",
},
},
de: {
...de,
messages: {
...de.messages,
phone: "Dieses {field} muss eine gültige Telefonnummer sein.",
},
},
en: {
...en,
messages: {
...en.messages,
phone: "This {field} must be a valid phone number",
},
},
es: {
...es,
messages: {
...es.messages,
phone: "Este {field} debe ser un número de teléfono válido.",
},
},
fa: {
...fa,
messages: {
...fa.messages,
phone: "این {field} باید یک شماره تلفن معتبر باشد.",
},
},
fr: {
...fr,
messages: {
...fr.messages,
phone: "Ce {field} doit être un numéro de téléphone valide.",
},
},
he: {
...he,
messages: {
...he.messages,
phone: "זה {field} חייב להיות מספר טלפון תקין.",
},
},
hi_IN: {
...hi_IN,
messages: {
...hi_IN.messages,
phone: "यह {field} कोई मान्य फ़ोन नंबर होना चाहिए।",
},
},
it: {
...it,
messages: {
...it.messages,
phone: "Questo {field} deve essere un numero di telefono valido.",
},
},
ja: {
...ja,
messages: {
...ja.messages,
phone: "この{field}は有効な電話番号である必要があります。",
},
},
nl: {
...nl,
messages: {
...nl.messages,
phone: "Dit {field} moet een geldig telefoonnummer zijn.",
},
},
pl: {
...pl,
messages: {
...pl.messages,
phone: "To {field} musi być prawidłowy numer telefonu.",
},
},
pt_BR: {
...pt_BR,
messages: {
...pt_BR.messages,
phone: "Este {field} deve ser um número de telefone válido.",
},
},
ru: {
...ru,
messages: {
...ru.messages,
phone: "Это {field} должно быть действительным номером телефона.",
},
},
sin: {
...sin,
messages: {
...sin.messages,
phone: "මෙම {field} වටේ වලංගු දුරකතන අංකය විය යුතුයි.",
},
},
tr: {
...tr,
messages: {
...tr.messages,
phone: "Bu {field} geçerli bir telefon numarası olmalıdır.",
},
},
uk: {
...uk,
messages: {
...uk.messages,
phone: "Це {field} повинно бути дійсним номером телефону.",
},
},
zh_CN: {
...zh_CN,
messages: {
...zh_CN.messages,
phone: "这个 {field} 必须是一个有效的电话号码。",
},
},
}),
validateOnBlur: true,
validateOnInput: true,
validateOnChange: true,
});
},
};

View File

@@ -0,0 +1,32 @@
{
"code": "hi_IN",
"messages": {
"_default": "यह {field} मान्य नहीं है",
"alpha": "{field} फ़ील्ड में केवल वर्णात्मक अक्षर हो सकते हैं",
"alpha_num": "{field} फ़ील्ड में केवल वर्णात्मक और संख्यात्मक अक्षर हो सकते हैं",
"alpha_dash": "{field} फ़ील्ड में वर्णात्मक और संख्यात्मक अक्षरों के साथ डैश और अंडरस्कोर हो सकते हैं",
"alpha_spaces": "{field} फ़ील्ड में केवल वर्णात्मक अक्षर और अंतर हो सकते हैं",
"between": "{field} फ़ील्ड 0:{min} और 1:{max} के बीच होना चाहिए",
"confirmed": "{field} फ़ील्ड की पुष्टि मेल नहीं खाती",
"digits": "{field} फ़ील्ड संख्यात्मक होनी चाहिए और बिल्कुल 0:{length} अंक होने चाहिए",
"dimensions": "{field} फ़ील्ड 0:{width} पिक्सेल और 1:{height} पिक्सेल होना चाहिए",
"email": "{field} फ़ील्ड में एक मान्य ईमेल होना चाहिए",
"not_one_of": "{field} फ़ील्ड मान्य मूल्य नहीं है",
"ext": "{field} फ़ील्ड में मान्य फ़ाइल नहीं है",
"image": "{field} फ़ील्ड एक छवि होनी चाहिए",
"integer": "{field} फ़ील्ड एक पूर्णांक होना चाहिए",
"length": "{field} फ़ील्ड 0:{length} लंबा होना चाहिए",
"max_value": "{field} फ़ील्ड 0:{max} या उससे कम होना चाहिए",
"max": "{field} फ़ील्ड 0:{length} अक्षरों से अधिक नहीं हो सकता",
"mimes": "{field} फ़ील्ड को मान्य फ़ाइल प्रकार होना चाहिए",
"min_value": "{field} फ़ील्ड 0:{min} या उससे अधिक होना चाहिए",
"min": "{field} फ़ील्ड कम से कम 0:{length} अक्षरों का होना चाहिए",
"numeric": "{field} फ़ील्ड में केवल संख्याएँ हो सकती हैं",
"one_of": "{field} फ़ील्ड मान्य मूल्य नहीं है",
"regex": "{field} फ़ील्ड का प्रारूप अवैध है",
"required_if": "{field} फ़ील्ड आवश्यक है",
"required": "{field} फ़ील्ड आवश्यक है",
"size": "{field} फ़ील्ड का आकार 0:{size}KB से कम होना चाहिए",
"url": "{field} फ़ील्ड में एक मान्य URL नहीं है"
}
}

View File

@@ -0,0 +1,32 @@
{
"code": "sin",
"messages": {
"_default": "මේ {field} වල වලංගු නොවේ",
"alpha": "{field} ක්ෂණික සංඛ්‍යාවක් පිළිබඳව සියල්ල සියල්ල සහිතව හැකිය",
"alpha_num": "{field} ක්ෂණික සහ සංඛ්‍යාවක් පිළිබඳව සියල්ල සහිතව හැකිය",
"alpha_dash": "{field} ක්ෂණික සහ සංඛ්‍යාවක් සමග දැහැ හෝ පරිදි ලොව සහිතව හැකිය",
"alpha_spaces": "{field} ක්ෂණික සංඛ්‍යාවක් සහිතව හැකිය, සහ වීඩියෝ හෝම්හෝ සහිතව හැකිය",
"between": "{field} ක්ෂණික 0:{min} සහ 1:{max} අතර විය යුතුය",
"confirmed": "{field} ක්ෂණික තහවුරු නොගත් බව තහවුරු කර නොයාය",
"digits": "{field} ක්ෂණික සෂ්යෝගයක් හා සියලුමේ විය 0:{length} දිගු විය යුතුය",
"dimensions": "{field} ක්ෂණික 0:{width} පික්සල සහ 1:{height} පික්සල විය යුතුය",
"email": "{field} ක්ෂණික වලංගු ඊමේල් එක හෝ යුක්ත විය යුතුය",
"not_one_of": "{field} ක්ෂණික වලංගු අගය නොවේ",
"ext": "{field} ක්ෂණික වලංගු ගොනුව නොවේ",
"image": "{field} ක්ෂණික වලංගු ඡායාරූපය යුතුය",
"integer": "{field} ක්ෂණික වලංගු නික්මෙර වර්ගයේ යුතුය",
"length": "{field} ක්ෂණික වලංගු 0:{length} හෝමාව යුතුය",
"max_value": "{field} ක්ෂණික 0:{max} හෝමා හෝමා හෝමා යුතුය",
"max": "{field} ක්ෂණික 0:{length} අකුරු වලංගු වී නොයාය",
"mimes": "{field} ක්ෂණික ගොනුවේ වලංගු ගොනු වර්ගය හෝ හෝ හෝ යුතුය",
"min_value": "{field} ක්ෂණික 0:{min} හෝමාව හෝමාව හෝමාව හෝමාව හෝමාව යුතුය",
"min": "{field} ක්ෂණික 0:{length} හෝමාවක් හෝමාවක් හෝමාවක් හෝමාවක් යුතුය",
"numeric": "{field} ක්ෂණික වලංගු සංඛ්‍යාවෙන් වයස්ක්‍ර සංඛ්‍යාවෙන් වයස්ක්‍ර විය ෺",
"one_of": "{field} ක්ෂණික වලංගු අගය නොවේ",
"regex": "{field} ක්ෂණික වලංගු ආකාරය අවලංගුය",
"required_if": "{field} ක්ෂණිකයෙන් හෝයි",
"required": "{field} ක්ෂණිකයෙන් හෝයි",
"size": "{field} ක්ෂණික වලංගු විය හැකි ආකාරය 0:{size}KB හෝ හොයා යුතුයි",
"url": "{field} ක්ෂණික වලංගු වර්ගවල URL නොවේ"
}
}

View File

@@ -0,0 +1,20 @@
<?php
return [
'acl' => [
'title' => 'نماذج الويب',
'view' => 'عرض',
'create' => 'إنشاء',
'edit' => 'تعديل',
'delete' => 'حذف',
],
'menu' => [
'title' => 'نماذج الويب',
'title-info' => 'إضافة، تعديل أو حذف نماذج الويب من CRM',
],
'validations' => [
'invalid-phone-number' => 'رقم الهاتف غير صحيح',
],
];

View File

@@ -0,0 +1,20 @@
<?php
return [
'acl' => [
'title' => 'Web Forms',
'view' => 'View',
'create' => 'Create',
'edit' => 'Edit',
'delete' => 'Delete',
],
'menu' => [
'title' => 'Web Forms',
'title-info' => 'Add, edit, or delete web forms from the CRM',
],
'validations' => [
'invalid-phone-number' => 'Invalid phone number',
],
];

View File

@@ -0,0 +1,20 @@
<?php
return [
'acl' => [
'title' => 'Formularios Web',
'view' => 'Ver',
'create' => 'Crear',
'edit' => 'Editar',
'delete' => 'Eliminar',
],
'menu' => [
'title' => 'Formularios Web',
'title-info' => 'Agregar, editar o eliminar formularios web desde CRM',
],
'validations' => [
'invalid-phone-number' => 'Número de teléfono no válido',
],
];

View File

@@ -0,0 +1,20 @@
<?php
return [
'acl' => [
'title' => 'فرم‌های وب',
'view' => 'نمایش',
'create' => 'ایجاد',
'edit' => 'ویرایش',
'delete' => 'حذف',
],
'menu' => [
'title' => 'فرم‌های وب',
'title-info' => 'افزودن، ویرایش یا حذف فرم‌های وب از CRM',
],
'validations' => [
'invalid-phone-number' => 'شماره تلفن نامعتبر است',
],
];

View File

@@ -0,0 +1,20 @@
<?php
return [
'acl' => [
'title' => 'Formulários Web',
'view' => 'Visualizar',
'create' => 'Adicionar',
'edit' => 'Editar',
'delete' => 'Excluir',
],
'menu' => [
'title' => 'Formulários Web',
'title-info' => 'Adicione, edite ou exclua formulários web no CRM',
],
'validations' => [
'invalid-phone-number' => 'Número de telefone inválido',
],
];

View File

@@ -0,0 +1,20 @@
<?php
return [
'acl' => [
'title' => 'Web Formları',
'view' => 'Görüntüle',
'create' => 'Oluştur',
'edit' => 'Düzenle',
'delete' => 'Sil',
],
'menu' => [
'title' => 'Web Formları',
'title-info' => 'CRMden web formlarını ekle, düzenle veya sil',
],
'validations' => [
'invalid-phone-number' => 'Geçersiz telefon numarası',
],
];

View File

@@ -0,0 +1,20 @@
<?php
return [
'acl' => [
'title' => 'Biểu mẫu Web',
'view' => 'Xem',
'create' => 'Tạo',
'edit' => 'Chỉnh sửa',
'delete' => 'Xóa',
],
'menu' => [
'title' => 'Biểu mẫu Web',
'title-info' => 'Thêm, chỉnh sửa hoặc xóa biểu mẫu web từ CRM',
],
'validations' => [
'invalid-phone-number' => 'Số điện thoại không hợp lệ.',
],
];

View File

@@ -0,0 +1,40 @@
<v-button {{ $attributes }}></v-button>
@pushOnce('scripts')
<script
type="text/x-template"
id="v-button-template"
>
<button
v-if="! loading"
:class="[buttonClass, '']"
>
@{{ title }}
</button>
<button
v-else
:class="[buttonClass, '']"
>
<!-- Spinner -->
<x-admin::spinner class="absolute" />
<span class="realative h-full w-full opacity-0">
@{{ title }}
</span>
</button>
</script>
<script type="module">
app.component('v-button', {
template: '#v-button-template',
props: {
loading: Boolean,
buttonType: String,
title: String,
buttonClass: String,
},
});
</script>
@endPushOnce

View File

@@ -0,0 +1,64 @@
<v-flash-group ref='flashes'></v-flash-group>
@pushOnce('scripts')
<script
type="text/x-template"
id="v-flash-group-template"
>
<transition-group
tag='div'
name="flash-group"
enter-from-class="ltr:translate-y-full rtl:-translate-y-full"
enter-active-class="transform transition duration-300 ease-[cubic-bezier(.4,0,.2,1)]"
enter-to-class="ltr:translate-y-0 rtl:-translate-y-0"
leave-from-class="ltr:translate-y-0 rtl:-translate-y-0"
leave-active-class="transform transition duration-300 ease-[cubic-bezier(.4,0,.2,1)]"
leave-to-class="ltr:translate-y-full rtl:-translate-y-full"
class='fixed bottom-5 left-1/2 z-[10003] grid -translate-x-1/2 justify-items-end gap-2.5'
>
<x-admin::flash-group.item />
</transition-group>
</script>
<script type="module">
app.component('v-flash-group', {
template: '#v-flash-group-template',
data() {
return {
uid: 0,
flashes: []
}
},
created() {
@foreach (['success', 'warning', 'error', 'info'] as $key)
@if (session()->has($key))
this.flashes.push({'type': '{{ $key }}', 'message': "{{ session($key) }}", 'uid': this.uid++});
@endif
@endforeach
this.registerGlobalEvents();
},
methods: {
add(flash) {
flash.uid = this.uid++;
this.flashes.push(flash);
},
remove(flash) {
let index = this.flashes.indexOf(flash);
this.flashes.splice(index, 1);
},
registerGlobalEvents() {
this.$emitter.on('add-flash', this.add);
},
}
});
</script>
@endpushOnce

View File

@@ -0,0 +1,205 @@
<v-flash-item
v-for='flash in flashes'
:key='flash.uid'
:flash="flash"
@onRemove="remove($event)"
>
</v-flash-item>
@pushOnce('scripts')
<script
type="text/x-template"
id="v-flash-item-template"
>
<div
class="flex w-max items-start justify-between gap-2 rounded-lg bg-white p-3 shadow-[0px_10px_20px_0px_rgba(0,0,0,0.12)] dark:bg-gray-950"
:style="typeStyles[flash.type]['container']"
@mouseenter="pauseTimer"
@mouseleave="resumeTimer"
>
<!-- Icon -->
<span
class="icon-toast-done rounded-full bg-white text-2xl dark:bg-gray-900"
:class="iconClasses[flash.type]"
:style="typeStyles[flash.type]['icon']"
></span>
<div class="flex flex-col gap-1.5">
<!-- Heading -->
<p class="text-base font-semibold dark:text-white">
@{{ typeHeadings[flash.type] }}
</p>
<!-- Message -->
<p
class="flex items-center break-all text-sm dark:text-white"
:style="typeStyles[flash.type]['message']"
>
@{{ flash.message }}
</p>
</div>
<button
class="relative ml-4 inline-flex rounded-full bg-white p-1.5 text-gray-400 dark:bg-gray-950"
@click="remove"
>
<span class="icon-cross-large text-2xl text-slate-800"></span>
<svg class="absolute inset-0 h-full w-full -rotate-90" viewBox="0 0 24 24">
<circle
class="text-gray-200"
stroke-width="1.5"
stroke="#D0D4DA"
fill="transparent"
r="10"
cx="12"
cy="12"
/>
<circle
class="text-blue-600 transition-all duration-100 ease-out"
stroke-width="1.5"
:stroke-dasharray="circumference"
:stroke-dashoffset="strokeDashoffset"
stroke-linecap="round"
:stroke="typeStyles[flash.type]['stroke']"
fill="transparent"
r="10"
cx="12"
cy="12"
/>
</svg>
</button>
</div>
</script>
<script type="module">
app.component('v-flash-item', {
template: '#v-flash-item-template',
props: ['flash'],
data() {
return {
iconClasses: {
success: 'icon-success',
error: 'icon-error',
warning: 'icon-warning',
info: 'icon-info',
},
typeHeadings: {
success: "@lang('admin::app.components.flash-group.success')",
error: "@lang('admin::app.components.flash-group.error')",
warning: "@lang('admin::app.components.flash-group.warning')",
info: "@lang('admin::app.components.flash-group.info')",
},
typeStyles: {
success: {
container: 'border-left: 8px solid #16A34A',
icon: 'color: #16A34A',
stroke: '#16A34A',
},
error: {
container: 'border-left: 8px solid #FF4D50',
icon: 'color: #FF4D50',
stroke: '#FF4D50',
},
warning: {
container: 'border-left: 8px solid #FBAD15',
icon: 'color: #FBAD15',
stroke: '#FBAD15',
},
info: {
container: 'border-left: 8px solid #0E90D9',
icon: 'color: #0E90D9',
stroke: '#0E90D9',
},
},
duration: 5000,
progress: 0,
circumference: 2 * Math.PI * 10,
timer: null,
isPaused: false,
remainingTime: 5000,
};
},
computed: {
strokeDashoffset() {
return this.circumference - (this.progress / 100) * this.circumference;
}
},
created() {
this.startTimer();
},
beforeUnmount() {
this.stopTimer();
},
methods: {
remove() {
this.$emit('onRemove', this.flash)
},
startTimer() {
const interval = 100;
const step = (100 / (this.duration / interval));
this.timer = setInterval(() => {
if (! this.isPaused) {
this.progress += step;
this.remainingTime -= interval;
if (this.progress >= 100) {
this.stopTimer();
this.remove();
}
}
}, interval);
},
stopTimer() {
clearInterval(this.timer);
},
pauseTimer() {
this.isPaused = true;
},
resumeTimer() {
this.isPaused = false;
},
}
});
</script>
@endpushOnce

View File

@@ -0,0 +1,339 @@
@props([
'type' => 'text',
'name' => '',
])
@switch($type)
@case('hidden')
@case('text')
@case('email')
@case('password')
@case('number')
<v-field
v-slot="{ field, errors }"
{{ $attributes->only(['name', ':name', 'value', ':value', 'v-model', 'rules', ':rules', 'label', ':label']) }}
name="{{ $name }}"
>
<input
type="{{ $type }}"
name="{{ $name }}"
v-bind="field"
:class="[errors.length ? 'border !border-red-600 hover:border-red-600' : '']"
{{ $attributes->except(['value', ':value', 'v-model', 'rules', ':rules', 'label', ':label'])->merge(['class' => 'w-full rounded border border-gray-200 px-2.5 py-2 text-sm font-normal text-gray-800 transition-all hover:border-gray-400 focus:border-gray-400 dark:border-gray-800 dark:bg-gray-900 dark:text-gray-300 dark:hover:border-gray-400 dark:focus:border-gray-400']) }}
/>
</v-field>
@break
@case('price')
<v-field
v-slot="{ field, errors }"
{{ $attributes->only(['name', ':name', 'value', ':value', 'v-model', 'rules', ':rules', 'label', ':label']) }}
name="{{ $name }}"
>
<div
class="flex w-full items-center overflow-hidden rounded-md border text-sm text-gray-600 transition-all focus-within:border-gray-400 hover:border-gray-400 dark:border-gray-800 dark:bg-gray-900 dark:text-gray-300 dark:hover:border-gray-400 dark:focus:border-gray-400"
:class="[errors.length ? 'border !border-red-600 hover:border-red-600' : '']"
>
@if (isset($currency))
<span {{ $currency->attributes->merge(['class' => 'py-2.5 text-gray-500 ltr:pl-4 rtl:pr-4']) }}>
{{ $currency }}
</span>
@else
<span class="py-2.5 text-gray-500 ltr:pl-4 rtl:pr-4">
{{ config('app.currency') }}
</span>
@endif
<input
type="text"
name="{{ $name }}"
v-bind="field"
{{ $attributes->except(['value', ':value', 'v-model', 'rules', ':rules', 'label', ':label'])->merge(['class' => 'w-full p-2.5 text-sm text-gray-600 dark:bg-gray-900 dark:text-gray-300']) }}
/>
</div>
</v-field>
@break
@case('file')
<v-field
v-slot="{ field, errors, handleChange, handleBlur }"
{{ $attributes->only(['name', ':name', 'value', ':value', 'v-model', 'rules', ':rules', 'label', ':label']) }}
name="{{ $name }}"
>
<input
type="{{ $type }}"
v-bind="{ name: field.name }"
:class="[errors.length ? 'border !border-red-600 hover:border-red-600' : '']"
{{ $attributes->except(['value', ':value', 'v-model', 'rules', ':rules', 'label', ':label'])->merge(['class' => 'w-full rounded-md border px-3 py-2.5 text-sm text-gray-600 transition-all hover:border-gray-400 focus:border-gray-400 dark:border-gray-800 dark:bg-gray-900 dark:text-gray-300 dark:file:bg-gray-800 dark:file:dark:text-white dark:hover:border-gray-400 dark:focus:border-gray-400']) }}
@change="handleChange"
@blur="handleBlur"
/>
</v-field>
@break
@case('color')
<v-field
name="{{ $name }}"
v-slot="{ field, errors }"
{{ $attributes->except('class') }}
>
<input
type="{{ $type }}"
:class="[errors.length ? 'border border-red-500' : '']"
v-bind="field"
{{ $attributes->except(['value'])->merge(['class' => 'w-full appearance-none rounded-md border text-sm text-gray-600 transition-all hover:border-gray-400 dark:text-gray-300 dark:hover:border-gray-400']) }}
>
</v-field>
@break
@case('textarea')
<v-field
v-slot="{ field, errors }"
{{ $attributes->only(['name', ':name', 'value', ':value', 'v-model', 'rules', ':rules', 'label', ':label']) }}
name="{{ $name }}"
>
<textarea
type="{{ $type }}"
name="{{ $name }}"
v-bind="field"
:class="[errors.length ? 'border !border-red-600 hover:border-red-600' : '']"
{{ $attributes->except(['value', ':value', 'v-model', 'rules', ':rules', 'label', ':label'])->merge(['class' => 'w-full rounded border border-gray-200 px-2.5 py-2 text-sm font-normal text-gray-800 transition-all hover:border-gray-400 focus:border-gray-400 dark:border-gray-800 dark:bg-gray-900 dark:text-gray-300 dark:hover:border-gray-400 dark:focus:border-gray-400']) }}
>
</textarea>
@if ($attributes->get('tinymce', false) || $attributes->get(':tinymce', false))
<x-admin::tinymce
:selector="'textarea#' . $attributes->get('id')"
::field="field"
/>
@endif
</v-field>
@break
@case('date')
<v-field
v-slot="{ field, errors }"
{{ $attributes->only(['name', ':name', 'value', ':value', 'v-model', 'rules', ':rules', 'label', ':label'])->merge(['rules' => 'regex:^\d{4}-\d{2}-\d{2}$']) }}
name="{{ $name }}"
>
<x-admin::flat-picker.date>
<input
name="{{ $name }}"
v-bind="field"
:class="[errors.length ? 'border !border-red-600 hover:border-red-600' : '']"
{{ $attributes->except(['value', ':value', 'v-model', 'rules', ':rules', 'label', ':label'])->merge(['class' => 'w-full rounded border border-gray-200 px-2.5 py-2 text-sm font-normal text-gray-800 transition-all hover:border-gray-400 focus:border-gray-400 dark:border-gray-800 dark:bg-gray-900 dark:text-gray-300 dark:hover:border-gray-400 dark:focus:border-gray-400']) }}
autocomplete="off"
/>
</x-admin::flat-picker.date>
</v-field>
@break
@case('datetime')
<v-field
v-slot="{ field, errors }"
{{ $attributes->only(['name', ':name', 'value', ':value', 'v-model', 'rules', ':rules', 'label', ':label'])->merge(['rules' => 'regex:^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$']) }}
name="{{ $name }}"
>
<x-admin::flat-picker.datetime>
<input
name="{{ $name }}"
v-bind="field"
:class="[errors.length ? 'border !border-red-600 hover:border-red-600' : '']"
{{ $attributes->except(['value', ':value', 'v-model', 'rules', ':rules', 'label', ':label'])->merge(['class' => 'w-full rounded border border-gray-200 px-2.5 py-2 text-sm font-normal text-gray-800 transition-all hover:border-gray-400 focus:border-gray-400 dark:border-gray-800 dark:bg-gray-900 dark:text-gray-300 dark:hover:border-gray-400 dark:focus:border-gray-400']) }}
autocomplete="off"
>
</x-admin::flat-picker.datetime>
</v-field>
@break
@case('select')
<v-field
v-slot="{ field, errors }"
{{ $attributes->only(['name', ':name', 'value', ':value', 'v-model', 'rules', ':rules', 'label', ':label']) }}
name="{{ $name }}"
>
<select
name="{{ $name }}"
v-bind="field"
:class="[errors.length ? 'border border-red-500' : '']"
{{ $attributes->except(['value', ':value', 'v-model', 'rules', ':rules', 'label', ':label'])->merge(['class' => 'custom-select w-full rounded border border-gray-200 px-2.5 py-2 text-sm font-normal text-gray-800 transition-all hover:border-gray-400 focus:border-gray-400 dark:border-gray-800 dark:bg-gray-900 dark:text-gray-300 dark:hover:border-gray-400']) }}
>
{{ $slot }}
</select>
</v-field>
@break
@case('multiselect')
<v-field
as="select"
v-slot="{ value }"
:class="[errors && errors['{{ $name }}'] ? 'border !border-red-600 hover:border-red-600' : '']"
{{ $attributes->except([])->merge(['class' => 'flex w-full flex-col rounded-md border bg-white px-3 py-2.5 text-sm font-normal text-gray-600 transition-all hover:border-gray-400 dark:border-gray-800 dark:bg-gray-900 dark:text-gray-300 dark:hover:border-gray-400']) }}
name="{{ $name }}"
multiple
>
{{ $slot }}
</v-field>
@break
@case('checkbox')
<v-field
v-slot="{ field }"
type="checkbox"
class="hidden"
{{ $attributes->only(['name', ':name', 'value', ':value', 'v-model', 'rules', ':rules', 'label', ':label', 'key', ':key']) }}
name="{{ $name }}"
>
<input
type="checkbox"
name="{{ $name }}"
v-bind="field"
class="peer sr-only"
{{ $attributes->except(['rules', 'label', ':label', 'key', ':key']) }}
/>
<v-checked-handler
:field="field"
checked="{{ $attributes->get('checked') }}"
>
</v-checked-handler>
</v-field>
<label
{{
$attributes
->except(['value', ':value', 'v-model', 'rules', ':rules', 'label', ':label', 'key', ':key'])
->merge(['class' => 'text-gray-500 icon-checkbox-outline peer-checked:icon-checkbox-select text-2xl peer-checked:text-blue-600'])
->merge(['class' => $attributes->get('disabled') ? 'cursor-not-allowed opacity-70' : 'cursor-pointer'])
}}
>
</label>
@break
@case('radio')
<v-field
type="radio"
class="hidden"
v-slot="{ field }"
{{ $attributes->only(['name', ':name', 'value', ':value', 'v-model', 'rules', ':rules', 'label', ':label', 'key', ':key']) }}
name="{{ $name }}"
>
<input
type="radio"
name="{{ $name }}"
v-bind="field"
{{ $attributes->except(['rules', 'label', ':label', 'key', ':key'])->merge(['class' => 'peer sr-only']) }}
/>
<v-checked-handler
class="hidden"
:field="field"
checked="{{ $attributes->get('checked') }}"
>
</v-checked-handler>
</v-field>
<label
{{ $attributes->except(['value', ':value', 'v-model', 'rules', ':rules', 'label', ':label', 'key', ':key'])->merge(['class' => 'icon-radio-normal peer-checked:icon-radio-selected cursor-pointer text-2xl peer-checked:text-blue-600']) }}
>
</label>
@break
@case('switch')
<label class="relative inline-flex cursor-pointer items-center">
<v-field
type="checkbox"
class="hidden"
v-slot="{ field }"
{{ $attributes->only(['name', ':name', 'value', ':value', 'v-model', 'rules', ':rules', 'label', ':label', 'key', ':key']) }}
name="{{ $name }}"
>
<input
type="checkbox"
name="{{ $name }}"
id="{{ $name }}"
class="peer sr-only"
v-bind="field"
{{ $attributes->except(['v-model', 'rules', ':rules', 'label', ':label', 'key', ':key']) }}
/>
<v-checked-handler
class="hidden"
:field="field"
checked="{{ $attributes->get('checked') }}"
>
</v-checked-handler>
</v-field>
<label
class="peer h-5 w-9 cursor-pointer rounded-full bg-gray-200 after:absolute after:top-0.5 after:h-4 after:w-4 after:rounded-full after:border after:border-gray-300 after:bg-white after:transition-all after:content-[''] peer-checked:bg-blue-600 peer-checked:after:border-white peer-focus:outline-none peer-focus:ring-blue-300 after:ltr:left-0.5 peer-checked:after:ltr:translate-x-full after:rtl:right-0.5 peer-checked:after:rtl:-translate-x-full dark:bg-gray-800 dark:after:border-white dark:after:bg-white dark:peer-checked:bg-gray-950"
for="{{ $name }}"
></label>
</label>
@break
@case('image')
<x-admin::media.images
name="{{ $name }}"
::class="[errors && errors['{{ $name }}'] ? 'border !border-red-600 hover:border-red-600' : '']"
{{ $attributes }}
/>
@break
@case('inline')
<x-admin::form.control-group.controls.inline.text {{ $attributes }}/>
@break
@case('custom')
<v-field {{ $attributes }}>
{{ $slot }}
</v-field>
@break
@case('tags')
<x-admin::form.control-group.controls.tags
:name="$name"
:data="$attributes->get(':data') ?? $attributes->get('data')"
{{ $attributes}}
/>
@break
@endswitch
@pushOnce('scripts')
<script
type="text/x-template"
id="v-checked-handler-template"
>
</script>
<script type="module">
app.component('v-checked-handler', {
template: '#v-checked-handler-template',
props: ['field', 'checked'],
mounted() {
if (this.checked == '') {
return;
}
this.field.checked = true;
this.field.onChange();
},
});
</script>
@endpushOnce

View File

@@ -0,0 +1,16 @@
@props([
'name' => null,
'controlName' => '',
])
<v-error-message
{{ $attributes }}
name="{{ $name ?? $controlName }}"
v-slot="{ message }"
>
<p
{{ $attributes->merge(['class' => 'mt-1 text-xs italic text-red-600']) }}
v-text="message"
>
</p>
</v-error-message>

View File

@@ -0,0 +1,3 @@
<div {{ $attributes->merge(['class' => 'mb-4']) }}>
{{ $slot }}
</div>

View File

@@ -0,0 +1,3 @@
<label {{ $attributes->merge(['class' => 'mb-1.5 flex items-center gap-1 text-sm font-normal text-gray-800 dark:text-white']) }}>
{{ $slot }}
</label>

View File

@@ -0,0 +1,40 @@
<!--
If a component has the as attribute, it indicates that it uses
the ajaxified form or some customized slot form.
-->
@if ($attributes->has('as'))
<v-form {{ $attributes }}>
{{ $slot }}
</v-form>
<!--
Otherwise, a traditional form will be provided with a minimal
set of configurations.
-->
@else
@props([
'method' => 'POST',
])
@php
$method = strtoupper($method);
@endphp
<v-form
method="{{ $method === 'GET' ? 'GET' : 'POST' }}"
:initial-errors="{{ json_encode($errors->getMessages()) }}"
v-slot="{ meta, errors, setValues }"
@invalid-submit="onInvalidSubmit"
{{ $attributes }}
>
@unless(in_array($method, ['HEAD', 'GET', 'OPTIONS']))
@csrf
@endunless
@if (! in_array($method, ['GET', 'POST']))
@method($method)
@endif
{{ $slot }}
</v-form>
@endif

View File

@@ -0,0 +1,113 @@
<!DOCTYPE html>
<html
lang="{{ app()->getLocale() }}"
dir="{{ in_array(app()->getLocale(), ['fa', 'ar']) ? 'rtl' : 'ltr' }}"
>
<head>
<title>{{ $title ?? '' }}</title>
<meta charset="UTF-8">
<meta
http-equiv="X-UA-Compatible"
content="IE=edge"
>
<meta
http-equiv="content-language"
content="{{ app()->getLocale() }}"
>
<meta
name="viewport"
content="width=device-width, initial-scale=1"
>
<meta
name="base-url"
content="{{ url()->to('/') }}"
>
@stack('meta')
{{
vite()->set(['src/Resources/assets/css/app.css', 'src/Resources/assets/js/app.js'], 'webform')
}}
<link
href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700;800&display=swap"
rel="stylesheet"
/>
<link
href="https://fonts.googleapis.com/css2?family=DM+Serif+Display&display=swap"
rel="stylesheet"
/>
@if ($favicon = core()->getConfigData('general.design.admin_logo.favicon'))
<link
type="image/x-icon"
href="{{ Storage::url($favicon) }}"
rel="shortcut icon"
sizes="16x16"
>
@else
<link
type="image/x-icon"
href="{{ vite()->asset('images/favicon.ico') }}"
rel="shortcut icon"
sizes="16x16"
/>
@endif
@stack('styles')
<style>
{!! core()->getConfigData('general.content.custom_scripts.custom_css') !!}
</style>
{!! view_render_event('webform.layout.head') !!}
</head>
<body>
{!! view_render_event('webform.layout.body.before') !!}
<div id="app">
<!-- Flash Message Blade Component -->
<x-web_form::flash-group />
{!! view_render_event('webform.layout.content.before') !!}
<!-- Page Content Blade Component -->
{{ $slot }}
{!! view_render_event('webform.layout.content.after') !!}
</div>
{!! view_render_event('webform.layout.body.after') !!}
@stack('scripts')
{!! view_render_event('webform.layout.vue-app-mount.before') !!}
<script>
/**
* Load event, the purpose of using the event is to mount the application
* after all of our Vue components which is present in blade file have
* been registered in the app. No matter what app.mount() should be
* called in the last.
*/
window.addEventListener("load", function(event) {
app.mount("#app");
});
</script>
{!! view_render_event('webform.layout.vue-app-mount.after') !!}
<script type="text/javascript">
{!! core()->getConfigData('general.content.custom_scripts.custom_javascript') !!}
</script>
</body>
</html>

View File

@@ -0,0 +1,27 @@
<!-- Spinner -->
@props(['color' => 'currentColor'])
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
aria-hidden="true"
viewBox="0 0 24 24"
{{ $attributes->merge(['class' => 'h-5 w-5 animate-spin']) }}
>
<circle
class="opacity-25"
cx="12"
cy="12"
r="10"
stroke="{{ $color }}"
stroke-width="4"
>
</circle>
<path
class="opacity-75"
fill="{{ $color }}"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
>
</path>
</svg>

View File

@@ -0,0 +1,257 @@
@foreach ($webForm->attributes as $attribute)
@php
$parentAttribute = $attribute->attribute;
$fieldName = $parentAttribute->entity_type . '[' . $parentAttribute->code . ']';
$validations = $attribute->is_required ? 'required' : '';
@endphp
<x-web_form::form.control-group>
<x-web_form::form.control-group.label
:for="$fieldName"
class="{{ $validations }}"
style="color: {{ $webForm->attribute_label_color }} !important;"
>
{{ $attribute->name ?? $parentAttribute->name }}
</x-web_form::form.control-group.label>
@switch($parentAttribute->type)
@case('text')
<x-web_form::form.control-group.control
type="text"
:name="$fieldName"
:id="$fieldName"
:rules="$validations"
:label="$attribute->name ?? $parentAttribute->name"
:placeholder="$attribute->placeholder"
/>
<x-web_form::form.control-group.error :control-name="$fieldName" />
@break
@case('price')
<x-web_form::form.control-group.control
type="text"
:name="$fieldName"
:id="$fieldName"
:rules="$validations.'|numeric'"
:label="$attribute->name ?? $parentAttribute->name"
:placeholder="$attribute->placeholder"
/>
<x-web_form::form.control-group.error :control-name="$fieldName" />
@break
@case('email')
<x-web_form::form.control-group.control
type="email"
name="{{ $fieldName }}[0][value]"
id="{{ $fieldName }}[0][value]"
rules="{{ $validations }}|email"
:label="$attribute->name ?? $parentAttribute->name"
:placeholder="$attribute->placeholder"
/>
<x-web_form::form.control-group.control
type="hidden"
name="{{ $fieldName }}[0][label]"
id="{{ $fieldName }}[0][label]"
rules="required"
value="work"
/>
<x-web_form::form.control-group.error control-name="{{ $fieldName }}[0][value]" />
@break
@case('checkbox')
@php
$options = $parentAttribute->lookup_type
? app('Webkul\Attribute\Repositories\AttributeRepository')->getLookUpOptions($parentAttribute->lookup_type)
: $parentAttribute->options()->orderBy('sort_order')->get();
@endphp
@foreach ($options as $option)
<x-web_form::form.control-group class="!mb-2 flex select-none items-center gap-2.5">
<x-web_form::form.control-group.control
type="checkbox"
name="{{ $fieldName }}[]"
id="{{ $fieldName }}[]"
value="{{ $option->id }}"
for="{{ $fieldName }}[]"
/>
<label
class="cursor-pointer text-xs font-medium text-gray-600 dark:text-gray-300"
for="{{ $fieldName }}[]"
>
@lang('web_form::app.catalog.attributes.edit.is-required')
</label>
</x-web_form::form.control-group>
@endforeach
@case('file')
@case('image')
<x-web_form::form.control-group.control
type="file"
:name="$fieldName"
:id="$fieldName"
:rules="$validations"
:placeholder="$attribute->placeholder"
:label="$attribute->name ?? $parentAttribute->name"
/>
<x-web_form::form.control-group.error control-name="{{ $fieldName }}" />
@break;
@case('phone')
<x-web_form::form.control-group.control
type="text"
name="{{ $fieldName }}[0][value]"
id="{{ $fieldName }}[0][value]"
rules="{{ $validations }}|phone"
:label="$attribute->name ?? $parentAttribute->name"
:placeholder="$attribute->placeholder"
/>
<x-web_form::form.control-group.control
type="hidden"
name="{{ $fieldName }}[0][label]"
id="{{ $fieldName }}[0][label]"
rules="required"
value="work"
/>
<x-web_form::form.control-group.error control-name="{{ $fieldName }}[0][value]" />
@break
@case('date')
<x-web_form::form.control-group.control
type="date"
:name="$fieldName"
:id="$fieldName"
:rules="$validations"
:label="$attribute->name ?? $parentAttribute->name"
:placeholder="$attribute->placeholder"
/>
<x-web_form::form.control-group.error :control-name="$fieldName" />
@break
@case('datetime')
<x-web_form::form.control-group.control
type="datetime"
:name="$fieldName"
:id="$fieldName"
:rules="$validations"
:label="$attribute->name ?? $parentAttribute->name"
:placeholder="$attribute->placeholder"
/>
<x-web_form::form.control-group.error :control-name="$fieldName" />
@break
@case('select')
@case('lookup')
@php
$options = $parentAttribute->lookup_type
? app('Webkul\Attribute\Repositories\AttributeRepository')->getLookUpOptions($parentAttribute->lookup_type)
: $parentAttribute->options()->orderBy('sort_order')->get();
@endphp
<x-web_form::form.control-group.control
type="select"
:name="$fieldName"
:id="$fieldName"
:rules="$validations"
:label="$attribute->name ?? $parentAttribute->name"
:placeholder="$attribute->placeholder"
>
@foreach ($options as $option)
<option value="{{ $option->id }}">{{ $option->name }}</option>
@endforeach
</x-web_form::form.control-group.control>
<x-web_form::form.control-group.error :control-name="$fieldName" />
@break
@case('multiselect')
@php
$options = $parentAttribute->lookup_type
? app('Webkul\Attribute\Repositories\AttributeRepository')->getLookUpOptions($parentAttribute->lookup_type)
: $parentAttribute->options()->orderBy('sort_order')->get();
@endphp
<x-web_form::form.control-group.control
type="select"
id="{{ $fieldName }}"
name="{{ $fieldName }}[]"
:rules="$validations"
:label="$attribute->name ?? $parentAttribute->name"
:placeholder="$attribute->placeholder"
>
@foreach ($options as $option)
<option value="{{ $option->id }}">{{ $option->name }}</option>
@endforeach
</x-web_form::form.control-group.control>
<x-web_form::form.control-group.error :control-name="$fieldName" />
@break
@case('checkbox')
<div class="checkbox-control">
@php
$options = $parentAttribute->lookup_type
? app('Webkul\Attribute\Repositories\AttributeRepository')->getLookUpOptions($parentAttribute->lookup_type)
: $parentAttribute->options()->orderBy('sort_order')->get();
@endphp
@foreach ($options as $option)
<span class="checkbox">
<input
type="checkbox"
name="{{ $fieldName }}[]"
value="{{ $option->id }}"
/>
<label class="checkbox-view" style="display: inline;"></label>
{{ $option->name }}
</span>
@endforeach
</div>
<p
id="{{ $fieldName }}[]-error"
class="error-message mt-1 text-xs italic text-red-600"
></p>
@break
@case('boolean')
<x-web_form::form.control-group.control
type="select"
:name="$fieldName"
:id="$fieldName"
:rules="$validations"
:label="$attribute->name ?? $parentAttribute->name"
:placeholder="$attribute->placeholder"
>
<option value="1">Yes</option>
<option value="0">No</option>
</x-web_form::form.control-group.control>
<x-web_form::form.control-group.error :control-name="$fieldName" />
@break
@endswitch
</x-web_form::form.control-group>
@endforeach

View File

@@ -0,0 +1,3 @@
(function() {
document.write(`{!! view('web_form::settings.web-forms.preview', compact('webForm'))->render() !!}`.replaceAll('$', '\$'));
})();

View File

@@ -0,0 +1,156 @@
<x-web_form::layouts>
<x-slot:title>
{{ $webForm->title }}
</x-slot>
<!-- Web Form -->
<v-web-form>
<div class="flex h-[100vh] items-center justify-center">
<div class="flex flex-col items-center gap-5">
<x-web_form::spinner />
</div>
</div>
</v-web-form>
@pushOnce('scripts')
<script
type="text/template"
id="v-web-form-template"
>
<div
class="flex h-[100vh] items-center justify-center"
style="background-color: {{ $webForm->background_color }}"
>
<div class="flex flex-col items-center gap-5">
<!-- Logo -->
<img
class="w-max"
src="{{ vite()->asset('images/logo.svg') }}"
alt="{{ config('app.name') }}"
/>
<h1
class="text-2xl font-bold"
style="color: {{ $webForm->form_title_color }} !important;"
>
{{ $webForm->title }}
</h1>
<p class="mt-2 text-base text-gray-600">{{ $webForm->description }}</p>
<div
class="box-shadow flex min-w-[300px] flex-col rounded-lg border border-gray-200 bg-white p-4 dark:bg-gray-900"
style="background-color: {{ $webForm->form_background_color }}"
>
{!! view_render_event('web_forms.web_forms.form_controls.before', ['webForm' => $webForm]) !!}
<!-- Webform Form -->
<x-web_form::form
v-slot="{ meta, values, errors, handleSubmit }"
as="div"
ref="modalForm"
>
<form
@submit="handleSubmit($event, create)"
ref="webForm"
>
@include('web_form::settings.web-forms.controls')
<div class="flex justify-center">
<x-web_form::button
class="primary-button"
:title="$webForm->submit_button_label"
::loading="isStoring"
::disabled="isStoring"
style="background-color: {{ $webForm->form_submit_button_color }} !important"
/>
</div>
</form>
</x-web_form::form>
{!! view_render_event('web_forms.web_forms.form_controls.after', ['webForm' => $webForm]) !!}
</div>
</div>
</div>
</script>
<script type="module">
app.component('v-web-form', {
template: '#v-web-form-template',
data() {
return {
isStoring: false,
};
},
methods: {
create(params, { resetForm, setErrors }) {
this.isStoring = true;
const formData = new FormData(this.$refs.webForm);
let inputNames = Array.from(formData.keys());
inputNames = inputNames.reduce((acc, name) => {
const dotName = name.replace(/\[([^\]]+)\]/g, '.$1');
acc[dotName] = name;
return acc;
}, {});
this.$axios
.post('{{ route('admin.settings.web_forms.form_store', $webForm->id) }}', formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
})
.then(response => {
resetForm();
this.$refs.webForm.reset();
this.$emitter.emit('add-flash', { type: 'success', message: response.data.message });
})
.catch(error => {
if (error.response.data.redirect) {
window.location.href = error.response.data.redirect;
return;
}
if (! error.response.data.errors) {
this.$emitter.emit('add-flash', { type: 'error', message: error.response.data.message });
return;
}
const laravelErrors = error.response.data.errors || {};
const mappedErrors = {};
for (
const [dotKey, messages]
of Object.entries(laravelErrors)
) {
const inputName = inputNames[dotKey];
if (
inputName
&& messages.length
) {
mappedErrors[inputName] = messages[0];
}
}
setErrors(mappedErrors);
})
.finally(() => {
this.isStoring = false;
});
}
}
});
</script>
@endPushOnce
</x-web_form::layouts>

View File

@@ -0,0 +1,16 @@
<?php
use Illuminate\Support\Facades\Route;
use Webkul\WebForm\Http\Controllers\WebFormController;
Route::controller(WebFormController::class)->middleware(['web', 'admin_locale'])->prefix('web-forms')->group(function () {
Route::get('forms/{id}/form.js', 'formJS')->name('admin.settings.web_forms.form_js');
Route::get('forms/{id}/form.html', 'preview')->name('admin.settings.web_forms.preview');
Route::post('forms/{id}', 'formStore')->name('admin.settings.web_forms.form_store');
Route::group(['middleware' => ['user']], function () {
Route::get('form/{id}/form.html', 'view')->name('admin.settings.web_forms.view');
});
});

View File

@@ -0,0 +1,33 @@
<?php
namespace Webkul\WebForm\Rules;
use Illuminate\Contracts\Validation\Rule;
class PhoneNumber implements Rule
{
/**
* Determine if the validation rule passes.
*
* @param string $attribute
* @param mixed $value
* @return bool
*/
public function passes($attribute, $value)
{
// This regular expression allows phone numbers with the following conditions:
// - The phone number can start with an optional "+" sign.
// - After the "+" sign, there should be one or more digits.
return preg_match('/^\+?\d+$/', $value);
}
/**
* Get the validation error message.
*
* @return string
*/
public function message()
{
return __('web_form::app.validations.invalid-phone-number');
}
}

View File

@@ -0,0 +1,47 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ["./src/Resources/**/*.blade.php", "./src/Resources/**/*.js"],
theme: {
container: {
center: true,
screens: {
"2xl": "1920px",
},
padding: {
DEFAULT: "16px",
},
},
screens: {
sm: "525px",
md: "768px",
lg: "1024px",
xl: "1240px",
"2xl": "1920px",
},
extend: {
colors: {
brandColor: '#0E90D9',
},
fontFamily: {
inter: ['Inter'],
icon: ['icomoon']
}
},
},
darkMode: 'class',
plugins: [],
safelist: [
{
pattern: /icon-/,
}
]
};

View File

@@ -0,0 +1,46 @@
import { defineConfig, loadEnv } from "vite";
import vue from "@vitejs/plugin-vue";
import laravel from "laravel-vite-plugin";
import path from "path";
export default defineConfig(({ mode }) => {
const envDir = "../../../";
Object.assign(process.env, loadEnv(mode, envDir));
return {
build: {
emptyOutDir: true,
},
envDir,
server: {
host: process.env.VITE_HOST || "localhost",
port: process.env.VITE_PORT || 5174,
},
plugins: [
vue(),
laravel({
hotFile: "../../../public/webform-vite.hot",
publicDirectory: "../../../public",
buildDirectory: "webform/build",
input: [
"src/Resources/assets/css/app.css",
"src/Resources/assets/js/app.js",
],
refresh: true,
}),
],
experimental: {
renderBuiltUrl(filename, { hostId, hostType, type }) {
if (hostType === "css") {
return path.basename(filename);
}
},
},
};
});