diff --git a/plugins/akami/coffe30/Plugin.php b/plugins/akami/coffe30/Plugin.php new file mode 100644 index 00000000..b8e240b2 --- /dev/null +++ b/plugins/akami/coffe30/Plugin.php @@ -0,0 +1,14 @@ + + + + + diff --git a/plugins/akami/coffe30/controllers/slider/_reorder_toolbar.htm b/plugins/akami/coffe30/controllers/slider/_reorder_toolbar.htm new file mode 100644 index 00000000..12ed9927 --- /dev/null +++ b/plugins/akami/coffe30/controllers/slider/_reorder_toolbar.htm @@ -0,0 +1,3 @@ +
+ +
\ No newline at end of file diff --git a/plugins/akami/coffe30/controllers/slider/config_form.yaml b/plugins/akami/coffe30/controllers/slider/config_form.yaml new file mode 100644 index 00000000..4dd8742e --- /dev/null +++ b/plugins/akami/coffe30/controllers/slider/config_form.yaml @@ -0,0 +1,10 @@ +name: Slider +form: $/akami/coffe30/models/slider/fields.yaml +modelClass: Akami\Coffe30\Models\Slider +defaultRedirect: akami/coffe30/slider +create: + redirect: 'akami/coffe30/slider/update/:id' + redirectClose: akami/coffe30/slider +update: + redirect: akami/coffe30/slider + redirectClose: akami/coffe30/slider diff --git a/plugins/akami/coffe30/controllers/slider/config_list.yaml b/plugins/akami/coffe30/controllers/slider/config_list.yaml new file mode 100644 index 00000000..187d9950 --- /dev/null +++ b/plugins/akami/coffe30/controllers/slider/config_list.yaml @@ -0,0 +1,12 @@ +list: $/akami/coffe30/models/slider/columns.yaml +modelClass: Akami\Coffe30\Models\Slider +title: Slider +noRecordsMessage: 'backend::lang.list.no_records' +showSetup: true +showCheckboxes: true +recordsPerPage: 20 +toolbar: + buttons: list_toolbar + search: + prompt: 'backend::lang.list.search_prompt' +recordUrl: 'akami/coffe30/slider/update/:id' diff --git a/plugins/akami/coffe30/controllers/slider/config_reorder.yaml b/plugins/akami/coffe30/controllers/slider/config_reorder.yaml new file mode 100644 index 00000000..b4d06597 --- /dev/null +++ b/plugins/akami/coffe30/controllers/slider/config_reorder.yaml @@ -0,0 +1,4 @@ +title: Slider +modelClass: Akami\Coffe30\Models\Slider +toolbar: + buttons: reorder_toolbar diff --git a/plugins/akami/coffe30/controllers/slider/create.htm b/plugins/akami/coffe30/controllers/slider/create.htm new file mode 100644 index 00000000..e107084a --- /dev/null +++ b/plugins/akami/coffe30/controllers/slider/create.htm @@ -0,0 +1,46 @@ + + + + +fatalError): ?> + + 'layout']) ?> + +
+ formRender() ?> +
+ +
+
+ + + + + +
+
+ + + + +

fatalError)) ?>

+

+ \ No newline at end of file diff --git a/plugins/akami/coffe30/controllers/slider/index.htm b/plugins/akami/coffe30/controllers/slider/index.htm new file mode 100644 index 00000000..ea43a363 --- /dev/null +++ b/plugins/akami/coffe30/controllers/slider/index.htm @@ -0,0 +1 @@ +listRender() ?> diff --git a/plugins/akami/coffe30/controllers/slider/preview.htm b/plugins/akami/coffe30/controllers/slider/preview.htm new file mode 100644 index 00000000..dd7f873b --- /dev/null +++ b/plugins/akami/coffe30/controllers/slider/preview.htm @@ -0,0 +1,22 @@ + + + + +fatalError): ?> + +
+ formRenderPreview() ?> +
+ + +

fatalError) ?>

+ + +

+ + + +

\ No newline at end of file diff --git a/plugins/akami/coffe30/controllers/slider/reorder.htm b/plugins/akami/coffe30/controllers/slider/reorder.htm new file mode 100644 index 00000000..100fae43 --- /dev/null +++ b/plugins/akami/coffe30/controllers/slider/reorder.htm @@ -0,0 +1,8 @@ + + + + +reorderRender() ?> \ No newline at end of file diff --git a/plugins/akami/coffe30/controllers/slider/update.htm b/plugins/akami/coffe30/controllers/slider/update.htm new file mode 100644 index 00000000..8d08cb8c --- /dev/null +++ b/plugins/akami/coffe30/controllers/slider/update.htm @@ -0,0 +1,54 @@ + + + + +fatalError): ?> + + 'layout']) ?> + +
+ formRender() ?> +
+ +
+
+ + + + + + + +
+
+ + + +

fatalError)) ?>

+

+ \ No newline at end of file diff --git a/plugins/akami/coffe30/lang/en/lang.php b/plugins/akami/coffe30/lang/en/lang.php new file mode 100644 index 00000000..1baf2698 --- /dev/null +++ b/plugins/akami/coffe30/lang/en/lang.php @@ -0,0 +1,6 @@ + [ + 'name' => 'Coffe30', + 'description' => '' + ] +]; \ No newline at end of file diff --git a/plugins/akami/coffe30/models/Slider.php b/plugins/akami/coffe30/models/Slider.php new file mode 100644 index 00000000..7cd97175 --- /dev/null +++ b/plugins/akami/coffe30/models/Slider.php @@ -0,0 +1,29 @@ +engine = 'InnoDB'; + $table->increments('id')->unsigned(); + $table->string('img'); + $table->string('url')->nullable(); + }); + } + + public function down() + { + Schema::dropIfExists('akami_coffe30_slider'); + } +} diff --git a/plugins/akami/coffe30/updates/version.yaml b/plugins/akami/coffe30/updates/version.yaml new file mode 100644 index 00000000..136141c7 --- /dev/null +++ b/plugins/akami/coffe30/updates/version.yaml @@ -0,0 +1,5 @@ +1.0.1: + - 'Initialize plugin.' +1.0.2: + - 'Created table akami_coffe30_slider' + - builder_table_create_akami_coffe30_slider.php diff --git a/plugins/andreishilov/utmsaver/Plugin.php b/plugins/andreishilov/utmsaver/Plugin.php new file mode 100644 index 00000000..15172b24 --- /dev/null +++ b/plugins/andreishilov/utmsaver/Plugin.php @@ -0,0 +1,70 @@ + 'andreishilov.utmsaver::lang.plugin.name', + 'description' => 'andreishilov.utmsaver::lang.plugin.description', + 'author' => 'andreishilov.utmsaver::lang.plugin.author', + 'icon' => 'icon-magnet' + ]; + } + + public function boot() + { + if (!App::runningInBackend()) { + Event::listen('cms.page.start', function (Controller $controller) { + Facades\UTM::save(); + }); + } + } + + public function register() + { + App::singleton('AndreiShilov.UTM', function () { + return UTM::instance(); + }); + } + + public function registerSettings() + { + return [ + 'config' => [ + 'label' => 'andreishilov.utmsaver::lang.settings.label', + 'description' => 'andreishilov.utmsaver::lang.settings.description', + 'icon' => 'icon-magnet', + 'class' => 'AndreiShilov\UTMSaver\Models\Settings', + 'permissions' => ['andreishilov.utmsaver.settings'], + ] + ]; + } + + public function registerPermissions() + { + return [ + 'andreishilov.utmsaver.settings' => [ + 'label' => 'andreishilov.utmsaver::lang.plugin.name', + ], + ]; + } + + public function registerMarkupTags() + { + return [ + 'functions' => [ + 'utm' => function ($utmTag) { + return Facades\UTM::get($utmTag); + }, + ] + ]; + } +} diff --git a/plugins/andreishilov/utmsaver/classes/UTM.php b/plugins/andreishilov/utmsaver/classes/UTM.php new file mode 100644 index 00000000..9f16443f --- /dev/null +++ b/plugins/andreishilov/utmsaver/classes/UTM.php @@ -0,0 +1,74 @@ +utm[$item] = $value; + } + } + + if (count(request()->all()) > 0) { + $UTM = []; + foreach (request()->all() as $key => $value) { + if (in_array($key, self::UTM)) { + $UTM[$key] = trim($value); + } + } + + if (count($UTM) > 0) { + $ttl = Settings::get('cookie_ttl', 30) * 24 * 60; + foreach (self::UTM as $tag) { + if (isset($UTM[$tag])) { + $this->utm[$tag] = $UTM[$tag]; + Cookie::queue(Cookie::make($tag, $UTM[$tag], $ttl)); + } else { + unset($this->utm[$tag]); + Cookie::forget($tag); + } + } + } + } + } + + /** + * @param $utmTag + * @return string|null + */ + public function get($utmTag) + { + return $this->utm[$utmTag] ?? null; + } + + /** + * @return array + */ + public function all() + { + return $this->utm; + } +} \ No newline at end of file diff --git a/plugins/andreishilov/utmsaver/composer.json b/plugins/andreishilov/utmsaver/composer.json new file mode 100644 index 00000000..41543075 --- /dev/null +++ b/plugins/andreishilov/utmsaver/composer.json @@ -0,0 +1,8 @@ +{ + "name": "andreishilov/utmsaver-plugin", + "type": "october-plugin", + "description": "None", + "require": { + "composer/installers": "~1.0" + } +} \ No newline at end of file diff --git a/plugins/andreishilov/utmsaver/facades/UTM.php b/plugins/andreishilov/utmsaver/facades/UTM.php new file mode 100644 index 00000000..f3bad5c5 --- /dev/null +++ b/plugins/andreishilov/utmsaver/facades/UTM.php @@ -0,0 +1,17 @@ + [ + 'name' => 'UTM Saver', + 'description' => 'Plugin allows to save visitors utm to cookie', + 'author' => 'Andrei Shilov', + ], + 'config' => [ + 'cookie_ttl' => [ + 'title' => 'UTM ttl, days', + 'description' => 'Cookies time of living' + ], + 'cookie_prefix' => [ + 'title' => 'Prefix cookie', + 'description' => 'You can set cookie prefix to avoid conflicts with another modules' + ], + ], + 'settings' => [ + 'label' => 'UTM Saver', + 'description' => 'Setting of UTM Saver module', + ], +]; \ No newline at end of file diff --git a/plugins/andreishilov/utmsaver/lang/ru/lang.php b/plugins/andreishilov/utmsaver/lang/ru/lang.php new file mode 100644 index 00000000..38fffe87 --- /dev/null +++ b/plugins/andreishilov/utmsaver/lang/ru/lang.php @@ -0,0 +1,22 @@ + [ + 'name' => 'UTM Saver', + 'description' => 'Плагин позволяет сохранять utm-метки посетителя в cookie', + 'author' => 'Андрей Шилов', + ], + 'config' => [ + 'cookie_ttl' => [ + 'title' => 'UTM ttl, дней', + 'description' => 'Время жизни cookie' + ], + 'cookie_prefix' => [ + 'title' => 'Префикс cookie', + 'description' => 'Можно указать для избежания конфликтов имен cookie с другими модулями' + ], + ], + 'settings' => [ + 'label' => 'UTM Saver', + 'description' => 'Настройки модуля UTM Saver', + ], +]; \ No newline at end of file diff --git a/plugins/andreishilov/utmsaver/models/Settings.php b/plugins/andreishilov/utmsaver/models/Settings.php new file mode 100644 index 00000000..0fdf155c --- /dev/null +++ b/plugins/andreishilov/utmsaver/models/Settings.php @@ -0,0 +1,13 @@ + 'andreishilov.utmshopaholic::lang.plugin.name', + 'description' => 'andreishilov.utmshopaholic::lang.plugin.description', + 'author' => 'andreishilov.utmshopaholic::lang.plugin.author', + 'icon' => 'icon-magnet' + ]; + } + + public function registerPermissions() + { + return [ + 'andreishilov.utmshopaholic.widgets' => [ + 'label' => 'andreishilov.utmshopaholic::lang.plugin.name', + ], + ]; + } + + public function registerReportWidgets() + { + return [ + 'AndreiShilov\UTMShopaholic\ReportWidgets\SourcesOfOrders' => [ + 'label' => 'andreishilov.utmshopaholic::lang.reportwidget.sourcesoforders.name', + 'context' => 'dashboard', + 'permissions' => [ + 'andreishilov.utmshopaholic.widgets', + ], + ], + ]; + } + + public function boot() + { + Event::listen(OrderProcessor::EVENT_ORDER_CREATED, function ($obOrder) { + if (!App::runningInBackend()) { + UTM::save(); + $utmParameters = UTM::all(); + if (count($utmParameters) > 0) { + $utm = new Models\UTM($utmParameters); + $utm->order()->associate($obOrder); + $utm->save(); + } + } + }); + + Order::extend(function ($model) { + $model->hasOne['utm'] = [ + Models\UTM::class, + 'delete' => true + ]; + + $model->addDynamicMethod('scopeHasUtmLike', function (Builder $query, $value) { + $query->whereHas('utm', function (Builder $query) use ($value) { + $query->where('utm_source', 'like', '%' . $value . '%') + ->orWhere('utm_campaign', 'like', '%' . $value . '%') + ->orWhere('utm_medium', 'like', '%' . $value . '%') + ->orWhere('utm_term', 'like', '%' . $value . '%') + ->orWhere('utm_content', 'like', '%' . $value . '%'); + }); + }); + }); + + Event::listen('backend.form.extendFields', function (Form $widget) { + if (!$widget->getController() instanceof Orders || !$widget->model instanceof Order) { + return; + } + + $widget->addTabFields([ + 'utm' => [ + 'type' => 'partial', + 'tab' => 'UTM', + 'path' => '$/andreishilov/utmshopaholic/controllers/orders/_field_utmparameters.htm', + ] + ]); + }); + + Orders::extend(function ($controller) { + $myConfigPath = '$/andreishilov/utmshopaholic/controllers/orders/config_relation.yaml'; + + if (!isset($controller->relationConfig)) { + $controller->addDynamicProperty('relationConfig'); + } + + $controller->relationConfig = $controller->mergeConfig( + $controller->relationConfig, + $myConfigPath + ); + }); + + Orders::extendListColumns(function (Lists $list, Model $model) { + if (!$model instanceof Order) { + return; + } + + $utm = [ + 'utm_source', + 'utm_campaign', + 'utm_medium', + 'utm_term', + 'utm_content', + ]; + + foreach ($utm as $parameter) { + $list->addColumns([ + 'utm[' . $parameter . ']' => [ + 'label' => Lang::get('andreishilov.utmshopaholic::lang.utm.' . $parameter), + 'sortable' => false, + ], + ]); + } + }); + + Event::listen('backend.filter.extendScopes', function (Filter $filterWidget) { + if (!$filterWidget->getController() instanceof Orders) { + return; + } + + $filterWidget->addScopes([ + 'utm' => [ + 'label' => 'utm', + 'type' => 'text', + 'scope' => 'hasUtmLike', + ] + ]); + }); + } +} \ No newline at end of file diff --git a/plugins/andreishilov/utmshopaholic/composer.json b/plugins/andreishilov/utmshopaholic/composer.json new file mode 100644 index 00000000..65e413f1 --- /dev/null +++ b/plugins/andreishilov/utmshopaholic/composer.json @@ -0,0 +1,8 @@ +{ + "name": "andreishilov/utmshopaholic-plugin", + "type": "october-plugin", + "description": "None", + "require": { + "composer/installers": "~1.0" + } +} \ No newline at end of file diff --git a/plugins/andreishilov/utmshopaholic/controllers/orders/_field_utmparameters.htm b/plugins/andreishilov/utmshopaholic/controllers/orders/_field_utmparameters.htm new file mode 100644 index 00000000..f9a969c4 --- /dev/null +++ b/plugins/andreishilov/utmshopaholic/controllers/orders/_field_utmparameters.htm @@ -0,0 +1 @@ +relationRender('utm') ?> \ No newline at end of file diff --git a/plugins/andreishilov/utmshopaholic/controllers/orders/config_relation.yaml b/plugins/andreishilov/utmshopaholic/controllers/orders/config_relation.yaml new file mode 100644 index 00000000..3a78454c --- /dev/null +++ b/plugins/andreishilov/utmshopaholic/controllers/orders/config_relation.yaml @@ -0,0 +1,8 @@ +utm: + label: UTM + view: + form: $/andreishilov/utmshopaholic/models/utm/fields.yaml + toolbarButtons: update|delete + manage: + form: $/andreishilov/utmshopaholic/models/utm/fields.yaml + list: $/andreishilov/utmshopaholic/models/utm/columns.yaml \ No newline at end of file diff --git a/plugins/andreishilov/utmshopaholic/lang/en/lang.php b/plugins/andreishilov/utmshopaholic/lang/en/lang.php new file mode 100644 index 00000000..cec17fd8 --- /dev/null +++ b/plugins/andreishilov/utmshopaholic/lang/en/lang.php @@ -0,0 +1,22 @@ + [ + 'name' => 'UTM Analytics for Shopaholic', + 'description' => 'Plugin provides UTM-analytics for Shopaholic orders', + 'author' => 'Andrei Shilov', + ], + 'reportwidget' => [ + 'sourcesoforders' => [ + 'name' => 'Sources of orders', + 'parameters' => [ + 'days' => 'Days' + ] + ] + ], + 'utm' => [ + 'utm_source' => 'UTM Source', + 'utm_campaign' => 'UTM Campaign', + 'utm_medium' => 'UTM Medium', + 'utm_term' => 'UTM Term', + 'utm_content' => 'UTM Content', + ] +]; \ No newline at end of file diff --git a/plugins/andreishilov/utmshopaholic/lang/ru/lang.php b/plugins/andreishilov/utmshopaholic/lang/ru/lang.php new file mode 100644 index 00000000..0a18751a --- /dev/null +++ b/plugins/andreishilov/utmshopaholic/lang/ru/lang.php @@ -0,0 +1,22 @@ + [ + 'name' => 'UTM Аналитика для Shopaholic', + 'description' => 'Плагин предоставляет аналитику UTM для заказов Shopaholic', + 'author' => 'Андрей Шилов', + ], + 'reportwidget' => [ + 'sourcesoforders' => [ + 'name' => 'Источники заказов', + 'parameters' => [ + 'days' => 'Дней' + ] + ] + ], + 'utm' => [ + 'utm_source' => 'UTM Source', + 'utm_campaign' => 'UTM Campaign', + 'utm_medium' => 'UTM Medium', + 'utm_term' => 'UTM Term', + 'utm_content' => 'UTM Content', + ] +]; \ No newline at end of file diff --git a/plugins/andreishilov/utmshopaholic/models/UTM.php b/plugins/andreishilov/utmshopaholic/models/UTM.php new file mode 100644 index 00000000..bce596d9 --- /dev/null +++ b/plugins/andreishilov/utmshopaholic/models/UTM.php @@ -0,0 +1,49 @@ + [ + Order::class + ], + ]; + + public function setUtmSourceAttribute($value) + { + $this->attributes['utm_source'] = Str::substr($value, 0 , 128); + } + + public function setUtmCampaignAttribute($value) + { + $this->attributes['utm_campaign'] = Str::substr($value, 0 , 128); + } + + public function setUtmMediumAttribute($value) + { + $this->attributes['utm_medium'] = Str::substr($value, 0 , 128); + } + + public function setUtmTermAttribute($value) + { + $this->attributes['utm_term'] = Str::substr($value, 0 , 512); + } + + public function setUtmContentAttribute($value) + { + $this->attributes['utm_content'] = Str::substr($value, 0 , 128); + } +} diff --git a/plugins/andreishilov/utmshopaholic/models/utm/columns.yaml b/plugins/andreishilov/utmshopaholic/models/utm/columns.yaml new file mode 100644 index 00000000..70523cc7 --- /dev/null +++ b/plugins/andreishilov/utmshopaholic/models/utm/columns.yaml @@ -0,0 +1,21 @@ +columns: + utm_source: + label: UTM Source + type: text + searchable: true + utm_campaign: + label: UTM Campaign + type: text + searchable: true + utm_medium: + label: UTM Medium + type: text + searchable: true + utm_term: + label: UTM Term + type: text + searchable: true + utm_content: + label: UTM Content + type: text + searchable: true \ No newline at end of file diff --git a/plugins/andreishilov/utmshopaholic/models/utm/fields.yaml b/plugins/andreishilov/utmshopaholic/models/utm/fields.yaml new file mode 100644 index 00000000..1be78e51 --- /dev/null +++ b/plugins/andreishilov/utmshopaholic/models/utm/fields.yaml @@ -0,0 +1,21 @@ +fields: + utm_source: + label: UTM Source + span: full + type: text + utm_campaign: + label: UTM Campaign + span: full + type: text + utm_medium: + label: UTM Medium + span: full + type: text + utm_term: + label: UTM Term + span: full + type: text + utm_content: + label: UTM Content + span: full + type: text \ No newline at end of file diff --git a/plugins/andreishilov/utmshopaholic/reportwidgets/SourcesOfOrders.php b/plugins/andreishilov/utmshopaholic/reportwidgets/SourcesOfOrders.php new file mode 100644 index 00000000..b2d7dd33 --- /dev/null +++ b/plugins/andreishilov/utmshopaholic/reportwidgets/SourcesOfOrders.php @@ -0,0 +1,56 @@ + [ + 'title' => 'Number of days', + 'default' => '30', + 'type' => 'string', + 'validationPattern' => '^[0-9]+$' + ], + ]; + } + + public function render() + { + $options = $this->getProperties(); + $dateFrom = Carbon::today()->addDays(-$options['days']); + + $query = Order::query() + ->join('andreishilov_utmshopaholic_utm', function (JoinClause $join) { + $join->on('andreishilov_utmshopaholic_utm.order_id', '=', 'lovata_orders_shopaholic_orders.id'); + }) + ->groupBy('andreishilov_utmshopaholic_utm.utm_source') + ->where('lovata_orders_shopaholic_orders.created_at', '>=', $dateFrom) + ->orderBy('amount', 'desc') + ->select([ + DB::raw('count(lovata_orders_shopaholic_orders.id) as amount'), + 'andreishilov_utmshopaholic_utm.utm_source as utm_source' + ]); + + $items = $query->get(); + + $this->vars['days'] = $options['days']; + $this->vars['data'] = [ + 'items' => $items->toArray(), + 'total' => 0, + ]; + + foreach ($items as $item) { + $this->vars['data']['total'] += $item['amount']; + } + + return $this->makePartial('widget'); + } +} \ No newline at end of file diff --git a/plugins/andreishilov/utmshopaholic/reportwidgets/sourcesoforders/partials/_widget.htm b/plugins/andreishilov/utmshopaholic/reportwidgets/sourcesoforders/partials/_widget.htm new file mode 100644 index 00000000..a43a0ad9 --- /dev/null +++ b/plugins/andreishilov/utmshopaholic/reportwidgets/sourcesoforders/partials/_widget.htm @@ -0,0 +1,16 @@ +
+

+
+ +
+ +

+ + : +

+
\ No newline at end of file diff --git a/plugins/andreishilov/utmshopaholic/updates/create_utm_table.php b/plugins/andreishilov/utmshopaholic/updates/create_utm_table.php new file mode 100644 index 00000000..06296909 --- /dev/null +++ b/plugins/andreishilov/utmshopaholic/updates/create_utm_table.php @@ -0,0 +1,32 @@ +engine = 'InnoDB'; + $table->increments('id'); + $table->integer('order_id')->unsigned()->nullable()->unique(); + $table->string('utm_source', 128)->nullable(); + $table->string('utm_campaign', 128)->nullable(); + $table->string('utm_medium', 128)->nullable(); + $table->string('utm_term', 512)->nullable(); + $table->string('utm_content', 128)->nullable(); + + $table->index('utm_source'); + $table->index('utm_campaign'); + }); + } + + public function down() + { + Schema::dropIfExists('andreishilov_utmshopaholic_utm'); + } +} diff --git a/plugins/andreishilov/utmshopaholic/updates/version.yaml b/plugins/andreishilov/utmshopaholic/updates/version.yaml new file mode 100644 index 00000000..adf84c02 --- /dev/null +++ b/plugins/andreishilov/utmshopaholic/updates/version.yaml @@ -0,0 +1,5 @@ +1.0.1: + - 'Initialize plugin.' + - 'create_utm_table.php' +1.0.2: + - Shopaholic plugin added to dependencies \ No newline at end of file diff --git a/plugins/codecycler/invoiceshopaholic/Plugin.php b/plugins/codecycler/invoiceshopaholic/Plugin.php new file mode 100644 index 00000000..b3089205 --- /dev/null +++ b/plugins/codecycler/invoiceshopaholic/Plugin.php @@ -0,0 +1,61 @@ + 'Invoice for Shopaholic', + 'description' => 'Adds a send invoice action to the notify plugin', + 'author' => 'Codecycler', + 'icon' => 'icon-bell-o', + ]; + } + + /** + * Register method, called when the plugin is first registered. + * + * @return void + */ + public function register() + { + + } + + /** + * Boot method, called right before the request route. + * + * @return array + */ + public function boot() + { + } + + public function registerNotificationRules() + { + return [ + 'actions' => [ + GenerateInvoicePdf::class, + ], + ]; + } +} diff --git a/plugins/codecycler/invoiceshopaholic/composer.json b/plugins/codecycler/invoiceshopaholic/composer.json new file mode 100644 index 00000000..e4041750 --- /dev/null +++ b/plugins/codecycler/invoiceshopaholic/composer.json @@ -0,0 +1,8 @@ +{ + "name": "codecycler/invoiceshopaholic-plugin", + "type": "october-plugin", + "description": "None", + "require": { + "composer/installers": "~1.0" + } +} \ No newline at end of file diff --git a/plugins/codecycler/invoiceshopaholic/notifyrules/GenerateInvoicePdf.php b/plugins/codecycler/invoiceshopaholic/notifyrules/GenerateInvoicePdf.php new file mode 100644 index 00000000..e21c26fc --- /dev/null +++ b/plugins/codecycler/invoiceshopaholic/notifyrules/GenerateInvoicePdf.php @@ -0,0 +1,75 @@ + 'Generate an invoice and send it to the customer', + 'description' => 'Generate an invoice for the given order and send to customer', + 'icon' => 'icon-envelope', + ]; + } + + public function triggerAction($params) + { + $sOrderNumber = $params['order_number']; + + // Get the invoice by order number + $obOrder = Order::getByNumber($sOrderNumber)->first(); + + // Generate PDF by template + $filename = storage_path('app/uploads/invoice-' . $sOrderNumber . '.pdf'); + + // + $data = array_merge($params, [ + 'obOrder' => $obOrder, + 'arOrder' => OrderItem::make($obOrder->id), + ]); + + // + PDF::loadTemplate($this->host->pdf_template, $data) + ->save($filename); + + // + Mail::send($this->host->mail_template, $data, function ($message) use ($data, $filename) { + $twig = new Twig\Environment(new Twig\Loader\ArrayLoader([ + 'subject' => $this->host->email_subject, + 'to' => $this->host->email_address, + ])); + + $message->subject($twig->render('subject', $data)); + $message->to($twig->render('to', $data)); + + $message->attach($filename); + }); + } + + public function getMailTemplateOptions() + { + $codes = array_keys(MailTemplate::listAllTemplates()); + $result = array_combine($codes, $codes); + return $result; + } + + public function getPdfTemplateOptions() + { + $codes = Template::all()->pluck('code')->toArray(); + $result = array_combine($codes, $codes); + return $result; + } + + public function defineFormFields() + { + return 'fields.yaml'; + } +} diff --git a/plugins/codecycler/invoiceshopaholic/notifyrules/generateinvoicepdf/fields.yaml b/plugins/codecycler/invoiceshopaholic/notifyrules/generateinvoicepdf/fields.yaml new file mode 100644 index 00000000..7ca318e0 --- /dev/null +++ b/plugins/codecycler/invoiceshopaholic/notifyrules/generateinvoicepdf/fields.yaml @@ -0,0 +1,23 @@ +fields: + mail_template: + label: Mail template + type: dropdown + placeholder: Select template + span: left + pdf_template: + label: PDF template + type: dropdown + placeholder: Select template + span: right + order_number: + label: Order number + type: text + span: left + email_subject: + label: Email subject + type: text + span: left + email_address: + label: Email address + type: text + span: left diff --git a/plugins/codecycler/invoiceshopaholic/updates/version.yaml b/plugins/codecycler/invoiceshopaholic/updates/version.yaml new file mode 100644 index 00000000..87ff282b --- /dev/null +++ b/plugins/codecycler/invoiceshopaholic/updates/version.yaml @@ -0,0 +1,2 @@ +1.0.1: First version of InvoiceShopaholic +1.0.2: Added requirements to plugins diff --git a/plugins/codecycler/notifyshopaholic/LICENSE b/plugins/codecycler/notifyshopaholic/LICENSE new file mode 100644 index 00000000..f288702d --- /dev/null +++ b/plugins/codecycler/notifyshopaholic/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/plugins/codecycler/notifyshopaholic/Plugin.php b/plugins/codecycler/notifyshopaholic/Plugin.php new file mode 100644 index 00000000..ae548359 --- /dev/null +++ b/plugins/codecycler/notifyshopaholic/Plugin.php @@ -0,0 +1,79 @@ + 'codecycler.notifyshopaholic::lang.plugin.name', + 'description' => 'codecycler.notifyshopaholic::lang.plugin.description', + 'author' => 'Codecycler', + 'icon' => 'icon-bell-o' + ]; + } + + public function boot() + { + Event::subscribe(ExtendOrderModel::class); + } + + public function register() + { + $this->bindNotificationEvents(); + } + + public function registerNotificationRules() + { + return [ + 'groups' => [ + 'shopaholic' => [ + 'label' => 'Shopaholic', + 'icon' => 'icon-shopping-cart' + ], + ], + 'events' => [ + OrderCreated::class, + OrderUpdated::class, + ], + 'actions' => [], + 'conditions' => [ + OrderStatus::class, + ], + ]; + } + + public function bindNotificationEvents() + { + if (! class_exists(Notifier::class)) { + return; + } + + Notifier::bindEvents([ + OrderProcessor::EVENT_ORDER_CREATED => OrderCreated::class, + ExtendOrderModel::EVENT_ORDER_UPDATED => OrderUpdated::class, + ]); + } +} diff --git a/plugins/codecycler/notifyshopaholic/README.md b/plugins/codecycler/notifyshopaholic/README.md new file mode 100644 index 00000000..f65f67b8 --- /dev/null +++ b/plugins/codecycler/notifyshopaholic/README.md @@ -0,0 +1,23 @@ +# oc-notifyshopaholic-plugin +This plugin can be used for automating Shopaholic using the `RainLab.Notify` plugin. + +## Installation +You can easily install this plugin by requiring it as a dependency in your composer project. Just run `composer require codecycler/oc-notifyshopaholic-plugin` or manually add it to your `composer.json` file to install this plugin. + +## Events +### OrderCreated +Binded to `OrderProcessor::EVENT_ORDER_CREATED` + +### OrderUpdated +Binded to `Codecycler\NotifyShopaholic\Classes\Events\ExtendOrderModel::EVENT_ORDER_UPDATED` which listens to the `afterUpdate` event for the `Order` model. + +## Roadmap +More events, conditions and actions will be developed when needed by our projects. + +## Contribution +Please feel free to contribute by creating a PR and submitting it to the **develop** branch. + +## License +© 2021, [Codecycler](https://codecycler.com) under [GNU GPL v3](https://opensource.org/licenses/GPL-3.0).
+ +This plugin is sponsored by [Codecycler](https://codecycler.com) \ No newline at end of file diff --git a/plugins/codecycler/notifyshopaholic/classes/events/ExtendOrderModel.php b/plugins/codecycler/notifyshopaholic/classes/events/ExtendOrderModel.php new file mode 100644 index 00000000..8085d175 --- /dev/null +++ b/plugins/codecycler/notifyshopaholic/classes/events/ExtendOrderModel.php @@ -0,0 +1,18 @@ +bindEvent('model.afterUpdate', function () use ($obOrder) { + Event::fire(self::EVENT_ORDER_UPDATED, $obOrder); + }); + }); + } +} \ No newline at end of file diff --git a/plugins/codecycler/notifyshopaholic/composer.json b/plugins/codecycler/notifyshopaholic/composer.json new file mode 100644 index 00000000..defe25bb --- /dev/null +++ b/plugins/codecycler/notifyshopaholic/composer.json @@ -0,0 +1,8 @@ +{ + "name": "codecycler/notifyshopaholic-plugin", + "type": "october-plugin", + "description": "None", + "require": { + "composer/installers": "~1.0" + } +} \ No newline at end of file diff --git a/plugins/codecycler/notifyshopaholic/lang/en/lang.php b/plugins/codecycler/notifyshopaholic/lang/en/lang.php new file mode 100644 index 00000000..29e63b51 --- /dev/null +++ b/plugins/codecycler/notifyshopaholic/lang/en/lang.php @@ -0,0 +1,34 @@ + [ + 'name' => 'Shopaholic for Notify', + 'description' => 'Shopaholic integration for the RainLab.Notify plugin for October CMS', + ], + + 'events' => [ + 'order_created' => [ + 'name' => 'Order created', + 'description' => 'When an order is created', + 'group' => 'shopaholic', + ], + 'order_updated' => [ + 'name' => 'Order updated', + 'description' => 'When an order is updated', + 'group' => 'shopaholic', + ], + ], + + 'rules' => [ + 'title' => [ + 'order_number' => 'Order number', + 'name' => 'Name', + 'last_name' => 'Last name', + 'email' => 'Emailaddress', + ], + 'label' => [ + 'order_number' => 'The order number', + 'name' => 'Name of the user', + 'last_name' => 'Last name of the user', + 'email' => 'Emailaddress of the user', + ], + ], +]; diff --git a/plugins/codecycler/notifyshopaholic/notifyrules/conditions/OrderStatus.php b/plugins/codecycler/notifyshopaholic/notifyrules/conditions/OrderStatus.php new file mode 100644 index 00000000..e7e32790 --- /dev/null +++ b/plugins/codecycler/notifyshopaholic/notifyrules/conditions/OrderStatus.php @@ -0,0 +1,42 @@ +status_id == $this->host->status_id; + } + + public function getStatusIdOptions() + { + return Status::all()->pluck('name', 'id')->toArray(); + } + + public function defineFormFields() + { + return 'fields.yaml'; + } +} \ No newline at end of file diff --git a/plugins/codecycler/notifyshopaholic/notifyrules/conditions/orderstatus/fields.yaml b/plugins/codecycler/notifyshopaholic/notifyrules/conditions/orderstatus/fields.yaml new file mode 100644 index 00000000..8c9f1589 --- /dev/null +++ b/plugins/codecycler/notifyshopaholic/notifyrules/conditions/orderstatus/fields.yaml @@ -0,0 +1,5 @@ +fields: + status_id: + label: Status + type: dropdown + span: full \ No newline at end of file diff --git a/plugins/codecycler/notifyshopaholic/notifyrules/events/OrderCreated.php b/plugins/codecycler/notifyshopaholic/notifyrules/events/OrderCreated.php new file mode 100644 index 00000000..bd438244 --- /dev/null +++ b/plugins/codecycler/notifyshopaholic/notifyrules/events/OrderCreated.php @@ -0,0 +1,55 @@ + e(trans('codecycler.notifyshopaholic::lang.events.order_created.name')), + 'description' => e(trans('codecycler.notifyshopaholic::lang.events.order_created.description')), + 'group' => e(trans('codecycler.notifyshopaholic::lang.events.order_created.group')), + ]; + } + + public function defineParams() + { + return [ + 'order_number' => [ + 'title' => e(trans('codecycler.notifyshopaholic::lang.rules.title.order_number')), + 'label' => e(trans('codecycler.notifyshopaholic::lang.rules.label.order_number')), + ], + 'name' => [ + 'title' => e(trans('codecycler.notifyshopaholic::lang.rules.title.name')), + 'label' => e(trans('codecycler.notifyshopaholic::lang.rules.label.name')), + ], + 'last_name' => [ + 'title' => e(trans('codecycler.notifyshopaholic::lang.rules.title.last_name')), + 'label' => e(trans('codecycler.notifyshopaholic::lang.rules.label.last_name')), + ], + 'email' => [ + 'title' => e(trans('codecycler.notifyshopaholic::lang.rules.title.email')), + 'label' => e(trans('codecycler.notifyshopaholic::lang.rules.label.email')), + ], + ]; + } + + public static function makeParamsFromEvent(array $args, $eventName = null) + { + $obOrder = array_get($args, 0); + + $data = [ + 'order_number' => $obOrder->order_number, + 'name' => $obOrder->getProperty('name'), + 'last_name' => $obOrder->getProperty('last_name'), + 'email' => $obOrder->getProperty('email'), + 'user' => $obOrder->getProperty('email'), + 'order' => $obOrder, + ]; + + return $data; + } +} diff --git a/plugins/codecycler/notifyshopaholic/notifyrules/events/OrderUpdated.php b/plugins/codecycler/notifyshopaholic/notifyrules/events/OrderUpdated.php new file mode 100644 index 00000000..5c302d16 --- /dev/null +++ b/plugins/codecycler/notifyshopaholic/notifyrules/events/OrderUpdated.php @@ -0,0 +1,55 @@ + e(trans('codecycler.notifyshopaholic::lang.events.order_updated.name')), + 'description' => e(trans('codecycler.notifyshopaholic::lang.events.order_updated.description')), + 'group' => e(trans('codecycler.notifyshopaholic::lang.events.order_updated.group')), + ]; + } + + public function defineParams() + { + return [ + 'order_number' => [ + 'title' => e(trans('codecycler.notifyshopaholic::lang.rules.title.order_number')), + 'label' => e(trans('codecycler.notifyshopaholic::lang.rules.label.order_number')), + ], + 'name' => [ + 'title' => e(trans('codecycler.notifyshopaholic::lang.rules.title.name')), + 'label' => e(trans('codecycler.notifyshopaholic::lang.rules.label.name')), + ], + 'last_name' => [ + 'title' => e(trans('codecycler.notifyshopaholic::lang.rules.title.last_name')), + 'label' => e(trans('codecycler.notifyshopaholic::lang.rules.label.last_name')), + ], + 'email' => [ + 'title' => e(trans('codecycler.notifyshopaholic::lang.rules.title.email')), + 'label' => e(trans('codecycler.notifyshopaholic::lang.rules.label.email')), + ], + ]; + } + + public static function makeParamsFromEvent(array $args, $eventName = null) + { + $obOrder = array_get($args, 0); + + $data = [ + 'order_number' => $obOrder->order_number, + 'name' => $obOrder->getProperty('name'), + 'last_name' => $obOrder->getProperty('last_name'), + 'email' => $obOrder->getProperty('email'), + 'user' => $obOrder->getProperty('email'), + 'order' => $obOrder, + ]; + + return $data; + } +} diff --git a/plugins/codecycler/notifyshopaholic/updates/version.yaml b/plugins/codecycler/notifyshopaholic/updates/version.yaml new file mode 100644 index 00000000..00134ff9 --- /dev/null +++ b/plugins/codecycler/notifyshopaholic/updates/version.yaml @@ -0,0 +1,7 @@ +1.0.1: + - First version of NotifyShopaholic + - Added Order Created event +1.0.2: + - Added installation instructions to the documentation +1.0.3: + - Ready for marketplace! diff --git a/plugins/codecycler/notifyshopaholic/vendor/autoload.php b/plugins/codecycler/notifyshopaholic/vendor/autoload.php new file mode 100644 index 00000000..fec3e6e9 --- /dev/null +++ b/plugins/codecycler/notifyshopaholic/vendor/autoload.php @@ -0,0 +1,7 @@ + + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/plugins/codecycler/notifyshopaholic/vendor/codecycler/oc-extend-plugin/Plugin.php b/plugins/codecycler/notifyshopaholic/vendor/codecycler/oc-extend-plugin/Plugin.php new file mode 100644 index 00000000..fd838c4d --- /dev/null +++ b/plugins/codecycler/notifyshopaholic/vendor/codecycler/oc-extend-plugin/Plugin.php @@ -0,0 +1,48 @@ + 'Extend', + 'description' => 'Extending October CMS plugins tool', + 'author' => 'Codecycler', + 'icon' => 'icon-external-link-square' + ]; + } + + /** + * Register method, called when the plugin is first registered. + * + * @return void + */ + public function register() + { + ExtensionManager::instance() + ->load(); + } + + /** + * Boot method, called right before the request route. + * + * @return array + */ + public function boot() + { + ExtensionManager::instance() + ->bind(); + } +} diff --git a/plugins/codecycler/notifyshopaholic/vendor/codecycler/oc-extend-plugin/README.md b/plugins/codecycler/notifyshopaholic/vendor/codecycler/oc-extend-plugin/README.md new file mode 100644 index 00000000..a98b1d9b --- /dev/null +++ b/plugins/codecycler/notifyshopaholic/vendor/codecycler/oc-extend-plugin/README.md @@ -0,0 +1,5 @@ +# Codecycler.Extend / oc-extend-plugin +Use this plugin to easily extend any October CMS plugin. It helps with structure and keeps the code base of your extending plugin clean. + +## Documentation & Examples +Please take a look at [the documentation](https://docs.codecycler.com/oc-extend-plugin-392bff68cdf74b01816508db6fe55bf3). \ No newline at end of file diff --git a/plugins/codecycler/notifyshopaholic/vendor/codecycler/oc-extend-plugin/classes/ExtensionManager.php b/plugins/codecycler/notifyshopaholic/vendor/codecycler/oc-extend-plugin/classes/ExtensionManager.php new file mode 100644 index 00000000..8f22fbd9 --- /dev/null +++ b/plugins/codecycler/notifyshopaholic/vendor/codecycler/oc-extend-plugin/classes/ExtensionManager.php @@ -0,0 +1,53 @@ +getAllPlugins(); + + // + foreach ($plugins as $plugin) { + if (method_exists($plugin, 'registerExtenders')) { + $this->register($plugin->registerExtenders()); + } + } + } + + public function register($extenders) + { + if (!is_array($extenders)) { + $extenders = [$extenders]; + } + + foreach ($extenders as $extender) { + if (in_array($extender, $this->extenders)) { + continue; + } + + $this->extenders[] = $extender; + } + } + + public function bind() + { + foreach ($this->get() as $extender) { + Event::subscribe($extender); + } + } + + public function get() + { + return $this->extenders; + } +} \ No newline at end of file diff --git a/plugins/codecycler/notifyshopaholic/vendor/codecycler/oc-extend-plugin/classes/PluginExtender.php b/plugins/codecycler/notifyshopaholic/vendor/codecycler/oc-extend-plugin/classes/PluginExtender.php new file mode 100644 index 00000000..f5b9d826 --- /dev/null +++ b/plugins/codecycler/notifyshopaholic/vendor/codecycler/oc-extend-plugin/classes/PluginExtender.php @@ -0,0 +1,187 @@ +load(); + } + + /** + * This method reloads the extension config + */ + public function load() + { + $this->model = $this->model(); + $this->controller = $this->controller(); + } + + public function model() + { + return null; + } + + public function controller() + { + return null; + } + + public function extendSubscribe() + { + } + + public function subscribe() + { + $this->extendFormFields(); + $this->extendRelationConfig(); + $this->extendRelations(); + $this->extendSubscribe(); + $this->extendMethods(); + } + + public function addFields() + { + return []; + } + + public function addTabFields() + { + return []; + } + + public function addRelationConfig() + { + return []; + } + + public function belongsToMany() + { + return []; + } + + public function hasMany() + { + return []; + } + + public function belongsTo() + { + return []; + } + + public function methods() + { + return []; + } + + /** + * Private functions required by the extender + */ + + private function extendRelationConfig() + { + if (count($this->addRelationConfig()) < 1) { + return; + } + + $controller = new $this->controller; + + $addRelationController = false; + $configExists = false; + $configEmpty = true; + $relationConfig = $this->addRelationConfig(); + + if (!in_array('Backend\Behaviors\RelationController', $controller->implement) && !in_array('Backend.Behaviors.RelationController', $controller->implement)) { + $addRelationController = true; + } + + if (property_exists($controller, 'relationConfig')) { + $configExists = true; + } + + if ($configExists && $controller->relationConfig) { + $configEmpty = false; + } + + $controller::extend(function ($c) use ($addRelationController, $configExists, $configEmpty, $relationConfig, $controller) { + if ($addRelationController) { + array_push($c->implement, 'Backend\Behaviors\RelationController'); + } + + if (!$configExists) { + $c->addDynamicProperty('relationConfig', function ($relationConfig) { + return $relationConfig; + }); + } else { + if ($configEmpty) { + $c->relationConfig = $relationConfig; + } else { + if (is_array($c->relationConfig)) { + $c->relationConfig = array_merge($relationConfig, $c->relationConfig); + } + + if (is_string($c->relationConfig)) { + $controllerPath = plugins_path(strtolower(str_replace('\\', '/', get_class($controller)))) . '/' . $c->relationConfig; + + $currentRelationConfig = Yaml::parseFile($controllerPath); + + $c->relationConfig = array_merge($currentRelationConfig, $relationConfig); + } + } + } + }); + } + + private function extendFormFields() + { + Event::listen('backend.form.extendFields', function ($formWidget, $formData) { + if (!$formWidget->getController() instanceof $this->controller) { + return; + } + + if (!$formWidget->model instanceof $this->model) { + return; + } + + if ($formWidget->isNested) { + return; + } + + $formWidget->addFields($this->addFields()); + $formWidget->addTabFields($this->addTabFields()); + }); + } + + private function extendRelations() + { + $model = $this->model; + $belongsToMany = $this->belongsToMany(); + $hasMany = $this->hasMany(); + $belongsTo = $this->belongsTo(); + + $model::extend(function ($model) use ($belongsToMany, $hasMany, $belongsTo) { + $model->belongsToMany = array_merge($model->belongsToMany, $belongsToMany); + $model->hasMany = array_merge($model->hasMany, $hasMany); + $model->belongsTo = array_merge($model->belongsTo, $belongsTo); + }); + } + + private function extendMethods() + { + $model = $this->model; + $methods = $this->methods(); + + $model::extend(function ($model) use ($methods) { + foreach ($methods as $functionName => $method) { + $model->addDynamicMethod($functionName, $method); + } + }); + } +} \ No newline at end of file diff --git a/plugins/codecycler/notifyshopaholic/vendor/codecycler/oc-extend-plugin/composer.json b/plugins/codecycler/notifyshopaholic/vendor/codecycler/oc-extend-plugin/composer.json new file mode 100644 index 00000000..cbdb625c --- /dev/null +++ b/plugins/codecycler/notifyshopaholic/vendor/codecycler/oc-extend-plugin/composer.json @@ -0,0 +1,13 @@ +{ + "name": "codecycler/oc-extend-plugin", + "description": "Extending October CMS plugins tool", + "type": "october-plugin", + "license": "GPL-3.0-only", + "authors": [ + { + "name": "Sebastiaan Kloos", + "email": "sebastiaan@codecycler.com" + } + ], + "require": {} +} diff --git a/plugins/codecycler/notifyshopaholic/vendor/codecycler/oc-extend-plugin/updates/version.yaml b/plugins/codecycler/notifyshopaholic/vendor/codecycler/oc-extend-plugin/updates/version.yaml new file mode 100644 index 00000000..f386d346 --- /dev/null +++ b/plugins/codecycler/notifyshopaholic/vendor/codecycler/oc-extend-plugin/updates/version.yaml @@ -0,0 +1 @@ +1.0.1: First version of Extend diff --git a/plugins/codecycler/notifyshopaholic/vendor/composer/ClassLoader.php b/plugins/codecycler/notifyshopaholic/vendor/composer/ClassLoader.php new file mode 100644 index 00000000..fce8549f --- /dev/null +++ b/plugins/codecycler/notifyshopaholic/vendor/composer/ClassLoader.php @@ -0,0 +1,445 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Autoload; + +/** + * ClassLoader implements a PSR-0, PSR-4 and classmap class loader. + * + * $loader = new \Composer\Autoload\ClassLoader(); + * + * // register classes with namespaces + * $loader->add('Symfony\Component', __DIR__.'/component'); + * $loader->add('Symfony', __DIR__.'/framework'); + * + * // activate the autoloader + * $loader->register(); + * + * // to enable searching the include path (eg. for PEAR packages) + * $loader->setUseIncludePath(true); + * + * In this example, if you try to use a class in the Symfony\Component + * namespace or one of its children (Symfony\Component\Console for instance), + * the autoloader will first look for the class under the component/ + * directory, and it will then fallback to the framework/ directory if not + * found before giving up. + * + * This class is loosely based on the Symfony UniversalClassLoader. + * + * @author Fabien Potencier + * @author Jordi Boggiano + * @see http://www.php-fig.org/psr/psr-0/ + * @see http://www.php-fig.org/psr/psr-4/ + */ +class ClassLoader +{ + // PSR-4 + private $prefixLengthsPsr4 = array(); + private $prefixDirsPsr4 = array(); + private $fallbackDirsPsr4 = array(); + + // PSR-0 + private $prefixesPsr0 = array(); + private $fallbackDirsPsr0 = array(); + + private $useIncludePath = false; + private $classMap = array(); + private $classMapAuthoritative = false; + private $missingClasses = array(); + private $apcuPrefix; + + public function getPrefixes() + { + if (!empty($this->prefixesPsr0)) { + return call_user_func_array('array_merge', $this->prefixesPsr0); + } + + return array(); + } + + public function getPrefixesPsr4() + { + return $this->prefixDirsPsr4; + } + + public function getFallbackDirs() + { + return $this->fallbackDirsPsr0; + } + + public function getFallbackDirsPsr4() + { + return $this->fallbackDirsPsr4; + } + + public function getClassMap() + { + return $this->classMap; + } + + /** + * @param array $classMap Class to filename map + */ + public function addClassMap(array $classMap) + { + if ($this->classMap) { + $this->classMap = array_merge($this->classMap, $classMap); + } else { + $this->classMap = $classMap; + } + } + + /** + * Registers a set of PSR-0 directories for a given prefix, either + * appending or prepending to the ones previously set for this prefix. + * + * @param string $prefix The prefix + * @param array|string $paths The PSR-0 root directories + * @param bool $prepend Whether to prepend the directories + */ + public function add($prefix, $paths, $prepend = false) + { + if (!$prefix) { + if ($prepend) { + $this->fallbackDirsPsr0 = array_merge( + (array) $paths, + $this->fallbackDirsPsr0 + ); + } else { + $this->fallbackDirsPsr0 = array_merge( + $this->fallbackDirsPsr0, + (array) $paths + ); + } + + return; + } + + $first = $prefix[0]; + if (!isset($this->prefixesPsr0[$first][$prefix])) { + $this->prefixesPsr0[$first][$prefix] = (array) $paths; + + return; + } + if ($prepend) { + $this->prefixesPsr0[$first][$prefix] = array_merge( + (array) $paths, + $this->prefixesPsr0[$first][$prefix] + ); + } else { + $this->prefixesPsr0[$first][$prefix] = array_merge( + $this->prefixesPsr0[$first][$prefix], + (array) $paths + ); + } + } + + /** + * Registers a set of PSR-4 directories for a given namespace, either + * appending or prepending to the ones previously set for this namespace. + * + * @param string $prefix The prefix/namespace, with trailing '\\' + * @param array|string $paths The PSR-4 base directories + * @param bool $prepend Whether to prepend the directories + * + * @throws \InvalidArgumentException + */ + public function addPsr4($prefix, $paths, $prepend = false) + { + if (!$prefix) { + // Register directories for the root namespace. + if ($prepend) { + $this->fallbackDirsPsr4 = array_merge( + (array) $paths, + $this->fallbackDirsPsr4 + ); + } else { + $this->fallbackDirsPsr4 = array_merge( + $this->fallbackDirsPsr4, + (array) $paths + ); + } + } elseif (!isset($this->prefixDirsPsr4[$prefix])) { + // Register directories for a new namespace. + $length = strlen($prefix); + if ('\\' !== $prefix[$length - 1]) { + throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); + } + $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; + $this->prefixDirsPsr4[$prefix] = (array) $paths; + } elseif ($prepend) { + // Prepend directories for an already registered namespace. + $this->prefixDirsPsr4[$prefix] = array_merge( + (array) $paths, + $this->prefixDirsPsr4[$prefix] + ); + } else { + // Append directories for an already registered namespace. + $this->prefixDirsPsr4[$prefix] = array_merge( + $this->prefixDirsPsr4[$prefix], + (array) $paths + ); + } + } + + /** + * Registers a set of PSR-0 directories for a given prefix, + * replacing any others previously set for this prefix. + * + * @param string $prefix The prefix + * @param array|string $paths The PSR-0 base directories + */ + public function set($prefix, $paths) + { + if (!$prefix) { + $this->fallbackDirsPsr0 = (array) $paths; + } else { + $this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths; + } + } + + /** + * Registers a set of PSR-4 directories for a given namespace, + * replacing any others previously set for this namespace. + * + * @param string $prefix The prefix/namespace, with trailing '\\' + * @param array|string $paths The PSR-4 base directories + * + * @throws \InvalidArgumentException + */ + public function setPsr4($prefix, $paths) + { + if (!$prefix) { + $this->fallbackDirsPsr4 = (array) $paths; + } else { + $length = strlen($prefix); + if ('\\' !== $prefix[$length - 1]) { + throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); + } + $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; + $this->prefixDirsPsr4[$prefix] = (array) $paths; + } + } + + /** + * Turns on searching the include path for class files. + * + * @param bool $useIncludePath + */ + public function setUseIncludePath($useIncludePath) + { + $this->useIncludePath = $useIncludePath; + } + + /** + * Can be used to check if the autoloader uses the include path to check + * for classes. + * + * @return bool + */ + public function getUseIncludePath() + { + return $this->useIncludePath; + } + + /** + * Turns off searching the prefix and fallback directories for classes + * that have not been registered with the class map. + * + * @param bool $classMapAuthoritative + */ + public function setClassMapAuthoritative($classMapAuthoritative) + { + $this->classMapAuthoritative = $classMapAuthoritative; + } + + /** + * Should class lookup fail if not found in the current class map? + * + * @return bool + */ + public function isClassMapAuthoritative() + { + return $this->classMapAuthoritative; + } + + /** + * APCu prefix to use to cache found/not-found classes, if the extension is enabled. + * + * @param string|null $apcuPrefix + */ + public function setApcuPrefix($apcuPrefix) + { + $this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null; + } + + /** + * The APCu prefix in use, or null if APCu caching is not enabled. + * + * @return string|null + */ + public function getApcuPrefix() + { + return $this->apcuPrefix; + } + + /** + * Registers this instance as an autoloader. + * + * @param bool $prepend Whether to prepend the autoloader or not + */ + public function register($prepend = false) + { + spl_autoload_register(array($this, 'loadClass'), true, $prepend); + } + + /** + * Unregisters this instance as an autoloader. + */ + public function unregister() + { + spl_autoload_unregister(array($this, 'loadClass')); + } + + /** + * Loads the given class or interface. + * + * @param string $class The name of the class + * @return bool|null True if loaded, null otherwise + */ + public function loadClass($class) + { + if ($file = $this->findFile($class)) { + includeFile($file); + + return true; + } + } + + /** + * Finds the path to the file where the class is defined. + * + * @param string $class The name of the class + * + * @return string|false The path if found, false otherwise + */ + public function findFile($class) + { + // class map lookup + if (isset($this->classMap[$class])) { + return $this->classMap[$class]; + } + if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) { + return false; + } + if (null !== $this->apcuPrefix) { + $file = apcu_fetch($this->apcuPrefix.$class, $hit); + if ($hit) { + return $file; + } + } + + $file = $this->findFileWithExtension($class, '.php'); + + // Search for Hack files if we are running on HHVM + if (false === $file && defined('HHVM_VERSION')) { + $file = $this->findFileWithExtension($class, '.hh'); + } + + if (null !== $this->apcuPrefix) { + apcu_add($this->apcuPrefix.$class, $file); + } + + if (false === $file) { + // Remember that this class does not exist. + $this->missingClasses[$class] = true; + } + + return $file; + } + + private function findFileWithExtension($class, $ext) + { + // PSR-4 lookup + $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext; + + $first = $class[0]; + if (isset($this->prefixLengthsPsr4[$first])) { + $subPath = $class; + while (false !== $lastPos = strrpos($subPath, '\\')) { + $subPath = substr($subPath, 0, $lastPos); + $search = $subPath . '\\'; + if (isset($this->prefixDirsPsr4[$search])) { + $pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1); + foreach ($this->prefixDirsPsr4[$search] as $dir) { + if (file_exists($file = $dir . $pathEnd)) { + return $file; + } + } + } + } + } + + // PSR-4 fallback dirs + foreach ($this->fallbackDirsPsr4 as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) { + return $file; + } + } + + // PSR-0 lookup + if (false !== $pos = strrpos($class, '\\')) { + // namespaced class name + $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1) + . strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR); + } else { + // PEAR-like class name + $logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext; + } + + if (isset($this->prefixesPsr0[$first])) { + foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) { + if (0 === strpos($class, $prefix)) { + foreach ($dirs as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { + return $file; + } + } + } + } + } + + // PSR-0 fallback dirs + foreach ($this->fallbackDirsPsr0 as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { + return $file; + } + } + + // PSR-0 include paths. + if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) { + return $file; + } + + return false; + } +} + +/** + * Scope isolated include. + * + * Prevents access to $this/self from included files. + */ +function includeFile($file) +{ + include $file; +} diff --git a/plugins/codecycler/notifyshopaholic/vendor/composer/LICENSE b/plugins/codecycler/notifyshopaholic/vendor/composer/LICENSE new file mode 100644 index 00000000..f27399a0 --- /dev/null +++ b/plugins/codecycler/notifyshopaholic/vendor/composer/LICENSE @@ -0,0 +1,21 @@ + +Copyright (c) Nils Adermann, Jordi Boggiano + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + diff --git a/plugins/codecycler/notifyshopaholic/vendor/composer/autoload_classmap.php b/plugins/codecycler/notifyshopaholic/vendor/composer/autoload_classmap.php new file mode 100644 index 00000000..7a91153b --- /dev/null +++ b/plugins/codecycler/notifyshopaholic/vendor/composer/autoload_classmap.php @@ -0,0 +1,9 @@ += 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded()); + if ($useStaticLoader) { + require_once __DIR__ . '/autoload_static.php'; + + call_user_func(\Composer\Autoload\ComposerStaticInit347031eccba95821db30e6a679fe62e3::getInitializer($loader)); + } else { + $map = require __DIR__ . '/autoload_namespaces.php'; + foreach ($map as $namespace => $path) { + $loader->set($namespace, $path); + } + + $map = require __DIR__ . '/autoload_psr4.php'; + foreach ($map as $namespace => $path) { + $loader->setPsr4($namespace, $path); + } + + $classMap = require __DIR__ . '/autoload_classmap.php'; + if ($classMap) { + $loader->addClassMap($classMap); + } + } + + $loader->register(true); + + return $loader; + } +} diff --git a/plugins/codecycler/notifyshopaholic/vendor/composer/autoload_static.php b/plugins/codecycler/notifyshopaholic/vendor/composer/autoload_static.php new file mode 100644 index 00000000..31da1ada --- /dev/null +++ b/plugins/codecycler/notifyshopaholic/vendor/composer/autoload_static.php @@ -0,0 +1,15 @@ +where('parent_id', $sData); + return $obQuery->where('parent_id', $sData)->addSelect('featured'); } /** diff --git a/plugins/lovata/shopaholic/models/category/columns.yaml b/plugins/lovata/shopaholic/models/category/columns.yaml index ff975c4a..a7d4266f 100644 --- a/plugins/lovata/shopaholic/models/category/columns.yaml +++ b/plugins/lovata/shopaholic/models/category/columns.yaml @@ -39,4 +39,8 @@ columns: label: 'lovata.toolbox::lang.field.updated_at' type: timetense sortable: true - invisible: true \ No newline at end of file + invisible: true + featured: + label: Featured + type: text + sortable: true diff --git a/plugins/lovata/shopaholic/models/category/fields.yaml b/plugins/lovata/shopaholic/models/category/fields.yaml index e40bfbe8..209d4f98 100644 --- a/plugins/lovata/shopaholic/models/category/fields.yaml +++ b/plugins/lovata/shopaholic/models/category/fields.yaml @@ -4,6 +4,11 @@ fields: span: left default: 1 type: switch + featured: + label: Featured + span: auto + default: 0 + type: switch name: label: 'lovata.toolbox::lang.field.name' span: left @@ -51,7 +56,7 @@ tabs: preview_image: label: 'lovata.toolbox::lang.field.preview_image' mode: image - fileTypes: jpg,jpeg,bmp,png,webp,gif,svg + fileTypes: 'jpg,jpeg,bmp,png,webp,gif,svg' useCaption: true thumbOptions: mode: crop @@ -62,7 +67,7 @@ tabs: icon: label: 'lovata.toolbox::lang.field.icon' mode: image - fileTypes: jpg,jpeg,bmp,png,webp,gif,svg + fileTypes: 'jpg,jpeg,bmp,png,webp,gif,svg' useCaption: true thumbOptions: mode: crop @@ -73,7 +78,7 @@ tabs: images: label: 'lovata.toolbox::lang.field.images' mode: image - fileTypes: jpg,jpeg,bmp,png,webp,gif,svg + fileTypes: 'jpg,jpeg,bmp,png,webp,gif,svg' useCaption: true thumbOptions: mode: crop diff --git a/plugins/lovata/shopaholic/tests/unit/collection/CategoryCollectionTest.php b/plugins/lovata/shopaholic/tests/unit/collection/CategoryCollectionTest.php index 1906e480..c7bb79c1 100644 --- a/plugins/lovata/shopaholic/tests/unit/collection/CategoryCollectionTest.php +++ b/plugins/lovata/shopaholic/tests/unit/collection/CategoryCollectionTest.php @@ -27,6 +27,7 @@ class CategoryCollectionTest extends CommonTest 'code' => 'code', 'preview_text' => 'preview_text', 'description' => 'description', + 'featured' => 'featured', ]; /** diff --git a/plugins/lovata/shopaholic/tests/unit/item/CategoryItemTest.php b/plugins/lovata/shopaholic/tests/unit/item/CategoryItemTest.php index 46064d0a..5692ad31 100644 --- a/plugins/lovata/shopaholic/tests/unit/item/CategoryItemTest.php +++ b/plugins/lovata/shopaholic/tests/unit/item/CategoryItemTest.php @@ -32,6 +32,7 @@ class CategoryItemTest extends CommonTest 'code' => 'code', 'preview_text' => 'preview_text', 'description' => 'description', + 'featured' => 'featured', ]; protected $arProductData = [ diff --git a/plugins/lovata/shopaholic/updates/builder_table_update_lovata_shopaholic_categories.php b/plugins/lovata/shopaholic/updates/builder_table_update_lovata_shopaholic_categories.php new file mode 100644 index 00000000..b945ff5f --- /dev/null +++ b/plugins/lovata/shopaholic/updates/builder_table_update_lovata_shopaholic_categories.php @@ -0,0 +1,23 @@ +boolean('featured')->default(0); + }); + } + + public function down() + { + Schema::table('lovata_shopaholic_categories', function($table) + { + $table->dropColumn('featured'); + }); + } +} diff --git a/plugins/lovata/shopaholic/updates/create_table_categories.php b/plugins/lovata/shopaholic/updates/create_table_categories.php index d316eb7e..aa1220e7 100644 --- a/plugins/lovata/shopaholic/updates/create_table_categories.php +++ b/plugins/lovata/shopaholic/updates/create_table_categories.php @@ -28,6 +28,7 @@ class CreateTableCategories extends Migration $obTable->string('slug')->unique(); $obTable->string('code')->nullable(); $obTable->string('external_id')->nullable(); + $obTable->boolean('featured')->default(0); $obTable->text('preview_text')->nullable(); $obTable->text('description')->nullable(); $obTable->integer('parent_id')->nullable()->unsigned(); diff --git a/plugins/lovata/shopaholic/updates/version.yaml b/plugins/lovata/shopaholic/updates/version.yaml index 951a85f3..24c9e6c3 100644 --- a/plugins/lovata/shopaholic/updates/version.yaml +++ b/plugins/lovata/shopaholic/updates/version.yaml @@ -136,3 +136,6 @@ - 'Added "lazy" param to tabs config of Products controller' 1.30.2: - 'Fixed bug with category tree in v2' +1.30.3: + - 'Updated table lovata_shopaholic_categories' + - builder_table_update_lovata_shopaholic_categories.php diff --git a/plugins/rainlab/blog/.gitignore b/plugins/rainlab/blog/.gitignore new file mode 100644 index 00000000..496ee2ca --- /dev/null +++ b/plugins/rainlab/blog/.gitignore @@ -0,0 +1 @@ +.DS_Store \ No newline at end of file diff --git a/plugins/rainlab/blog/LICENCE.md b/plugins/rainlab/blog/LICENCE.md new file mode 100644 index 00000000..d68943ee --- /dev/null +++ b/plugins/rainlab/blog/LICENCE.md @@ -0,0 +1,19 @@ +# MIT license + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/plugins/rainlab/blog/Plugin.php b/plugins/rainlab/blog/Plugin.php new file mode 100644 index 00000000..2780ee04 --- /dev/null +++ b/plugins/rainlab/blog/Plugin.php @@ -0,0 +1,172 @@ + 'rainlab.blog::lang.plugin.name', + 'description' => 'rainlab.blog::lang.plugin.description', + 'author' => 'Alexey Bobkov, Samuel Georges', + 'icon' => 'icon-pencil', + 'homepage' => 'https://github.com/rainlab/blog-plugin' + ]; + } + + public function registerComponents() + { + return [ + 'RainLab\Blog\Components\Post' => 'blogPost', + 'RainLab\Blog\Components\Posts' => 'blogPosts', + 'RainLab\Blog\Components\Categories' => 'blogCategories', + 'RainLab\Blog\Components\RssFeed' => 'blogRssFeed' + ]; + } + + public function registerPermissions() + { + return [ + 'rainlab.blog.manage_settings' => [ + 'tab' => 'rainlab.blog::lang.blog.tab', + 'label' => 'rainlab.blog::lang.blog.manage_settings' + ], + 'rainlab.blog.access_posts' => [ + 'tab' => 'rainlab.blog::lang.blog.tab', + 'label' => 'rainlab.blog::lang.blog.access_posts' + ], + 'rainlab.blog.access_categories' => [ + 'tab' => 'rainlab.blog::lang.blog.tab', + 'label' => 'rainlab.blog::lang.blog.access_categories' + ], + 'rainlab.blog.access_other_posts' => [ + 'tab' => 'rainlab.blog::lang.blog.tab', + 'label' => 'rainlab.blog::lang.blog.access_other_posts' + ], + 'rainlab.blog.access_import_export' => [ + 'tab' => 'rainlab.blog::lang.blog.tab', + 'label' => 'rainlab.blog::lang.blog.access_import_export' + ], + 'rainlab.blog.access_publish' => [ + 'tab' => 'rainlab.blog::lang.blog.tab', + 'label' => 'rainlab.blog::lang.blog.access_publish' + ] + ]; + } + + public function registerNavigation() + { + return [ + 'blog' => [ + 'label' => 'rainlab.blog::lang.blog.menu_label', + 'url' => Backend::url('rainlab/blog/posts'), + 'icon' => 'icon-pencil', + 'iconSvg' => 'plugins/rainlab/blog/assets/images/blog-icon.svg', + 'permissions' => ['rainlab.blog.*'], + 'order' => 300, + + 'sideMenu' => [ + 'new_post' => [ + 'label' => 'rainlab.blog::lang.posts.new_post', + 'icon' => 'icon-plus', + 'url' => Backend::url('rainlab/blog/posts/create'), + 'permissions' => ['rainlab.blog.access_posts'] + ], + 'posts' => [ + 'label' => 'rainlab.blog::lang.blog.posts', + 'icon' => 'icon-copy', + 'url' => Backend::url('rainlab/blog/posts'), + 'permissions' => ['rainlab.blog.access_posts'] + ], + 'categories' => [ + 'label' => 'rainlab.blog::lang.blog.categories', + 'icon' => 'icon-list-ul', + 'url' => Backend::url('rainlab/blog/categories'), + 'permissions' => ['rainlab.blog.access_categories'] + ] + ] + ] + ]; + } + + public function registerSettings() + { + return [ + 'blog' => [ + 'label' => 'rainlab.blog::lang.blog.menu_label', + 'description' => 'rainlab.blog::lang.blog.settings_description', + 'category' => 'rainlab.blog::lang.blog.menu_label', + 'icon' => 'icon-pencil', + 'class' => 'RainLab\Blog\Models\Settings', + 'order' => 500, + 'keywords' => 'blog post category', + 'permissions' => ['rainlab.blog.manage_settings'] + ] + ]; + } + + /** + * Register method, called when the plugin is first registered. + */ + public function register() + { + /* + * Register the image tag processing callback + */ + TagProcessor::instance()->registerCallback(function($input, $preview) { + if (!$preview) { + return $input; + } + + return preg_replace('|\([0-9]+)]*)\/>|m', + ' + + Click or drop an image... + + + ', + $input); + }); + } + + public function boot() + { + /* + * Register menu items for the RainLab.Pages plugin + */ + Event::listen('pages.menuitem.listTypes', function() { + return [ + 'blog-category' => 'rainlab.blog::lang.menuitem.blog_category', + 'all-blog-categories' => 'rainlab.blog::lang.menuitem.all_blog_categories', + 'blog-post' => 'rainlab.blog::lang.menuitem.blog_post', + 'all-blog-posts' => 'rainlab.blog::lang.menuitem.all_blog_posts', + 'category-blog-posts' => 'rainlab.blog::lang.menuitem.category_blog_posts', + ]; + }); + + Event::listen('pages.menuitem.getTypeInfo', function($type) { + if ($type == 'blog-category' || $type == 'all-blog-categories') { + return Category::getMenuTypeInfo($type); + } + elseif ($type == 'blog-post' || $type == 'all-blog-posts' || $type == 'category-blog-posts') { + return Post::getMenuTypeInfo($type); + } + }); + + Event::listen('pages.menuitem.resolveItem', function($type, $item, $url, $theme) { + if ($type == 'blog-category' || $type == 'all-blog-categories') { + return Category::resolveMenuItem($item, $url, $theme); + } + elseif ($type == 'blog-post' || $type == 'all-blog-posts' || $type == 'category-blog-posts') { + return Post::resolveMenuItem($item, $url, $theme); + } + }); + } +} diff --git a/plugins/rainlab/blog/README.md b/plugins/rainlab/blog/README.md new file mode 100644 index 00000000..f96addd1 --- /dev/null +++ b/plugins/rainlab/blog/README.md @@ -0,0 +1,318 @@ +# Blog Plugin + +A simple, extensible blogging platform for October CMS. + +[Blog & Forum Building Tutorial Video](https://player.vimeo.com/video/97088926) + +## Editing posts + +The plugin uses the markdown markup for the posts. You can use any Markdown syntax and some special tags for embedding images and videos (requires RainLab Blog Video plugin). To embed an image use the image placeholder: + + ![1](image) + +The number in the first part is the placeholder index. If you use multiple images in a post you should use an unique index for each image: + + ![1](image) + + ![2](image) + +You can also add classes or ids to images by using the [markdown extra](http://michelf.ca/projects/php-markdown/extra/) syntax: + + ![1](image){#id .class} + +## Excerpt Vs. Read more + +Posts are managed by selecting *Blog > Posts* from the menu. Each post can contain an excerpt by entering some text in this field on the *Manage* tab. This content is displayed on the page using the `summary` attribute of the blog post. + + {{ post.summary|raw }} + +Alternatively this field can be left blank and the excerpt can be captured from the main content (*Edit* tab). Use the special tag `` to specify a summary from the main content, all content above this tag will be treated as the summary. For example: + + This is a great introduction to a great blog post. This text is included as part of the excerpt / summary. + + + + Let's dive in to more detail about why this post is so great. This text will not be included in the summary. + +Finally, if no excerpt is specified and the "more" tag is not used, the blog post will capture the first 600 characters of the content and use this for the summary. + +## Implementing front-end pages + +The plugin provides several components for building the post list page (archive), category page, post details page and category list for the sidebar. + +### Post list page + +Use the `blogPosts` component to display a list of latest blog posts on a page. The component has the following properties: + +* **pageNumber** - this value is used to determine what page the user is on, it should be a routing parameter for the default markup. The default value is **{{ :page }}** to obtain the value from the route parameter `:page`. +* **categoryFilter** - a category slug to filter the posts by. If left blank, all posts are displayed. +* **postsPerPage** - how many posts to display on a single page (the pagination is supported automatically). The default value is 10. +* **noPostsMessage** - message to display in the empty post list. +* **sortOrder** - the column name and direction used for the sort order of the posts. The default value is **published_at desc**. +* **categoryPage** - path to the category page. The default value is **blog/category** - it matches the pages/blog/category.htm file in the theme directory. This property is used in the default component partial for creating links to the blog categories. +* **postPage** - path to the post details page. The default value is **blog/post** - it matches the pages/blog/post.htm file in the theme directory. This property is used in the default component partial for creating links to the blog posts. +* **exceptPost** - ignore a single post by its slug or unique ID. The ignored post will not be included in the list, useful for showing other/related posts. +* **exceptCategories** - ignore posts from a comma-separated list of categories, given by their unique slug. The ignored posts will not be included in the list. + +The blogPosts component injects the following variables to the page where it's used: + +* **posts** - a list of blog posts loaded from the database. +* **postPage** - contains the value of the `postPage` component's property. +* **category** - the blog category object loaded from the database. If the category is not found, the variable value is **null**. +* **categoryPage** - contains the value of the `categoryPage` component's property. +* **noPostsMessage** - contains the value of the `noPostsMessage` component's property. + +The component supports pagination and reads the current page index from the `:page` URL parameter. The next example shows the basic component usage on the blog home page: + + title = "Blog" + url = "/blog/:page?" + + [blogPosts] + postsPerPage = "5" + == + {% component 'blogPosts' %} + +The next example shows the basic component usage with the category filter: + + title = "Blog Category" + url = "/blog/category/:slug/:page?" + + [blogPosts] + categoryFilter = "{{ :slug }}" + == + function onEnd() + { + // Optional - set the page title to the category name + if ($this->category) + $this->page->title = $this->category->name; + } + == + {% if not category %} +

Category not found

+ {% else %} +

{{ category.name }}

+ + {% component 'blogPosts' %} + {% endif %} + +The post list and the pagination are coded in the default component partial `plugins/rainlab/blog/components/posts/default.htm`. If the default markup is not suitable for your website, feel free to copy it from the default partial and replace the `{% component %}` call in the example above with the partial contents. + +### Post page + +Use the `blogPost` component to display a blog post on a page. The component has the following properties: + +* **slug** - the value used for looking up the post by its slug. The default value is **{{ :slug }}** to obtain the value from the route parameter `:slug`. +* **categoryPage** - path to the category page. The default value is **blog/category** - it matches the pages/blog/category.htm file in the theme directory. This property is used in the default component partial for creating links to the blog categories. + +The component injects the following variables to the page where it's used: + +* **post** - the blog post object loaded from the database. If the post is not found, the variable value is **null**. + +The next example shows the basic component usage on the blog page: + + title = "Blog Post" + url = "/blog/post/:slug" + + [blogPost] + == + post) + $this->page->title = $this->post->title; + } + ?> + == + {% if post %} +

{{ post.title }}

+ + {% component 'blogPost' %} + {% else %} +

Post not found

+ {% endif %} + +The post details is coded in the default component partial `plugins/rainlab/blog/components/post/default.htm`. + +### Category list + +Use the `blogCategories` component to display a list of blog post categories with links. The component has the following properties: + +* **slug** - the value used for looking up the current category by its slug. The default value is **{{ :slug }}** to obtain the value from the route parameter `:slug`. +* **displayEmpty** - determines if empty categories should be displayed. The default value is false. +* **categoryPage** - path to the category page. The default value is **blog/category** - it matches the pages/blog/category.htm file in the theme directory. This property is used in the default component partial for creating links to the blog categories. + +The component injects the following variables to the page where it's used: + +* **categoryPage** - contains the value of the `categoryPage` component's property. +* **categories** - a list of blog categories loaded from the database. +* **currentCategorySlug** - slug of the current category. This property is used for marking the current category in the category list. + +The component can be used on any page. The next example shows the basic component usage on the blog home page: + + title = "Blog" + url = "/blog/:page?" + + [blogCategories] + == + ... + + ... + +The category list is coded in the default component partial `plugins/rainlab/blog/components/categories/default.htm`. + +### RSS feed + +Use the `blogRssFeed` component to display an RSS feed containing the latest blog posts. The following properties are supported: + +* **categoryFilter** - a category slug to filter the posts by. If left blank, all posts are displayed. +* **postsPerPage** - how many posts to display on the feed. The default value is 10. +* **blogPage** - path to the main blog page. The default value is **blog** - it matches the pages/blog.htm file in the theme directory. This property is used in the RSS feed for creating links to the main blog page. +* **postPage** - path to the post details page. The default value is **blog/post** - it matches the pages/blog/post.htm file in the theme directory. This property is used in the RSS feed for creating links to the blog posts. + +The component can be used on any page, it will hijack the entire page cycle to display the feed in RSS format. The next example shows how to use it: + + title = "RSS Feed" + url = "/blog/rss.xml" + + [blogRssFeed] + blogPage = "blog" + postPage = "blog/post" + == + + +## Configuration + +To overwrite the default configuration create a `config/rainlab/blog/config.php`. You can return only values you want to override. + +### Summary + +A summary attribute is generated for each post. + +If you enter an excerpt manually, it gets used as summary. Alternatively, you can use the `summary_separator` (default is ``) to mark the end of the summary. If a post contains no separator, the text gets truncated after the number of characters specified in `summary_default_length` (default is 600 characters). + +## Markdown guide + +October supports [standard markdown syntax](http://daringfireball.net/projects/markdown/) as well as [extended markdown syntax](http://michelf.ca/projects/php-markdown/extra/) + +### Classes and IDs + +Classes and IDs can be added to images and other elements as shown below: + +``` +[link](url){#id .class} +![1](image){#id .class} +# October {#id .class} +``` + +### Fenced code blogs + +Markdown extra makes it possible to use fenced code blocks. With fenced code blocks you do not need indentation on the areas you want to mark as code: + + + ``` + Code goes here + ``` + +You can also use the `~` symbol: + + ~~~ + Code goes here + ~~~ + +### Tables + +A *simple* table can be defined as follows: + +``` +First Header | Second Header +------------- | ------------- +Content Cell | Content Cell +Content Cell | Content Cell +``` + +If you want to you can also add a leading and tailing pipe: + +``` +| First Header | Second Header | +| ------------- | ------------- | +| Content Cell | Content Cell | +| Content Cell | Content Cell | +``` + +To add alignment to the cells you simply need to add a `:` either at the start or end of a separator: + +``` +| First Header | Second Header | +| :------------ | ------------: | +| Content Cell | Content Cell | +| Content Cell | Content Cell | +``` + +To center align cell just add `:` on both sides: + +``` +| First Header | Second Header | +| ------------- | :-----------: | +| Content Cell | Content Cell | +| Content Cell | Content Cell | +``` + +### Definition lists + +Below is an example of a simple definition list: + +``` +Laravel +: A popular PHP framework + +October +: Awesome CMS built on Laravel +``` + +A term can also have multiple definitions: + +``` +Laravel +: A popular PHP framework + +October +: Awesome CMS built on Laravel +: Supports markdown extra +``` + +You can also associate more than 1 term to a definition: + +``` +Laravel +October +: Built using PHP +``` + +### Footnotes + +With markdown extra it is possible to create reference style footnotes: + +``` +This is some text with a footnote.[^1] + +[^1]: And this is the footnote. +``` + +### Abbreviations + +With markdown extra you can add abbreviations to your markup. The use this functionality first create a definition list: + +``` +*[HTML]: Hyper Text Markup Language +*[PHP]: Hypertext Preprocessor +``` + +Now markdown extra will convert all occurrences of `HTML` and `PHP` as follows: + +``` +HTML +PHP +``` diff --git a/plugins/rainlab/blog/assets/css/rainlab.blog-export.css b/plugins/rainlab/blog/assets/css/rainlab.blog-export.css new file mode 100644 index 00000000..c0a42a7b --- /dev/null +++ b/plugins/rainlab/blog/assets/css/rainlab.blog-export.css @@ -0,0 +1,3 @@ +.export-behavior .export-columns { + max-height: 450px !important; +} diff --git a/plugins/rainlab/blog/assets/css/rainlab.blog-preview.css b/plugins/rainlab/blog/assets/css/rainlab.blog-preview.css new file mode 100644 index 00000000..eb21587d --- /dev/null +++ b/plugins/rainlab/blog/assets/css/rainlab.blog-preview.css @@ -0,0 +1,85 @@ +.blog-post-preview .editor-preview .preview-content { + padding: 20px; +} +.blog-post-preview .editor-preview span.image-placeholder { + display: block; +} +.blog-post-preview .editor-preview span.image-placeholder .upload-dropzone { + background: #ecf0f1; + display: block; + border: 1px solid #e5e9ec; + padding: 25px; + min-height: 123px; + position: relative; + text-align: center; + cursor: pointer; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} +.blog-post-preview .editor-preview span.image-placeholder .upload-dropzone span.label { + color: #b1b9be; + font-size: 16px; + display: inline-block; + margin-top: 25px; +} +.blog-post-preview .editor-preview span.image-placeholder .upload-dropzone:before { + display: inline-block; + font-family: FontAwesome; + font-weight: normal; + font-style: normal; + text-decoration: inherit; + -webkit-font-smoothing: antialiased; + *margin-right: .3em; + content: "\f03e"; + position: absolute; + left: 25px; + top: 25px; + line-height: 100%; + font-size: 73px; + color: #d1d3d4; +} +.blog-post-preview .editor-preview span.image-placeholder .upload-dropzone.hover, +.blog-post-preview .editor-preview span.image-placeholder .upload-dropzone:hover { + background: #2f99da; +} +.blog-post-preview .editor-preview span.image-placeholder .upload-dropzone.hover:before, +.blog-post-preview .editor-preview span.image-placeholder .upload-dropzone:hover:before, +.blog-post-preview .editor-preview span.image-placeholder .upload-dropzone.hover span.label, +.blog-post-preview .editor-preview span.image-placeholder .upload-dropzone:hover span.label { + color: white; +} +.blog-post-preview .editor-preview span.image-placeholder input[type=file] { + position: absolute; + left: -10000em; +} +.blog-post-preview-container .loading-indicator { + position: absolute; + display: none; + width: 20px; + height: 20px; + padding: 0!important; + background: transparent; + right: 10px; + left: auto; + top: 10px; +} +.blog-post-preview-container.loading-indicator-visible .loading-indicator { + display: block; +} +html.cssanimations .blog-post-preview span.image-placeholder.loading .upload-dropzone:before { + display: none; +} +html.cssanimations .blog-post-preview span.image-placeholder.loading .upload-dropzone .indicator { + display: block; + width: 50px; + height: 50px; + position: absolute; + left: 35px; + top: 35px; + background-image: url('../../../../../modules/system/assets/ui/images/loader-transparent.svg'); + background-size: 50px 50px; + background-position: 50% 50%; + -webkit-animation: spin 1s linear infinite; + animation: spin 1s linear infinite; +} diff --git a/plugins/rainlab/blog/assets/images/blog-icon.svg b/plugins/rainlab/blog/assets/images/blog-icon.svg new file mode 100644 index 00000000..ae6a619c --- /dev/null +++ b/plugins/rainlab/blog/assets/images/blog-icon.svg @@ -0,0 +1,30 @@ + + + + blog-icon + Created with Sketch. + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/plugins/rainlab/blog/assets/js/post-form.js b/plugins/rainlab/blog/assets/js/post-form.js new file mode 100644 index 00000000..cc5b8664 --- /dev/null +++ b/plugins/rainlab/blog/assets/js/post-form.js @@ -0,0 +1,185 @@ ++function ($) { "use strict"; + var PostForm = function () { + this.$form = $('#post-form') + this.$markdownEditor = $('[data-field-name=content] [data-control=markdowneditor]:first', this.$form) + this.$preview = $('.editor-preview', this.$markdownEditor) + + this.formAction = this.$form.attr('action') + this.sessionKey = $('input[name=_session_key]', this.$form).val() + + if (this.$markdownEditor.length > 0) { + this.codeEditor = this.$markdownEditor.markdownEditor('getEditorObject') + + this.$markdownEditor.on('initPreview.oc.markdowneditor', $.proxy(this.initPreview, this)) + + this.initDropzones() + this.initFormEvents() + this.addToolbarButton() + } + + this.initLayout() + } + + PostForm.prototype.addToolbarButton = function() { + this.buttonClickCount = 1 + + var self = this, + $button = this.$markdownEditor.markdownEditor('findToolbarButton', 'image') + + if (!$button.length) return + + $button.data('button-action', 'insertLine') + $button.data('button-template', '\n\n![1](image)\n') + + $button.on('click', function() { + $button.data('button-template', '\n\n!['+self.buttonClickCount+'](image)\n') + self.buttonClickCount++ + }) + } + + PostForm.prototype.initPreview = function() { + this.initImageUploaders() + } + + PostForm.prototype.updateScroll = function() { + // Reserved in case MarkdownEditor uses scrollbar plugin + // this.$preview.data('oc.scrollbar').update() + } + + PostForm.prototype.initImageUploaders = function() { + var self = this + $('span.image-placeholder .upload-dropzone', this.$preview).each(function(){ + var + $placeholder = $(this).parent(), + $link = $('span.label', $placeholder), + placeholderIndex = $placeholder.data('index') + + var uploaderOptions = { + url: self.formAction, + clickable: [$(this).get(0), $link.get(0)], + previewsContainer: $('
').get(0), + paramName: 'file', + headers: {} + } + + /* + * Add CSRF token to headers + */ + var token = $('meta[name="csrf-token"]').attr('content') + if (token) { + uploaderOptions.headers['X-CSRF-TOKEN'] = token + } + + var dropzone = new Dropzone($(this).get(0), uploaderOptions) + + dropzone.on('error', function(file, error) { + alert('Error uploading file: ' + error) + }) + dropzone.on('success', function(file, data){ + if (data.error) + alert(data.error) + else { + self.pauseUpdates() + var $img = $('') + $img.load(function(){ + self.updateScroll() + }) + + $placeholder.replaceWith($img) + + self.codeEditor.replace('!['+data.file+']('+data.path+')', { + needle: '!['+placeholderIndex+'](image)' + }) + self.resumeUpdates() + } + }) + dropzone.on('complete', function(){ + $placeholder.removeClass('loading') + }) + dropzone.on('sending', function(file, xhr, formData) { + formData.append('X_BLOG_IMAGE_UPLOAD', 1) + formData.append('_session_key', self.sessionKey) + $placeholder.addClass('loading') + }) + }) + } + + PostForm.prototype.pauseUpdates = function() { + this.$markdownEditor.markdownEditor('pauseUpdates') + } + + PostForm.prototype.resumeUpdates = function() { + this.$markdownEditor.markdownEditor('resumeUpdates') + } + + PostForm.prototype.initDropzones = function() { + $(document).bind('dragover', function (e) { + var dropZone = $('span.image-placeholder .upload-dropzone'), + foundDropzone, + timeout = window.dropZoneTimeout + + if (!timeout) + dropZone.addClass('in'); + else + clearTimeout(timeout); + + var found = false, + node = e.target + + do { + if ($(node).hasClass('dropzone')) { + found = true + foundDropzone = $(node) + break + } + + node = node.parentNode; + + } while (node != null); + + dropZone.removeClass('in hover') + + if (found) + foundDropzone.addClass('hover') + + window.dropZoneTimeout = setTimeout(function () { + window.dropZoneTimeout = null + dropZone.removeClass('in hover') + }, 100) + }) + } + + PostForm.prototype.initFormEvents = function() { + $(document).on('ajaxSuccess', '#post-form', function(event, context, data){ + if (context.handler == 'onSave' && !data.X_OCTOBER_ERROR_FIELDS) { + $(this).trigger('unchange.oc.changeMonitor') + } + }) + } + + PostForm.prototype.initLayout = function() { + $('#Form-secondaryTabs .tab-pane.layout-cell:not(:first-child)').addClass('padded-pane') + $('#Form-secondaryTabs .nav-tabs > li:not(:first-child)').addClass('tab-content-bg') + } + + PostForm.prototype.replacePlaceholder = function(placeholder, placeholderHtmlReplacement, mdCodePlaceholder, mdCodeReplacement) { + this.pauseUpdates() + placeholder.replaceWith(placeholderHtmlReplacement) + + this.codeEditor.replace(mdCodeReplacement, { + needle: mdCodePlaceholder + }) + this.updateScroll() + this.resumeUpdates() + } + + $(document).ready(function(){ + var form = new PostForm() + + if ($.oc === undefined) + $.oc = {} + + $.oc.blogPostForm = form + }) + +}(window.jQuery); \ No newline at end of file diff --git a/plugins/rainlab/blog/assets/less/rainlab.blog-preview.less b/plugins/rainlab/blog/assets/less/rainlab.blog-preview.less new file mode 100644 index 00000000..95334a05 --- /dev/null +++ b/plugins/rainlab/blog/assets/less/rainlab.blog-preview.less @@ -0,0 +1,99 @@ +@import "../../../../../modules/backend/assets/less/core/boot.less"; + +.blog-post-preview .editor-preview { + .preview-content { + padding: 20px; + } + + span.image-placeholder { + display: block; + + .upload-dropzone { + background: #ecf0f1; + display: block; + border: 1px solid #e5e9ec; + padding: 25px; + min-height: 123px; + position: relative; + text-align: center; + cursor: pointer; + .box-sizing(border-box); + + span.label { + color: #b1b9be; + font-size: 16px; + display: inline-block; + margin-top: 25px; + } + + &:before { + display: inline-block; + .icon(@picture-o); + position: absolute; + left: 25px; + top: 25px; + line-height: 100%; + font-size: 73px; + color: #d1d3d4; + } + + &.hover, &:hover { + background: #2f99da; + + &:before, span.label { + color: white; + } + } + } + + input[type=file] { + position: absolute; + left: -10000em; + } + } +} + +.blog-post-preview-container { + .loading-indicator { + position: absolute; + display: none; + width: 20px; + height: 20px; + padding: 0!important; + background: transparent; + right: 10px; + left: auto; + top: 10px; + } + + &.loading-indicator-visible { + .loading-indicator { + display: block; + } + } +} + +html.cssanimations { + .blog-post-preview { + span.image-placeholder.loading { + .upload-dropzone { + &:before { + display: none; + } + + .indicator { + display: block; + width: 50px; + height: 50px; + position: absolute; + left: 35px; + top: 35px; + background-image:url('../../../../../modules/system/assets/ui/images/loader-transparent.svg'); + background-size: 50px 50px; + background-position: 50% 50%; + .animation(spin 1s linear infinite); + } + } + } + } +} \ No newline at end of file diff --git a/plugins/rainlab/blog/classes/TagProcessor.php b/plugins/rainlab/blog/classes/TagProcessor.php new file mode 100644 index 00000000..d24f481d --- /dev/null +++ b/plugins/rainlab/blog/classes/TagProcessor.php @@ -0,0 +1,39 @@ +callbacks[] = $callback; + } + + public function processTags($markup, $preview) + { + foreach ($this->callbacks as $callback) { + $markup = $callback($markup, $preview); + } + + return $markup; + } +} diff --git a/plugins/rainlab/blog/components/Categories.php b/plugins/rainlab/blog/components/Categories.php new file mode 100644 index 00000000..8caed92f --- /dev/null +++ b/plugins/rainlab/blog/components/Categories.php @@ -0,0 +1,113 @@ + 'rainlab.blog::lang.settings.category_title', + 'description' => 'rainlab.blog::lang.settings.category_description' + ]; + } + + public function defineProperties() + { + return [ + 'slug' => [ + 'title' => 'rainlab.blog::lang.settings.category_slug', + 'description' => 'rainlab.blog::lang.settings.category_slug_description', + 'default' => '{{ :slug }}', + 'type' => 'string', + ], + 'displayEmpty' => [ + 'title' => 'rainlab.blog::lang.settings.category_display_empty', + 'description' => 'rainlab.blog::lang.settings.category_display_empty_description', + 'type' => 'checkbox', + 'default' => 0, + ], + 'categoryPage' => [ + 'title' => 'rainlab.blog::lang.settings.category_page', + 'description' => 'rainlab.blog::lang.settings.category_page_description', + 'type' => 'dropdown', + 'default' => 'blog/category', + 'group' => 'rainlab.blog::lang.settings.group_links', + ], + ]; + } + + public function getCategoryPageOptions() + { + return Page::sortBy('baseFileName')->lists('baseFileName', 'baseFileName'); + } + + public function onRun() + { + $this->currentCategorySlug = $this->page['currentCategorySlug'] = $this->property('slug'); + $this->categoryPage = $this->page['categoryPage'] = $this->property('categoryPage'); + $this->categories = $this->page['categories'] = $this->loadCategories(); + } + + /** + * Load all categories or, depending on the option, only those that have blog posts + * @return mixed + */ + protected function loadCategories() + { + $categories = BlogCategory::with('posts_count')->getNested(); + if (!$this->property('displayEmpty')) { + $iterator = function ($categories) use (&$iterator) { + return $categories->reject(function ($category) use (&$iterator) { + if ($category->getNestedPostCount() == 0) { + return true; + } + if ($category->children) { + $category->children = $iterator($category->children); + } + return false; + }); + }; + $categories = $iterator($categories); + } + + /* + * Add a "url" helper attribute for linking to each category + */ + return $this->linkCategories($categories); + } + + /** + * Sets the URL on each category according to the defined category page + * @return void + */ + protected function linkCategories($categories) + { + return $categories->each(function ($category) { + $category->setUrl($this->categoryPage, $this->controller); + + if ($category->children) { + $this->linkCategories($category->children); + } + }); + } +} diff --git a/plugins/rainlab/blog/components/Post.php b/plugins/rainlab/blog/components/Post.php new file mode 100644 index 00000000..8faa8ff6 --- /dev/null +++ b/plugins/rainlab/blog/components/Post.php @@ -0,0 +1,156 @@ + 'rainlab.blog::lang.settings.post_title', + 'description' => 'rainlab.blog::lang.settings.post_description' + ]; + } + + public function defineProperties() + { + return [ + 'slug' => [ + 'title' => 'rainlab.blog::lang.settings.post_slug', + 'description' => 'rainlab.blog::lang.settings.post_slug_description', + 'default' => '{{ :slug }}', + 'type' => 'string', + ], + 'categoryPage' => [ + 'title' => 'rainlab.blog::lang.settings.post_category', + 'description' => 'rainlab.blog::lang.settings.post_category_description', + 'type' => 'dropdown', + 'default' => 'blog/category', + ], + ]; + } + + public function getCategoryPageOptions() + { + return Page::sortBy('baseFileName')->lists('baseFileName', 'baseFileName'); + } + + public function init() + { + Event::listen('translate.localePicker.translateParams', function ($page, $params, $oldLocale, $newLocale) { + $newParams = $params; + + if (isset($params['slug'])) { + $records = BlogPost::transWhere('slug', $params['slug'], $oldLocale)->first(); + if ($records) { + $records->translateContext($newLocale); + $newParams['slug'] = $records['slug']; + } + } + + return $newParams; + }); + } + + public function onRun() + { + $this->categoryPage = $this->page['categoryPage'] = $this->property('categoryPage'); + $this->post = $this->page['post'] = $this->loadPost(); + if (!$this->post) { + $this->setStatusCode(404); + return $this->controller->run('404'); + } + } + + public function onRender() + { + if (empty($this->post)) { + $this->post = $this->page['post'] = $this->loadPost(); + } + } + + protected function loadPost() + { + $slug = $this->property('slug'); + + $post = new BlogPost; + $query = $post->query(); + + if ($post->isClassExtendedWith('RainLab.Translate.Behaviors.TranslatableModel')) { + $query->transWhere('slug', $slug); + } else { + $query->where('slug', $slug); + } + + if (!$this->checkEditor()) { + $query->isPublished(); + } + + $post = $query->first(); + + /* + * Add a "url" helper attribute for linking to each category + */ + if ($post && $post->exists && $post->categories->count()) { + $post->categories->each(function($category) { + $category->setUrl($this->categoryPage, $this->controller); + }); + } + + return $post; + } + + public function previousPost() + { + return $this->getPostSibling(-1); + } + + public function nextPost() + { + return $this->getPostSibling(1); + } + + protected function getPostSibling($direction = 1) + { + if (!$this->post) { + return; + } + + $method = $direction === -1 ? 'previousPost' : 'nextPost'; + + if (!$post = $this->post->$method()) { + return; + } + + $postPage = $this->getPage()->getBaseFileName(); + + $post->setUrl($postPage, $this->controller); + + $post->categories->each(function($category) { + $category->setUrl($this->categoryPage, $this->controller); + }); + + return $post; + } + + protected function checkEditor() + { + $backendUser = BackendAuth::getUser(); + + return $backendUser && $backendUser->hasAccess('rainlab.blog.access_posts'); + } +} diff --git a/plugins/rainlab/blog/components/Posts.php b/plugins/rainlab/blog/components/Posts.php new file mode 100644 index 00000000..3ac3c4a3 --- /dev/null +++ b/plugins/rainlab/blog/components/Posts.php @@ -0,0 +1,259 @@ + 'rainlab.blog::lang.settings.posts_title', + 'description' => 'rainlab.blog::lang.settings.posts_description' + ]; + } + + public function defineProperties() + { + return [ + 'pageNumber' => [ + 'title' => 'rainlab.blog::lang.settings.posts_pagination', + 'description' => 'rainlab.blog::lang.settings.posts_pagination_description', + 'type' => 'string', + 'default' => '{{ :page }}', + ], + 'categoryFilter' => [ + 'title' => 'rainlab.blog::lang.settings.posts_filter', + 'description' => 'rainlab.blog::lang.settings.posts_filter_description', + 'type' => 'string', + 'default' => '', + ], + 'postsPerPage' => [ + 'title' => 'rainlab.blog::lang.settings.posts_per_page', + 'type' => 'string', + 'validationPattern' => '^[0-9]+$', + 'validationMessage' => 'rainlab.blog::lang.settings.posts_per_page_validation', + 'default' => '10', + ], + 'noPostsMessage' => [ + 'title' => 'rainlab.blog::lang.settings.posts_no_posts', + 'description' => 'rainlab.blog::lang.settings.posts_no_posts_description', + 'type' => 'string', + 'default' => Lang::get('rainlab.blog::lang.settings.posts_no_posts_default'), + 'showExternalParam' => false, + ], + 'sortOrder' => [ + 'title' => 'rainlab.blog::lang.settings.posts_order', + 'description' => 'rainlab.blog::lang.settings.posts_order_description', + 'type' => 'dropdown', + 'default' => 'published_at desc', + ], + 'categoryPage' => [ + 'title' => 'rainlab.blog::lang.settings.posts_category', + 'description' => 'rainlab.blog::lang.settings.posts_category_description', + 'type' => 'dropdown', + 'default' => 'blog/category', + 'group' => 'rainlab.blog::lang.settings.group_links', + ], + 'postPage' => [ + 'title' => 'rainlab.blog::lang.settings.posts_post', + 'description' => 'rainlab.blog::lang.settings.posts_post_description', + 'type' => 'dropdown', + 'default' => 'blog/post', + 'group' => 'rainlab.blog::lang.settings.group_links', + ], + 'exceptPost' => [ + 'title' => 'rainlab.blog::lang.settings.posts_except_post', + 'description' => 'rainlab.blog::lang.settings.posts_except_post_description', + 'type' => 'string', + 'validationPattern' => '^[a-z0-9\-_,\s]+$', + 'validationMessage' => 'rainlab.blog::lang.settings.posts_except_post_validation', + 'default' => '', + 'group' => 'rainlab.blog::lang.settings.group_exceptions', + ], + 'exceptCategories' => [ + 'title' => 'rainlab.blog::lang.settings.posts_except_categories', + 'description' => 'rainlab.blog::lang.settings.posts_except_categories_description', + 'type' => 'string', + 'validationPattern' => '^[a-z0-9\-_,\s]+$', + 'validationMessage' => 'rainlab.blog::lang.settings.posts_except_categories_validation', + 'default' => '', + 'group' => 'rainlab.blog::lang.settings.group_exceptions', + ], + ]; + } + + public function getCategoryPageOptions() + { + return Page::sortBy('baseFileName')->lists('baseFileName', 'baseFileName'); + } + + public function getPostPageOptions() + { + return Page::sortBy('baseFileName')->lists('baseFileName', 'baseFileName'); + } + + public function getSortOrderOptions() + { + $options = BlogPost::$allowedSortingOptions; + + foreach ($options as $key => $value) { + $options[$key] = Lang::get($value); + } + + return $options; + } + + public function onRun() + { + $this->prepareVars(); + + $this->category = $this->page['category'] = $this->loadCategory(); + $this->posts = $this->page['posts'] = $this->listPosts(); + + /* + * If the page number is not valid, redirect + */ + if ($pageNumberParam = $this->paramName('pageNumber')) { + $currentPage = $this->property('pageNumber'); + + if ($currentPage > ($lastPage = $this->posts->lastPage()) && $currentPage > 1) { + return Redirect::to($this->currentPageUrl([$pageNumberParam => $lastPage])); + } + } + } + + protected function prepareVars() + { + $this->pageParam = $this->page['pageParam'] = $this->paramName('pageNumber'); + $this->noPostsMessage = $this->page['noPostsMessage'] = $this->property('noPostsMessage'); + + /* + * Page links + */ + $this->postPage = $this->page['postPage'] = $this->property('postPage'); + $this->categoryPage = $this->page['categoryPage'] = $this->property('categoryPage'); + } + + protected function listPosts() + { + $category = $this->category ? $this->category->id : null; + $categorySlug = $this->category ? $this->category->slug : null; + + /* + * List all the posts, eager load their categories + */ + $isPublished = !$this->checkEditor(); + + $posts = BlogPost::with(['categories', 'featured_images'])->listFrontEnd([ + 'page' => $this->property('pageNumber'), + 'sort' => $this->property('sortOrder'), + 'perPage' => $this->property('postsPerPage'), + 'search' => trim(input('search')), + 'category' => $category, + 'published' => $isPublished, + 'exceptPost' => is_array($this->property('exceptPost')) + ? $this->property('exceptPost') + : preg_split('/,\s*/', $this->property('exceptPost'), -1, PREG_SPLIT_NO_EMPTY), + 'exceptCategories' => is_array($this->property('exceptCategories')) + ? $this->property('exceptCategories') + : preg_split('/,\s*/', $this->property('exceptCategories'), -1, PREG_SPLIT_NO_EMPTY), + ]); + + /* + * Add a "url" helper attribute for linking to each post and category + */ + $posts->each(function($post) use ($categorySlug) { + $post->setUrl($this->postPage, $this->controller, ['category' => $categorySlug]); + + $post->categories->each(function($category) { + $category->setUrl($this->categoryPage, $this->controller); + }); + }); + + return $posts; + } + + protected function loadCategory() + { + if (!$slug = $this->property('categoryFilter')) { + return null; + } + + $category = new BlogCategory; + + $category = $category->isClassExtendedWith('RainLab.Translate.Behaviors.TranslatableModel') + ? $category->transWhere('slug', $slug) + : $category->where('slug', $slug); + + $category = $category->first(); + + return $category ?: null; + } + + protected function checkEditor() + { + $backendUser = BackendAuth::getUser(); + + return $backendUser && + $backendUser->hasAccess('rainlab.blog.access_posts') && + BlogSettings::get('show_all_posts', true); + } +} diff --git a/plugins/rainlab/blog/components/RssFeed.php b/plugins/rainlab/blog/components/RssFeed.php new file mode 100644 index 00000000..295433b0 --- /dev/null +++ b/plugins/rainlab/blog/components/RssFeed.php @@ -0,0 +1,159 @@ + 'rainlab.blog::lang.settings.rssfeed_title', + 'description' => 'rainlab.blog::lang.settings.rssfeed_description' + ]; + } + + public function defineProperties() + { + return [ + 'categoryFilter' => [ + 'title' => 'rainlab.blog::lang.settings.posts_filter', + 'description' => 'rainlab.blog::lang.settings.posts_filter_description', + 'type' => 'string', + 'default' => '', + ], + 'sortOrder' => [ + 'title' => 'rainlab.blog::lang.settings.posts_order', + 'description' => 'rainlab.blog::lang.settings.posts_order_description', + 'type' => 'dropdown', + 'default' => 'created_at desc', + ], + 'postsPerPage' => [ + 'title' => 'rainlab.blog::lang.settings.posts_per_page', + 'type' => 'string', + 'validationPattern' => '^[0-9]+$', + 'validationMessage' => 'rainlab.blog::lang.settings.posts_per_page_validation', + 'default' => '10', + ], + 'blogPage' => [ + 'title' => 'rainlab.blog::lang.settings.rssfeed_blog', + 'description' => 'rainlab.blog::lang.settings.rssfeed_blog_description', + 'type' => 'dropdown', + 'default' => 'blog/post', + 'group' => 'rainlab.blog::lang.settings.group_links', + ], + 'postPage' => [ + 'title' => 'rainlab.blog::lang.settings.posts_post', + 'description' => 'rainlab.blog::lang.settings.posts_post_description', + 'type' => 'dropdown', + 'default' => 'blog/post', + 'group' => 'rainlab.blog::lang.settings.group_links', + ], + ]; + } + + public function getBlogPageOptions() + { + return Page::sortBy('baseFileName')->lists('baseFileName', 'baseFileName'); + } + + public function getPostPageOptions() + { + return Page::sortBy('baseFileName')->lists('baseFileName', 'baseFileName'); + } + + public function getSortOrderOptions() + { + $options = BlogPost::$allowedSortingOptions; + + foreach ($options as $key => $value) { + $options[$key] = Lang::get($value); + } + + return $options; + } + + public function onRun() + { + $this->prepareVars(); + + $xmlFeed = $this->renderPartial('@default'); + + return Response::make($xmlFeed, '200')->header('Content-Type', 'text/xml'); + } + + protected function prepareVars() + { + $this->blogPage = $this->page['blogPage'] = $this->property('blogPage'); + $this->postPage = $this->page['postPage'] = $this->property('postPage'); + $this->category = $this->page['category'] = $this->loadCategory(); + $this->posts = $this->page['posts'] = $this->listPosts(); + + $this->page['link'] = $this->pageUrl($this->blogPage); + $this->page['rssLink'] = $this->currentPageUrl(); + } + + protected function listPosts() + { + $category = $this->category ? $this->category->id : null; + + /* + * List all the posts, eager load their categories + */ + $posts = BlogPost::with('categories')->listFrontEnd([ + 'sort' => $this->property('sortOrder'), + 'perPage' => $this->property('postsPerPage'), + 'category' => $category + ]); + + /* + * Add a "url" helper attribute for linking to each post and category + */ + $posts->each(function($post) { + $post->setUrl($this->postPage, $this->controller); + }); + + return $posts; + } + + protected function loadCategory() + { + if (!$categoryId = $this->property('categoryFilter')) { + return null; + } + + if (!$category = BlogCategory::whereSlug($categoryId)->first()) { + return null; + } + + return $category; + } +} diff --git a/plugins/rainlab/blog/components/categories/default.htm b/plugins/rainlab/blog/components/categories/default.htm new file mode 100644 index 00000000..e9b02807 --- /dev/null +++ b/plugins/rainlab/blog/components/categories/default.htm @@ -0,0 +1,10 @@ +{% if __SELF__.categories|length > 0 %} +
    + {% partial __SELF__ ~ "::items" + categories = __SELF__.categories + currentCategorySlug = __SELF__.currentCategorySlug + %} +
+{% else %} +

No categories were found.

+{% endif %} diff --git a/plugins/rainlab/blog/components/categories/items.htm b/plugins/rainlab/blog/components/categories/items.htm new file mode 100644 index 00000000..cb1b8e55 --- /dev/null +++ b/plugins/rainlab/blog/components/categories/items.htm @@ -0,0 +1,18 @@ +{% for category in categories %} + {% set postCount = category.post_count %} +
  • + {{ category.name }} + {% if postCount %} + {{ postCount }} + {% endif %} + + {% if category.children|length > 0 %} +
      + {% partial __SELF__ ~ "::items" + categories=category.children + currentCategorySlug=currentCategorySlug + %} +
    + {% endif %} +
  • +{% endfor %} diff --git a/plugins/rainlab/blog/components/post/default.htm b/plugins/rainlab/blog/components/post/default.htm new file mode 100644 index 00000000..291d4f32 --- /dev/null +++ b/plugins/rainlab/blog/components/post/default.htm @@ -0,0 +1,27 @@ +{% set post = __SELF__.post %} + +
    {{ post.content_html|raw }}
    + +{% if post.featured_images|length %} + +{% endif %} + +

    + Posted + {% if post.categories|length %} in + {% for category in post.categories %} + {{ category.name }}{% if not loop.last %}, {% endif %} + {% endfor %} + {% endif %} + on {{ post.published_at|date('M d, Y') }} +

    diff --git a/plugins/rainlab/blog/components/posts/default.htm b/plugins/rainlab/blog/components/posts/default.htm new file mode 100644 index 00000000..0c0e2b27 --- /dev/null +++ b/plugins/rainlab/blog/components/posts/default.htm @@ -0,0 +1,40 @@ +{% set posts = __SELF__.posts %} + +
      + {% for post in posts %} +
    • +

      {{ post.title }}

      + +

      + Posted + {% if post.categories|length %} in {% endif %} + {% for category in post.categories %} + {{ category.name }}{% if not loop.last %}, {% endif %} + {% endfor %} + on {{ post.published_at|date('M d, Y') }} +

      + +

      {{ post.summary|raw }}

      +
    • + {% else %} +
    • {{ __SELF__.noPostsMessage }}
    • + {% endfor %} +
    + +{% if posts.lastPage > 1 %} +
      + {% if posts.currentPage > 1 %} +
    • ← Prev
    • + {% endif %} + + {% for page in 1..posts.lastPage %} +
    • + {{ page }} +
    • + {% endfor %} + + {% if posts.lastPage > posts.currentPage %} +
    • Next →
    • + {% endif %} +
    +{% endif %} diff --git a/plugins/rainlab/blog/components/rssfeed/default.htm b/plugins/rainlab/blog/components/rssfeed/default.htm new file mode 100644 index 00000000..b4527ec8 --- /dev/null +++ b/plugins/rainlab/blog/components/rssfeed/default.htm @@ -0,0 +1,18 @@ + + + + {{ this.page.meta_title ?: this.page.title }} + {{ link }} + {{ this.page.meta_description ?: this.page.description }} + + {% for post in posts %} + + {{ post.title }} + {{ post.url }} + {{ post.url }} + {{ post.published_at.toRfc2822String }} + {{ post.summary }} + + {% endfor %} + + diff --git a/plugins/rainlab/blog/composer.json b/plugins/rainlab/blog/composer.json new file mode 100644 index 00000000..7f49a069 --- /dev/null +++ b/plugins/rainlab/blog/composer.json @@ -0,0 +1,25 @@ +{ + "name": "rainlab/blog-plugin", + "type": "october-plugin", + "description": "Blog plugin for October CMS", + "homepage": "https://octobercms.com/plugin/rainlab-blog", + "keywords": ["october", "octobercms", "blog"], + "license": "MIT", + "authors": [ + { + "name": "Alexey Bobkov", + "email": "aleksey.bobkov@gmail.com", + "role": "Co-founder" + }, + { + "name": "Samuel Georges", + "email": "daftspunky@gmail.com", + "role": "Co-founder" + } + ], + "require": { + "php": ">=7.0", + "composer/installers": "~1.0" + }, + "minimum-stability": "dev" +} diff --git a/plugins/rainlab/blog/config/config.php b/plugins/rainlab/blog/config/config.php new file mode 100644 index 00000000..bdf5743b --- /dev/null +++ b/plugins/rainlab/blog/config/config.php @@ -0,0 +1,18 @@ + '', + + 'summary_default_length' => 600 + +]; diff --git a/plugins/rainlab/blog/controllers/Categories.php b/plugins/rainlab/blog/controllers/Categories.php new file mode 100644 index 00000000..583cdbf2 --- /dev/null +++ b/plugins/rainlab/blog/controllers/Categories.php @@ -0,0 +1,47 @@ +delete(); + } + + Flash::success(Lang::get('rainlab.blog::lang.category.delete_success')); + } + + return $this->listRefresh(); + } +} diff --git a/plugins/rainlab/blog/controllers/Posts.php b/plugins/rainlab/blog/controllers/Posts.php new file mode 100644 index 00000000..311087f2 --- /dev/null +++ b/plugins/rainlab/blog/controllers/Posts.php @@ -0,0 +1,153 @@ +vars['postsTotal'] = Post::count(); + $this->vars['postsPublished'] = Post::isPublished()->count(); + $this->vars['postsDrafts'] = $this->vars['postsTotal'] - $this->vars['postsPublished']; + + $this->asExtension('ListController')->index(); + } + + public function create() + { + BackendMenu::setContextSideMenu('new_post'); + + $this->bodyClass = 'compact-container'; + $this->addCss('/plugins/rainlab/blog/assets/css/rainlab.blog-preview.css'); + $this->addJs('/plugins/rainlab/blog/assets/js/post-form.js'); + + return $this->asExtension('FormController')->create(); + } + + public function update($recordId = null) + { + $this->bodyClass = 'compact-container'; + $this->addCss('/plugins/rainlab/blog/assets/css/rainlab.blog-preview.css'); + $this->addJs('/plugins/rainlab/blog/assets/js/post-form.js'); + + return $this->asExtension('FormController')->update($recordId); + } + + public function export() + { + $this->addCss('/plugins/rainlab/blog/assets/css/rainlab.blog-export.css'); + + return $this->asExtension('ImportExportController')->export(); + } + + public function listExtendQuery($query) + { + if (!$this->user->hasAnyAccess(['rainlab.blog.access_other_posts'])) { + $query->where('user_id', $this->user->id); + } + } + + public function formExtendQuery($query) + { + if (!$this->user->hasAnyAccess(['rainlab.blog.access_other_posts'])) { + $query->where('user_id', $this->user->id); + } + } + + public function formExtendFieldsBefore($widget) + { + if (!$model = $widget->model) { + return; + } + + if ($model instanceof Post && $model->isClassExtendedWith('RainLab.Translate.Behaviors.TranslatableModel')) { + $widget->secondaryTabs['fields']['content']['type'] = 'RainLab\Blog\FormWidgets\MLBlogMarkdown'; + } + + if (BlogSettings::get('use_legacy_editor', false)) { + $widget->secondaryTabs['fields']['content']['legacyMode'] = true; + } + } + + public function index_onDelete() + { + if (($checkedIds = post('checked')) && is_array($checkedIds) && count($checkedIds)) { + + foreach ($checkedIds as $postId) { + if ((!$post = Post::find($postId)) || !$post->canEdit($this->user)) { + continue; + } + + $post->delete(); + } + + Flash::success(Lang::get('rainlab.blog::lang.post.delete_success')); + } + + return $this->listRefresh(); + } + + /** + * {@inheritDoc} + */ + public function listInjectRowClass($record, $definition = null) + { + if (!$record->published) { + return 'safe disabled'; + } + } + + public function formBeforeCreate($model) + { + $model->user_id = $this->user->id; + } + + public function onRefreshPreview() + { + $data = post('Post'); + + $previewHtml = Post::formatHtml($data['content'], true); + + return [ + 'preview' => $previewHtml + ]; + } +} diff --git a/plugins/rainlab/blog/controllers/categories/_list_toolbar.htm b/plugins/rainlab/blog/controllers/categories/_list_toolbar.htm new file mode 100644 index 00000000..fe438117 --- /dev/null +++ b/plugins/rainlab/blog/controllers/categories/_list_toolbar.htm @@ -0,0 +1,25 @@ +
    + + + + + + + + + +
    diff --git a/plugins/rainlab/blog/controllers/categories/_reorder_toolbar.htm b/plugins/rainlab/blog/controllers/categories/_reorder_toolbar.htm new file mode 100644 index 00000000..de33eb7b --- /dev/null +++ b/plugins/rainlab/blog/controllers/categories/_reorder_toolbar.htm @@ -0,0 +1,5 @@ + \ No newline at end of file diff --git a/plugins/rainlab/blog/controllers/categories/config_form.yaml b/plugins/rainlab/blog/controllers/categories/config_form.yaml new file mode 100644 index 00000000..250c41aa --- /dev/null +++ b/plugins/rainlab/blog/controllers/categories/config_form.yaml @@ -0,0 +1,16 @@ +# =================================== +# Form Behavior Config +# =================================== + +name: rainlab.blog::lang.blog.create_category +form: $/rainlab/blog/models/category/fields.yaml +modelClass: RainLab\Blog\Models\Category +defaultRedirect: rainlab/blog/categories + +create: + redirect: rainlab/blog/categories/update/:id + redirectClose: rainlab/blog/categories + +update: + redirect: rainlab/blog/categories + redirectClose: rainlab/blog/categories diff --git a/plugins/rainlab/blog/controllers/categories/config_list.yaml b/plugins/rainlab/blog/controllers/categories/config_list.yaml new file mode 100644 index 00000000..c2b6b205 --- /dev/null +++ b/plugins/rainlab/blog/controllers/categories/config_list.yaml @@ -0,0 +1,43 @@ +# =================================== +# List Behavior Config +# =================================== + +# Model List Column configuration +list: $/rainlab/blog/models/category/columns.yaml + +# Model Class name +modelClass: RainLab\Blog\Models\Category + +# List Title +title: rainlab.blog::lang.categories.list_title + +# Link URL for each record +recordUrl: rainlab/blog/categories/update/:id + +# Message to display if the list is empty +noRecordsMessage: backend::lang.list.no_records + +# Records to display per page +recordsPerPage: 5 + +# Display checkboxes next to each record +showCheckboxes: true + +# Toolbar widget configuration +toolbar: + # Partial for toolbar buttons + buttons: list_toolbar + + # Search widget configuration + search: + prompt: backend::lang.list.search_prompt + +# Legacy (v1) +showTree: true + +# Reordering +structure: + showTree: true + showReorder: true + treeExpanded: true + maxDepth: 0 diff --git a/plugins/rainlab/blog/controllers/categories/config_reorder.yaml b/plugins/rainlab/blog/controllers/categories/config_reorder.yaml new file mode 100644 index 00000000..392b0c51 --- /dev/null +++ b/plugins/rainlab/blog/controllers/categories/config_reorder.yaml @@ -0,0 +1,17 @@ +# =================================== +# Reorder Behavior Config +# =================================== + +# Reorder Title +title: rainlab.blog::lang.category.reorder + +# Attribute name +nameFrom: name + +# Model Class name +modelClass: RainLab\Blog\Models\Category + +# Toolbar widget configuration +toolbar: + # Partial for toolbar buttons + buttons: reorder_toolbar \ No newline at end of file diff --git a/plugins/rainlab/blog/controllers/categories/create.htm b/plugins/rainlab/blog/controllers/categories/create.htm new file mode 100644 index 00000000..ea47c339 --- /dev/null +++ b/plugins/rainlab/blog/controllers/categories/create.htm @@ -0,0 +1,46 @@ + +
      +
    • +
    • pageTitle)) ?>
    • +
    + + +fatalError): ?> + + 'layout']) ?> + +
    + formRender() ?> +
    + +
    +
    + + + + + +
    +
    + + + + +

    fatalError)) ?>

    +

    + diff --git a/plugins/rainlab/blog/controllers/categories/index.htm b/plugins/rainlab/blog/controllers/categories/index.htm new file mode 100644 index 00000000..766877d9 --- /dev/null +++ b/plugins/rainlab/blog/controllers/categories/index.htm @@ -0,0 +1,2 @@ + +listRender() ?> diff --git a/plugins/rainlab/blog/controllers/categories/reorder.htm b/plugins/rainlab/blog/controllers/categories/reorder.htm new file mode 100644 index 00000000..407face1 --- /dev/null +++ b/plugins/rainlab/blog/controllers/categories/reorder.htm @@ -0,0 +1 @@ +reorderRender() ?> \ No newline at end of file diff --git a/plugins/rainlab/blog/controllers/categories/update.htm b/plugins/rainlab/blog/controllers/categories/update.htm new file mode 100644 index 00000000..7949c329 --- /dev/null +++ b/plugins/rainlab/blog/controllers/categories/update.htm @@ -0,0 +1,54 @@ + +
      +
    • +
    • pageTitle)) ?>
    • +
    + + +fatalError): ?> + + 'layout']) ?> + +
    + formRender() ?> +
    + +
    +
    + + + + + + + +
    +
    + + + +

    fatalError)) ?>

    +

    + diff --git a/plugins/rainlab/blog/controllers/posts/_list_toolbar.htm b/plugins/rainlab/blog/controllers/posts/_list_toolbar.htm new file mode 100644 index 00000000..d0f93f67 --- /dev/null +++ b/plugins/rainlab/blog/controllers/posts/_list_toolbar.htm @@ -0,0 +1,37 @@ +
    + + + + + + user->hasAnyAccess(['rainlab.blog.access_import_export'])): ?> + + +
    diff --git a/plugins/rainlab/blog/controllers/posts/_post_toolbar.htm b/plugins/rainlab/blog/controllers/posts/_post_toolbar.htm new file mode 100644 index 00000000..96951e4c --- /dev/null +++ b/plugins/rainlab/blog/controllers/posts/_post_toolbar.htm @@ -0,0 +1,56 @@ +formGetContext() == 'create'; + $pageUrl = isset($pageUrl) ? $pageUrl : null; +?> + diff --git a/plugins/rainlab/blog/controllers/posts/config_form.yaml b/plugins/rainlab/blog/controllers/posts/config_form.yaml new file mode 100644 index 00000000..10c090f0 --- /dev/null +++ b/plugins/rainlab/blog/controllers/posts/config_form.yaml @@ -0,0 +1,16 @@ +# =================================== +# Form Behavior Config +# =================================== + +name: rainlab.blog::lang.blog.create_post +form: $/rainlab/blog/models/post/fields.yaml +modelClass: RainLab\Blog\Models\Post +defaultRedirect: rainlab/blog/posts + +create: + redirect: rainlab/blog/posts/update/:id + redirectClose: rainlab/blog/posts + +update: + redirect: rainlab/blog/posts + redirectClose: rainlab/blog/posts diff --git a/plugins/rainlab/blog/controllers/posts/config_import_export.yaml b/plugins/rainlab/blog/controllers/posts/config_import_export.yaml new file mode 100644 index 00000000..e0cf8c5b --- /dev/null +++ b/plugins/rainlab/blog/controllers/posts/config_import_export.yaml @@ -0,0 +1,41 @@ +# =================================== +# Import/Export Behavior Config +# =================================== + +import: + # Page title + title: rainlab.blog::lang.posts.import_post + + # Import List Column configuration + list: $/rainlab/blog/models/postimport/columns.yaml + + # Import Form Field configuration + form: $/rainlab/blog/models/postimport/fields.yaml + + # Import Model class + modelClass: RainLab\Blog\Models\PostImport + + # Redirect when finished + redirect: rainlab/blog/posts + + # Required permissions + permissions: rainlab.blog.access_import_export + +export: + # Page title + title: rainlab.blog::lang.posts.export_post + + # Output file name + fileName: posts.csv + + # Export List Column configuration + list: $/rainlab/blog/models/postexport/columns.yaml + + # Export Model class + modelClass: RainLab\Blog\Models\PostExport + + # Redirect when finished + redirect: rainlab/blog/posts + + # Required permissions + permissions: rainlab.blog.access_import_export diff --git a/plugins/rainlab/blog/controllers/posts/config_list.yaml b/plugins/rainlab/blog/controllers/posts/config_list.yaml new file mode 100644 index 00000000..62c7d237 --- /dev/null +++ b/plugins/rainlab/blog/controllers/posts/config_list.yaml @@ -0,0 +1,47 @@ +# =================================== +# List Behavior Config +# =================================== + +# Model List Column configuration +list: $/rainlab/blog/models/post/columns.yaml + +# Filter widget configuration +filter: $/rainlab/blog/models/post/scopes.yaml + +# Model Class name +modelClass: RainLab\Blog\Models\Post + +# List Title +title: rainlab.blog::lang.posts.list_title + +# Link URL for each record +recordUrl: rainlab/blog/posts/update/:id + +# Message to display if the list is empty +noRecordsMessage: backend::lang.list.no_records + +# Records to display per page +recordsPerPage: 25 + +# Displays the list column set up button +showSetup: true + +# Displays the sorting link on each column +showSorting: true + +# Default sorting column +defaultSort: + column: published_at + direction: desc + +# Display checkboxes next to each record +showCheckboxes: true + +# Toolbar widget configuration +toolbar: + # Partial for toolbar buttons + buttons: list_toolbar + + # Search widget configuration + search: + prompt: backend::lang.list.search_prompt diff --git a/plugins/rainlab/blog/controllers/posts/create.htm b/plugins/rainlab/blog/controllers/posts/create.htm new file mode 100644 index 00000000..e6cd6a55 --- /dev/null +++ b/plugins/rainlab/blog/controllers/posts/create.htm @@ -0,0 +1,23 @@ +fatalError): ?> + +
    + 'layout', + 'data-change-monitor' => 'true', + 'data-window-close-confirm' => e(trans('rainlab.blog::lang.post.close_confirm')), + 'id' => 'post-form' + ]) ?> + formRender() ?> + + +
    + + +
    + +
    +
    +

    fatalError)) ?>

    +

    +
    + diff --git a/plugins/rainlab/blog/controllers/posts/export.htm b/plugins/rainlab/blog/controllers/posts/export.htm new file mode 100644 index 00000000..1023caf0 --- /dev/null +++ b/plugins/rainlab/blog/controllers/posts/export.htm @@ -0,0 +1,27 @@ + +
      +
    • +
    • pageTitle)) ?>
    • +
    + + + 'layout']) ?> + +
    + exportRender() ?> +
    + +
    +
    + +
    +
    + + diff --git a/plugins/rainlab/blog/controllers/posts/import.htm b/plugins/rainlab/blog/controllers/posts/import.htm new file mode 100644 index 00000000..e33d6daa --- /dev/null +++ b/plugins/rainlab/blog/controllers/posts/import.htm @@ -0,0 +1,25 @@ + +
      +
    • +
    • pageTitle)) ?>
    • +
    + + + 'layout']) ?> + +
    + importRender() ?> +
    + +
    + +
    + + diff --git a/plugins/rainlab/blog/controllers/posts/index.htm b/plugins/rainlab/blog/controllers/posts/index.htm new file mode 100644 index 00000000..ea43a363 --- /dev/null +++ b/plugins/rainlab/blog/controllers/posts/index.htm @@ -0,0 +1 @@ +listRender() ?> diff --git a/plugins/rainlab/blog/controllers/posts/update.htm b/plugins/rainlab/blog/controllers/posts/update.htm new file mode 100644 index 00000000..3f82c198 --- /dev/null +++ b/plugins/rainlab/blog/controllers/posts/update.htm @@ -0,0 +1,25 @@ +fatalError): ?> + +
    + 'layout', + 'data-change-monitor' => 'true', + 'data-window-close-confirm' => e(trans('rainlab.blog::lang.post.close_confirm')), + 'id' => 'post-form' + ]) ?> + formRender() ?> + + +
    + + + +
    + +
    +
    +

    fatalError)) ?>

    +

    +
    + + diff --git a/plugins/rainlab/blog/formwidgets/BlogMarkdown.php b/plugins/rainlab/blog/formwidgets/BlogMarkdown.php new file mode 100644 index 00000000..caff7189 --- /dev/null +++ b/plugins/rainlab/blog/formwidgets/BlogMarkdown.php @@ -0,0 +1,133 @@ +viewPath = base_path().'/modules/backend/formwidgets/markdowneditor/partials'; + + $this->checkUploadPostback(); + + parent::init(); + } + + /** + * {@inheritDoc} + */ + protected function loadAssets() + { + $this->assetPath = '/modules/backend/formwidgets/markdowneditor/assets'; + parent::loadAssets(); + } + + /** + * Disable HTML cleaning on the widget level since the PostModel will handle it + * + * @return boolean + */ + protected function shouldCleanHtml() + { + return false; + } + + /** + * {@inheritDoc} + */ + public function onRefresh() + { + $content = post($this->formField->getName()); + + $previewHtml = PostModel::formatHtml($content, true); + + return [ + 'preview' => $previewHtml + ]; + } + + /** + * Handle images being uploaded to the blog post + * + * @return void + */ + protected function checkUploadPostback() + { + if (!post('X_BLOG_IMAGE_UPLOAD')) { + return; + } + + $uploadedFileName = null; + + try { + $uploadedFile = Input::file('file'); + + if ($uploadedFile) + $uploadedFileName = $uploadedFile->getClientOriginalName(); + + $validationRules = ['max:'.File::getMaxFilesize()]; + $validationRules[] = 'mimes:jpg,jpeg,bmp,png,gif'; + + $validation = Validator::make( + ['file_data' => $uploadedFile], + ['file_data' => $validationRules] + ); + + if ($validation->fails()) { + throw new ValidationException($validation); + } + + if (!$uploadedFile->isValid()) { + throw new SystemException(Lang::get('cms::lang.asset.file_not_valid')); + } + + $fileRelation = $this->model->content_images(); + + $file = new File(); + $file->data = $uploadedFile; + $file->is_public = true; + $file->save(); + + $fileRelation->add($file, $this->sessionKey); + $result = [ + 'file' => $uploadedFileName, + 'path' => $file->getPath() + ]; + + $response = Response::make()->setContent($result); + $this->controller->setResponse($response); + + } catch (Exception $ex) { + $message = $uploadedFileName + ? Lang::get('cms::lang.asset.error_uploading_file', ['name' => $uploadedFileName, 'error' => $ex->getMessage()]) + : $ex->getMessage(); + + $result = [ + 'error' => $message, + 'file' => $uploadedFileName + ]; + + $response = Response::make()->setContent($result); + $this->controller->setResponse($response); + } + } +} diff --git a/plugins/rainlab/blog/formwidgets/MLBlogMarkdown.php b/plugins/rainlab/blog/formwidgets/MLBlogMarkdown.php new file mode 100644 index 00000000..5da6fa90 --- /dev/null +++ b/plugins/rainlab/blog/formwidgets/MLBlogMarkdown.php @@ -0,0 +1,137 @@ +initLocale(); + } + + /** + * {@inheritDoc} + */ + public function render() + { + $this->actAsParent(); + $parentContent = parent::render(); + $this->actAsParent(false); + + if (!$this->isAvailable) { + return $parentContent; + } + + $this->vars['markdowneditor'] = $parentContent; + + $this->actAsControl(true); + + return $this->makePartial('mlmarkdowneditor'); + } + + public function prepareVars() + { + parent::prepareVars(); + $this->prepareLocaleVars(); + } + + /** + * Returns an array of translated values for this field + * @param $value + * @return array + */ + public function getSaveValue($value) + { + $localeData = $this->getLocaleSaveData(); + + /* + * Set the translated values to the model + */ + if ($this->model->methodExists('setAttributeTranslated')) { + foreach ($localeData as $locale => $value) { + $this->model->setAttributeTranslated('content', $value, $locale); + + $this->model->setAttributeTranslated( + 'content_html', + Post::formatHtml($value), + $locale + ); + } + } + + return array_get($localeData, $this->defaultLocale->code, $value); + } + + /** + * {@inheritDoc} + */ + protected function loadAssets() + { + $this->actAsParent(); + parent::loadAssets(); + $this->actAsParent(false); + + if (Locale::isAvailable()) { + $this->loadLocaleAssets(); + + $this->actAsControl(true); + $this->addJs('js/mlmarkdowneditor.js'); + $this->actAsControl(false); + } + } + + protected function actAsParent($switch = true) + { + if ($switch) { + $this->originalAssetPath = $this->assetPath; + $this->originalViewPath = $this->viewPath; + $this->assetPath = '/modules/backend/formwidgets/markdowneditor/assets'; + $this->viewPath = base_path('/modules/backend/formwidgets/markdowneditor/partials'); + } + else { + $this->assetPath = $this->originalAssetPath; + $this->viewPath = $this->originalViewPath; + } + } + + protected function actAsControl($switch = true) + { + if ($switch) { + $this->originalAssetPath = $this->assetPath; + $this->originalViewPath = $this->viewPath; + $this->assetPath = '/plugins/rainlab/translate/formwidgets/mlmarkdowneditor/assets'; + $this->viewPath = base_path('/plugins/rainlab/translate/formwidgets/mlmarkdowneditor/partials'); + } + else { + $this->assetPath = $this->originalAssetPath; + $this->viewPath = $this->originalViewPath; + } + } +} diff --git a/plugins/rainlab/blog/lang/bg/lang.php b/plugins/rainlab/blog/lang/bg/lang.php new file mode 100644 index 00000000..6e5161df --- /dev/null +++ b/plugins/rainlab/blog/lang/bg/lang.php @@ -0,0 +1,100 @@ + [ + 'name' => 'Блог', + 'description' => 'Стабилната блог платформа.' + ], + 'blog' => [ + 'menu_label' => 'Блог', + 'menu_description' => 'управление на публикациите', + 'posts' => 'публикации', + 'create_post' => 'създай публикация', + 'categories' => 'категории', + 'create_category' => 'създай категория', + 'tab' => 'Блог', + 'access_posts' => 'управление на публикациите', + 'access_categories' => 'управление на категории', + 'access_other_posts' => 'управление на други потребители публикации в блога', + 'delete_confirm' => 'Сигурни ли сте?', + 'chart_published' => 'Публикувано', + 'chart_drafts' => 'Чернови', + 'chart_total' => 'Общо' + ], + 'posts' => [ + 'list_title' => 'Управление публикациите в блога', + 'filter_category' => 'Категория', + 'filter_published' => 'Скрий публикуваните', + 'new_post' => 'Нова публикация' + ], + 'post' => [ + 'title' => 'Заглавие', + 'title_placeholder' => 'Ново заглавие на публикацията', + 'slug' => 'Slug', + 'slug_placeholder' => 'нов slug на публикацията', + 'categories' => 'Категории', + 'created' => 'Създаден', + 'updated' => 'Обновен', + 'published' => 'Публикуван', + 'published_validation' => 'Моля, посочете дата на публикуване', + 'tab_edit' => 'Промяна', + 'tab_categories' => 'Категории', + 'categories_comment' => 'Изберете категории към който пренадлежи публикацията ', + 'categories_placeholder' => 'Няма категирии, Създайте първата?!', + 'tab_manage' => 'Управление', + 'published_on' => 'публикувано в', + 'excerpt' => 'Откъс', + 'featured_images' => 'Избрани снимки', + 'delete_confirm' => 'Наистина ли искате да изтриете тази публикация?', + 'close_confirm' => 'Публикацията не е запазена.', + 'return_to_posts' => 'Върни ме към всички публикации' + ], + 'categories' => [ + 'list_title' => 'Управление категориите в блога', + 'new_category' => 'Нова категория', + 'uncategorized' => 'Без категория' + ], + 'category' => [ + 'name' => 'Име', + 'name_placeholder' => 'Ново име на категорията', + 'slug' => 'Slug', + 'slug_placeholder' => 'нов slug на категотията', + 'posts' => 'публикации', + 'delete_confirm' => 'Наистина ли искате да изтриете тази категория?', + 'return_to_categories' => 'Върни ме към всички категории' + ], + 'settings' => [ + 'category_title' => 'Списък с категории', + 'category_description' => 'Показва списък с категориите на блога.', + 'category_slug' => 'категория slug', + 'category_slug_description' => "Look up the blog category using the supplied slug value. This property is used by the default component partial for marking the currently active category.", + 'category_display_empty' => 'Показване на празни категории', + 'category_display_empty_description' => 'Показване на категории, които нямат никакви публикации.', + 'category_page' => 'Страница на категория', + 'category_page_description' => 'Име на страницата за категирия. Това се използва подразбиране от компонента.', + 'post_title' => 'Публикация', + 'post_description' => 'Показване на Публикациите в блога на страницата.', + 'post_slug' => 'Post slug', + 'post_slug_description' => "Търсене на публикации по зададен slug.", + 'post_category' => 'Страница за Категория', + 'post_category_description' => 'Име на страница за категория за генериране на линк.Това се използва подразбиране от компонента.', + 'posts_title' => 'Лист с Публикации', + 'posts_description' => 'Показване на лист с публикации на страницата.', + 'posts_pagination' => 'Номер на страницата', + 'posts_pagination_description' => 'Тази стойност се използва за определяне на коя страница е потребителя.', + 'posts_filter' => 'Филтер Категория', + 'posts_filter_description' => 'Въведи slug на категория или URL адрес за филтриране по. Оставете празно за да се покажат всички публикации.', + 'posts_per_page' => 'Публикации на страница', + 'posts_per_page_validation' => 'Невалиден формат за публикации на страница', + 'posts_no_posts' => 'Няма публикации', + 'posts_no_posts_description' => 'Съобщение което да се покаже, в случай ,че няма публикации за показване.Това се използва подразбиране от компонента.', + 'posts_order' => 'подреждане на публикации', + 'posts_order_description' => 'Атрибут по който да бъдат подредени публикациите', + 'posts_category' => 'страница на категориите', + 'posts_category_description' => 'Име на страницата за категории , за "публикувано в". Това се използва подразбиране от компонента.', + 'posts_post' => 'Post page', + 'posts_post_description' => 'Име на страницата за публикации "Прочетете повече". Това се използва подразбиране от компонента.', + 'posts_except_post' => 'Except post', + 'posts_except_post_description' => 'Enter ID/URL or variable with post ID/URL you want to except', + ] +]; diff --git a/plugins/rainlab/blog/lang/cs/lang.php b/plugins/rainlab/blog/lang/cs/lang.php new file mode 100644 index 00000000..553d1252 --- /dev/null +++ b/plugins/rainlab/blog/lang/cs/lang.php @@ -0,0 +1,154 @@ + [ + 'name' => 'Blog', + 'description' => 'Robustní blogová platforma.' + ], + 'blog' => [ + 'menu_label' => 'Blog', + 'menu_description' => 'Správa blogových příspěvků', + 'posts' => 'Příspěvky', + 'create_post' => 'Příspěvek', + 'categories' => 'Kategorie', + 'create_category' => 'Kategorie příspěvků', + 'tab' => 'Blog', + 'access_posts' => 'Správa blogových příspěvků', + 'access_categories' => 'Správa blogových kategorií', + 'access_other_posts' => 'Správa příspěvků ostatních uživatelů', + 'access_import_export' => 'Možnost importu a exportu příspěvků', + 'access_publish' => 'Možnost publikovat příspěvky', + 'delete_confirm' => 'Jste si jistí?', + 'chart_published' => 'Publikované', + 'chart_drafts' => 'Návrhy', + 'chart_total' => 'Celkem', + ], + 'posts' => [ + 'list_title' => 'Správa blogových příspěvků', + 'filter_category' => 'Kategorie', + 'filter_published' => 'Schovat publikované', + 'filter_date' => 'Datum', + 'new_post' => 'Nový příspěvek', + 'export_post' => 'Export příspěvků', + 'import_post' => 'Import příspěvků', + ], + 'post' => [ + 'title' => 'Název', + 'title_placeholder' => 'Zadejte název', + 'content' => 'Obsah', + 'content_html' => 'HTML obsah', + 'slug' => 'URL příspěvku', + 'slug_placeholder' => 'zadejte-url-prispevku', + 'categories' => 'Kategorie', + 'author_email' => 'E-mail autora', + 'created' => 'Vytvořeno', + 'created_date' => 'Vytvořeno dne', + 'updated' => 'Upraveno', + 'updated_date' => 'Upraveno dne', + 'published' => 'Publikováno', + 'published_date' => 'Publikováno dne', + 'published_validation' => 'Zadejte prosím datum publikace příspěvku', + 'tab_edit' => 'Upravit', + 'tab_categories' => 'Kategorie', + 'categories_comment' => 'Vyberte kategorie do kterých příspěvek patří', + 'categories_placeholder' => 'Nejsou zde žádné kategorie, nejdříve musíte nějaké vytvořit!', + 'tab_manage' => 'Nastavení', + 'published_on' => 'Publikováno dne', + 'excerpt' => 'Perex příspěvku', + 'summary' => 'Shrnutí', + 'featured_images' => 'Obrázky', + 'delete_confirm' => 'Opravdu chcete smazat tento příspěvek?', + 'delete_success' => 'Vybrané příspěvky úspěšně odstraněny.', + 'close_confirm' => 'Příspěvek není uložený.', + 'return_to_posts' => 'Zpět na seznam příspěvků', + ], + 'categories' => [ + 'list_title' => 'Správa blogových kategorií', + 'new_category' => 'Nová kategorie', + 'uncategorized' => 'Nezařazeno', + ], + 'category' => [ + 'name' => 'Název', + 'name_placeholder' => 'Název nové kategorie', + 'description' => 'Popis', + 'slug' => 'URL kategorie', + 'slug_placeholder' => 'zadejte-url-kategorie', + 'posts' => 'Počet příspěvků', + 'delete_confirm' => 'Opravdu chcete smazat tuto kategorii?', + 'delete_success' => 'Vybrané kategorie úspěšně odstraněny.', + 'return_to_categories' => 'Zpět na seznam blogových kategorií', + 'reorder' => 'Změnit pořadí', + ], + 'menuitem' => [ + 'blog_category' => 'Blogová kategorie', + 'all_blog_categories' => 'Všechny blogové kategorie', + 'blog_post' => 'Blogový příspěvek', + 'all_blog_posts' => 'Všechny blogové příspěvky', + 'category_blog_posts' => 'Blog category posts' + ], + 'settings' => [ + 'category_title' => 'Seznam kategorií', + 'category_description' => 'Zobrazí na stránce seznam blogových kategorií.', + 'category_slug' => 'URL kategorie', + 'category_slug_description' => "Najde blogovou kategorii s tímto URL. Používá se pro zobrazení aktivní kategorie.", + 'category_display_empty' => 'Zobrazit prázdné kategorie', + 'category_display_empty_description' => 'Zobrazit kategorie bez blogových příspěvků.', + 'category_page' => 'Stránka kategorií', + 'category_page_description' => 'Vyberte stránku která slouží k zobrazení všech kategorií (nebo detailu kategorie).', + 'post_title' => 'Příspěvek', + 'post_description' => 'Zobrazí blogový příspěvek na stránce.', + 'post_slug' => 'URL příspěvku', + 'post_slug_description' => "Najde příspěvek dle zadané URL.", + 'post_category' => 'Stránka kategorie', + 'post_category_description' => 'Vyberte stránku která slouží k zobrazení všech kategorií (nebo detailu kategorie).', + 'posts_title' => 'Seznam příspěvků', + 'posts_description' => 'Zobrazí na stránce seznam posledních příspěvků na stránkách.', + 'posts_pagination' => 'Číslo stránky', + 'posts_pagination_description' => 'Číslo stránky určující na které stránce se uživatel nachází. Použito pro stránkování.', + 'posts_filter' => 'Filtr kategorií', + 'posts_filter_description' => 'Zadejte URL kategorie, nebo URL parametr pro filtrování příspěvků. Nechte prázdné pro zobrazení všech příspěvků.', + 'posts_per_page' => 'Příspěvků na stránku', + 'posts_per_page_validation' => 'Špatný formát počtu příspěvků na stránku, musí být zadáno jako číslo', + 'posts_no_posts' => 'Hláška prázdné stránky', + 'posts_no_posts_description' => 'Zpráva se zobrazí pokud se nepovede najít žádné články.', + 'posts_no_posts_default' => 'Nenalezeny žádné příspěvky', + 'posts_order' => 'Řazení článků', + 'posts_order_decription' => 'Nastaví řazení článků ve výpisu', + 'posts_category' => 'Stránka kategorií', + 'posts_category_description' => 'Vyberte stránku která slouží k zobrazení všech kategorií (nebo detailu kategorie).', + 'posts_post' => 'Stránka příspěvků', + 'posts_post_description' => 'Vyberte stránku která slouží k zobrazení článků (nebo detailu článku).', + 'posts_except_post' => 'Vyloučit příspěvěk', + 'posts_except_post_description' => 'Zadejte ID nebo URL příspěvku který chcete vyloučit', + 'posts_except_categories' => 'Vyloučené kategorie', + 'posts_except_categories_description' => 'Pro vyloučení kategorií zadejte čárkou oddělené URL příspěvků nebo proměnnou, která tento seznam obsahuje.', + 'rssfeed_blog' => 'Blogová stránka', + 'rssfeed_blog_description' => 'Name of the main blog page file for generating links. This property is used by the default component partial.', + 'rssfeed_title' => 'RSS Kanál', + 'rssfeed_description' => 'Vygeneruje RSS kanál který obsahuje blogové příspěvky.', + 'group_links' => 'Odkazy', + 'group_exceptions' => 'Výjimky' + ], + 'sorting' => [ + 'title_asc' => 'Název (sestupně)', + 'title_desc' => 'Název (vzestupně)', + 'created_asc' => 'Vytvořeno (sestupně)', + 'created_desc' => 'Vytvořeno (vzestupně)', + 'updated_asc' => 'Upraveno (sestupně)', + 'updated_desc' => 'Upraveno (vzestupně)', + 'published_asc' => 'Publikováno (sestupně)', + 'published_desc' => 'Publikováno (vzestupně)', + 'random' => 'Náhodně' + ], + 'import' => [ + 'update_existing_label' => 'Uprav existující příspěvky', + 'update_existing_comment' => 'Zvolte pokud chcete upravit příspěvky se stejným ID, názvem nebo URL.', + 'auto_create_categories_label' => 'VYtvořit kategorie ze souboru', + 'auto_create_categories_comment' => 'Chcete-li tuto funkci použít, měli byste se shodovat se sloupcem Kategorie, jinak vyberte výchozí kategorie, které chcete použít z níže uvedených položek.', + 'categories_label' => 'Kategorie', + 'categories_comment' => 'Vyberte kategorie ke kterým budou příspěvky přiřazeny (volitelné).', + 'default_author_label' => 'Výchozí autor příspěvků (volitelné)', + 'default_author_comment' => 'Import se pokusí použít existujícího autora, pokud odpovídá sloupci email, jinak se použije výše uvedený autor.', + 'default_author_placeholder' => '-- vyberte autora --' + ] +]; diff --git a/plugins/rainlab/blog/lang/de/lang.php b/plugins/rainlab/blog/lang/de/lang.php new file mode 100644 index 00000000..7e78057a --- /dev/null +++ b/plugins/rainlab/blog/lang/de/lang.php @@ -0,0 +1,132 @@ + [ + 'name' => 'Blog', + 'description' => 'Eine robuste Blog Plattform.' + ], + 'blog' => [ + 'menu_label' => 'Blog', + 'menu_description' => 'Blog Artikel bearbeiten', + 'posts' => 'Artikel', + 'create_post' => 'Blog Artikel', + 'categories' => 'Kategorien', + 'create_category' => 'Blog Kategorie', + 'tab' => 'Blog', + 'access_posts' => 'Blog Artikel verwalten', + 'access_categories' => 'Blog Kategorien verwalten', + 'access_other_posts' => 'Blog Artikel anderer Benutzer verwalten', + 'access_import_export' => 'Blog Artikel importieren oder exportieren', + 'access_publish' => 'Kann Artikel veröffentlichen', + 'delete_confirm' => 'Bist du sicher?', + 'chart_published' => 'Veröffentlicht', + 'chart_drafts' => 'Entwurf', + 'chart_total' => 'Gesamt' + ], + 'posts' => [ + 'list_title' => 'Blog Artikel verwalten', + 'filter_category' => 'Kategorie', + 'filter_published' => 'Veröffentlichte ausblenden', + 'filter_date' => 'Date', + 'new_post' => 'Neuer Artikel', + 'export_post' => 'Exportiere Artikel', + 'import_post' => 'Importiere Artikel' + ], + 'post' => [ + 'title' => 'Titel', + 'title_placeholder' => 'Neuer Titel', + 'content' => 'Inhalt', + 'content_html' => 'HTML-Inhalt', + 'slug' => 'Slug', + 'slug_placeholder' => 'neuer-artikel-slug', + 'categories' => 'Kategorien', + 'author_email' => 'Autor E-Mail', + 'created' => 'Erstellt', + 'created_date' => 'Erstellzeitpunkt', + 'updated' => 'Aktualisiert', + 'updated_date' => 'Aktualisierungszeitpunk', + 'published' => 'Veröffentlicht', + 'published_date' => 'Veröffentlichungszeitpunkt', + 'published_validation' => 'Bitte gebe das Datum der Veröffentlichung an', + 'tab_edit' => 'Bearbeiten', + 'tab_categories' => 'Kategorien', + 'categories_comment' => 'Wähle die zugehörigen Kategorien', + 'categories_placeholder' => 'Es existieren keine Kategorien. Bitte lege zuerst Kategorien an!', + 'tab_manage' => 'Verwalten', + 'published_on' => 'Veröffentlicht am', + 'excerpt' => 'Textauszug', + 'summary' => 'Zusammenfassung', + 'featured_images' => 'Zugehörige Bilder', + 'delete_confirm' => 'Möchtest du diesen Artikel wirklich löschen?', + 'close_confirm' => 'Der Artikel ist noch nicht gespeichert.', + 'return_to_posts' => 'Zurück zur Artikel-Übersicht', + 'posted_byline' => 'Veröffentlicht in :categories am :date', + 'posted_byline_no_categories' => 'Veröffentlicht am :date', + 'date_format' => 'd. F Y', + ], + 'categories' => [ + 'list_title' => 'Blog Kategorien verwalten', + 'new_category' => 'Neue Kategorie', + 'uncategorized' => 'Allgemein' + ], + 'category' => [ + 'name' => 'Name', + 'name_placeholder' => 'Neuer Kategorie Name', + 'description' => 'Beschreibung', + 'slug' => 'Slug', + 'slug_placeholder' => 'neuer-kategorie-slug', + 'posts' => 'Artikel', + 'delete_confirm' => 'Möchtest du die Kategorie wirklich löschen?', + 'return_to_categories' => 'Zurück zur Kategorie-Übersicht.', + 'reorder' => 'Kategorien sortieren' + ], + 'menuitem' => [ + 'blog_category' => 'Blog Kategorie', + 'all_blog_categories' => 'Alle Blog Kategorien', + 'blog_post' => 'Blog Artikel', + 'all_blog_posts' => 'Alle Blog Artikel', + 'category_blog_posts' => 'Blog Kategorie Artikel' + ], + 'settings' => [ + 'category_title' => 'Blog Kategorie-Übersicht', + 'category_description' => 'Zeigt eine Blog Kategorien-Übersicht.', + 'category_slug' => 'Slug Parametername', + 'category_slug_description' => 'Der URL-Routen-Parameter welcher verwendet wird um die aktuelle Kategorie zu bestimmen. Wird von der Standard-Komponente benötigt um die aktive Kategorie zu markieren.', + 'category_display_empty' => 'Leere Kategorien anzeigen', + 'category_display_empty_description' => 'Kategorien zeigen welche keine Artikel besitzen.', + 'category_page' => 'Kategorien Seite', + 'category_page_description' => 'Name der Kategorien-Seiten-Datei für die Kategorien Links. Wird von der Standard-Komponente benötigt.', + 'post_title' => 'Blog Artikel', + 'post_description' => 'Zeigt einen Blog Artikel auf der Seite.', + 'post_slug' => 'Slug Parametername', + 'post_slug_description' => 'Der URL-Routen-Parameter um den Post mittels "Slug" zu bestimmen.', + 'post_category' => 'Kategorien-Seite', + 'post_category_description' => 'Name der Kategorien-Seiten-Datei für Kategorie-Links.', + 'posts_title' => 'Blog Artikel-Übersicht', + 'posts_description' => 'Stellt eine Liste der neuesten Artikel auf der Seite dar.', + 'posts_pagination' => 'Blättern Parametername', + 'posts_pagination_description' => 'Der erwartete Parametername welcher für Seiten verwendet wird.', + 'posts_filter' => 'Kategorien-Filter', + 'posts_filter_description' => 'Bitte gebe ein Kategorien-Slug oder URL-Parameter an, mittels den die Artikel gefiltert werden. Wenn der Wert leer ist, werden alle Artikel angezeigt.', + 'posts_per_page' => 'Artikel pro Seite', + 'posts_per_page_validation' => 'Ungültiger "Artikel pro Seiten" Wert', + 'posts_no_posts' => 'Keine Artikel Nachricht', + 'posts_no_posts_description' => 'Nachricht welche dargestellt wird wenn keine Artikel vorhanden sind. Dieser Wert wird von der Standard-Komponente verwendet.', + 'posts_order' => 'Artikel Sortierung', + 'posts_order_description' => 'Attribute nach welchem Artikel sortiert werden.', + 'posts_category' => 'Kategorien-Seite', + 'posts_category_description' => 'Name der Kategorien-Seiten-Datei für "Veröffentlicht in" Kategorien-Links. Dieser Wert von der Standard-Komponente verwendet.', + 'posts_post' => 'Artikel Seite', + 'posts_post_description' => 'Name der Artikel-Seiten-Datei für die "Erfahre mehr" Links. Dieser Wert für von der Standard-Komponente verwendet.', + 'posts_except_post' => 'Artikel ausschließen', + 'posts_except_post_description' => 'Gebe direkt die ID/URL oder eine Variable mit der Artikel-ID/URL an um diesen Artikel auszuschließen. Dieser Wert für von der Standard-Komponente verwendet.', + 'posts_except_categories' => 'Kategorien ausschließen', + 'posts_except_categories_description' => 'Gebe eine kommagetrennte Liste von Kategorie-Slugs oder eine Variable mit einer solchen Liste an um deren Artikel auszuschließen. Die Dieser Wert für von der Standard-Komponente verwendet.', + 'rssfeed_blog' => 'Blog Seite', + 'rssfeed_blog_description' => 'Name der Artikel-Seiten-Datei für die Links. Dieser Wert für von der Standard-Komponente verwendet.', + 'rssfeed_title' => 'RSS-Feed', + 'rssfeed_description' => 'Erstellt einen RSS-Feed mit Artikeln aus dem Blog.', + 'group_links' => 'Links', + 'group_exceptions' => 'Ausnahmen' + ] +]; diff --git a/plugins/rainlab/blog/lang/en/lang.php b/plugins/rainlab/blog/lang/en/lang.php new file mode 100644 index 00000000..06e7e471 --- /dev/null +++ b/plugins/rainlab/blog/lang/en/lang.php @@ -0,0 +1,169 @@ + [ + 'name' => 'Blog', + 'description' => 'A robust blogging platform.' + ], + 'blog' => [ + 'menu_label' => 'Blog', + 'menu_description' => 'Manage Blog Posts', + 'posts' => 'Posts', + 'create_post' => 'Blog post', + 'categories' => 'Categories', + 'create_category' => 'Blog category', + 'tab' => 'Blog', + 'access_posts' => 'Manage the blog posts', + 'access_categories' => 'Manage the blog categories', + 'access_other_posts' => 'Manage other users blog posts', + 'access_import_export' => 'Allowed to import and export posts', + 'access_publish' => 'Allowed to publish posts', + 'manage_settings' => 'Manage blog settings', + 'delete_confirm' => 'Are you sure?', + 'chart_published' => 'Published', + 'chart_drafts' => 'Drafts', + 'chart_total' => 'Total', + 'settings_description' => 'Manage blog settings', + 'show_all_posts_label' => 'Show All Posts to Backend Users', + 'show_all_posts_comment' => 'Display both published and unpublished posts on the frontend to backend users', + 'use_legacy_editor_label' => 'Use the Legacy Markdown Editor', + 'use_legacy_editor_comment' => 'Enable the older version of the markdown editor when using October CMS v2 and above', + 'tab_general' => 'General', + 'preview' => 'Preview' + ], + 'posts' => [ + 'list_title' => 'Manage the blog posts', + 'filter_category' => 'Category', + 'filter_published' => 'Published', + 'filter_date' => 'Date', + 'new_post' => 'New Post', + 'export_post' => 'Export Posts', + 'import_post' => 'Import Posts' + ], + 'post' => [ + 'title' => 'Title', + 'title_placeholder' => 'New post title', + 'content' => 'Content', + 'content_html' => 'HTML Content', + 'slug' => 'Slug', + 'slug_placeholder' => 'new-post-slug', + 'categories' => 'Categories', + 'author_email' => 'Author Email', + 'created' => 'Created', + 'created_date' => 'Created date', + 'updated' => 'Updated', + 'updated_date' => 'Updated date', + 'published' => 'Published', + 'published_by' => 'Published by', + 'current_user' => 'Current user', + 'published_date' => 'Published date', + 'published_validation' => 'Please specify the published date', + 'tab_edit' => 'Edit', + 'tab_categories' => 'Categories', + 'categories_comment' => 'Select categories the blog post belongs to', + 'categories_placeholder' => 'There are no categories, you should create one first!', + 'tab_manage' => 'Manage', + 'published_on' => 'Published on', + 'excerpt' => 'Excerpt', + 'summary' => 'Summary', + 'featured_images' => 'Featured Images', + 'delete_confirm' => 'Delete this post?', + 'delete_success' => 'Successfully deleted those posts.', + 'close_confirm' => 'The post is not saved.', + 'return_to_posts' => 'Return to posts list', + 'posted_byline' => 'Posted in :categories on :date.', + 'posted_byline_no_categories' => 'Posted on :date.', + 'date_format' => 'M d, Y', + ], + 'categories' => [ + 'list_title' => 'Manage the blog categories', + 'new_category' => 'New Category', + 'uncategorized' => 'Uncategorized' + ], + 'category' => [ + 'name' => 'Name', + 'name_placeholder' => 'New category name', + 'description' => 'Description', + 'slug' => 'Slug', + 'slug_placeholder' => 'new-category-slug', + 'posts' => 'Posts', + 'delete_confirm' => 'Delete this category?', + 'delete_success' => 'Successfully deleted those categories.', + 'return_to_categories' => 'Return to the blog category list', + 'reorder' => 'Reorder Categories' + ], + 'menuitem' => [ + 'blog_category' => 'Blog category', + 'all_blog_categories' => 'All blog categories', + 'blog_post' => 'Blog post', + 'all_blog_posts' => 'All blog posts', + 'category_blog_posts' => 'Blog category posts' + ], + 'settings' => [ + 'category_title' => 'Category List', + 'category_description' => 'Displays a list of blog categories on the page.', + 'category_slug' => 'Category slug', + 'category_slug_description' => "Look up the blog category using the supplied slug value. This property is used by the default component partial for marking the currently active category.", + 'category_display_empty' => 'Display empty categories', + 'category_display_empty_description' => 'Show categories that do not have any posts.', + 'category_page' => 'Category page', + 'category_page_description' => 'Name of the category page file for the category links. This property is used by the default component partial.', + 'post_title' => 'Post', + 'post_description' => 'Displays a blog post on the page.', + 'post_slug' => 'Post slug', + 'post_slug_description' => "Look up the blog post using the supplied slug value.", + 'post_category' => 'Category page', + 'post_category_description' => 'Name of the category page file for the category links. This property is used by the default component partial.', + 'posts_title' => 'Post List', + 'posts_description' => 'Displays a list of latest blog posts on the page.', + 'posts_pagination' => 'Page number', + 'posts_pagination_description' => 'This value is used to determine what page the user is on.', + 'posts_filter' => 'Category filter', + 'posts_filter_description' => 'Enter a category slug or URL parameter to filter the posts by. Leave empty to show all posts.', + 'posts_per_page' => 'Posts per page', + 'posts_per_page_validation' => 'Invalid format of the posts per page value', + 'posts_no_posts' => 'No posts message', + 'posts_no_posts_description' => 'Message to display in the blog post list in case if there are no posts. This property is used by the default component partial.', + 'posts_no_posts_default' => 'No posts found', + 'posts_order' => 'Post order', + 'posts_order_description' => 'Attribute on which the posts should be ordered', + 'posts_category' => 'Category page', + 'posts_category_description' => 'Name of the category page file for the "Posted into" category links. This property is used by the default component partial.', + 'posts_post' => 'Post page', + 'posts_post_description' => 'Name of the blog post page file for the "Learn more" links. This property is used by the default component partial.', + 'posts_except_post' => 'Except post', + 'posts_except_post_description' => 'Enter ID/URL or variable with post ID/URL you want to exclude. You may use a comma-separated list to specify multiple posts.', + 'posts_except_post_validation' => 'Post exceptions must be a single slug or ID, or a comma-separated list of slugs and IDs', + 'posts_except_categories' => 'Except categories', + 'posts_except_categories_description' => 'Enter a comma-separated list of category slugs or variable with such a list of categories you want to exclude', + 'posts_except_categories_validation' => 'Category exceptions must be a single category slug, or a comma-separated list of slugs', + 'rssfeed_blog' => 'Blog page', + 'rssfeed_blog_description' => 'Name of the main blog page file for generating links. This property is used by the default component partial.', + 'rssfeed_title' => 'RSS Feed', + 'rssfeed_description' => 'Generates an RSS feed containing posts from the blog.', + 'group_links' => 'Links', + 'group_exceptions' => 'Exceptions' + ], + 'sorting' => [ + 'title_asc' => 'Title (ascending)', + 'title_desc' => 'Title (descending)', + 'created_asc' => 'Created (ascending)', + 'created_desc' => 'Created (descending)', + 'updated_asc' => 'Updated (ascending)', + 'updated_desc' => 'Updated (descending)', + 'published_asc' => 'Published (ascending)', + 'published_desc' => 'Published (descending)', + 'random' => 'Random' + ], + 'import' => [ + 'update_existing_label' => 'Update existing posts', + 'update_existing_comment' => 'Check this box to update posts that have exactly the same ID, title or slug.', + 'auto_create_categories_label' => 'Create categories specified in the import file', + 'auto_create_categories_comment' => 'You should match the Categories column to use this feature, otherwise select the default categories to use from the items below.', + 'categories_label' => 'Categories', + 'categories_comment' => 'Select the categories that imported posts will belong to (optional).', + 'default_author_label' => 'Default post author (optional)', + 'default_author_comment' => 'The import will try to use an existing author if you match the Author Email column, otherwise the author specified above is used.', + 'default_author_placeholder' => '-- select author --' + ] +]; diff --git a/plugins/rainlab/blog/lang/es/lang.php b/plugins/rainlab/blog/lang/es/lang.php new file mode 100644 index 00000000..d34e8e4c --- /dev/null +++ b/plugins/rainlab/blog/lang/es/lang.php @@ -0,0 +1,166 @@ + [ + 'name' => 'Blog', + 'description' => 'Una plataforma robusta de blogging.' + ], + 'blog' => [ + 'menu_label' => 'Blog', + 'menu_description' => 'Administrar Publicaciones', + 'posts' => 'Publicaciones', + 'create_post' => 'Crear publicación', + 'categories' => 'Categorías', + 'create_category' => 'Categoría', + 'tab' => 'Blog', + 'access_posts' => 'Administrar las publicaciones', + 'access_categories' => 'Administrar las categorías', + 'access_other_posts' => 'Administrar publicaciones de otros usuarios', + 'access_import_export' => 'Autorizado para importar y exportar publicaciones', + 'access_publish' => 'Autorizado para publicar publicaciones', + 'manage_settings' => 'Administrar configuración del blog', + 'delete_confirm' => '¿Está seguro?', + 'chart_published' => 'Publicado', + 'chart_drafts' => 'Borradores', + 'chart_total' => 'Total', + 'settings_description' => 'Administrar configuración del blog', + 'show_all_posts_label' => 'Mostrar todas las publicaciones a los usuarios de backend', + 'show_all_posts_comment' => 'Mostrar las publicaciones publicados y los borradores a los usuarios de backend', + 'tab_general' => 'General' + ], + 'posts' => [ + 'list_title' => 'Administrar publicaciones', + 'filter_category' => 'Categoría', + 'filter_published' => 'Publicado', + 'filter_date' => 'Fecha', + 'new_post' => 'Nueva publicación', + 'export_post' => 'Exportar publicaciones', + 'import_post' => 'Importar publicaciones' + ], + 'post' => [ + 'title' => 'Título', + 'title_placeholder' => 'Título de la publicación', + 'content' => 'Contenido', + 'content_html' => 'Contenido HTML', + 'slug' => 'Identificador', + 'slug_placeholder' => 'nueva-publicacion', + 'categories' => 'Categorías', + 'author_email' => 'Email del Autor', + 'created' => 'Creado', + 'created_date' => 'Fecha de Creación', + 'updated' => 'Actualizado', + 'updated_date' => 'Fecha de Actualización', + 'published' => 'Publicado', + 'published_by' => 'Publicado por', + 'current_user' => 'Usuario actual', + 'published_date' => 'Fecha de publicación', + 'published_validation' => 'Por favor, especifique la fecha de publicación', + 'tab_edit' => 'Editar', + 'tab_categories' => 'Categorías', + 'categories_comment' => 'Seleccione las categorías para la publicación', + 'categories_placeholder' => 'No hay categorías, ¡crea una primero!', + 'tab_manage' => 'Administrar', + 'published_on' => 'Publicado el', + 'excerpt' => 'Resumen', + 'summary' => 'Resumen', + 'featured_images' => 'Imágenes Destacadas', + 'delete_confirm' => '¿Borrar la publicación?', + 'delete_success' => 'Publicación borrada correctamente', + 'close_confirm' => 'La publicación no está guardada.', + 'return_to_posts' => 'Volver a la lista de publicaciones', + 'posted_byline' => 'Publicado en :categories el :date.', + 'posted_byline_no_categories' => 'Publicado el :date.', + 'date_format' => 'd de M de Y', + ], + 'categories' => [ + 'list_title' => 'Administrar las categorías', + 'new_category' => 'Nueva categoría', + 'uncategorized' => 'Sin Categoría' + ], + 'category' => [ + 'name' => 'Nombre', + 'name_placeholder' => 'Nombre de la categoría', + 'description' => 'Descripción', + 'slug' => 'Identificador', + 'slug_placeholder' => 'nueva-categoría', + 'posts' => 'Publicaciones', + 'delete_confirm' => '¿Borrar esta categoría?', + 'delete_success' => 'Categorías borradas correctamente.', + 'return_to_categories' => 'Volver a la lista de categorías', + 'reorder' => 'Re-ordenar Categorías' + ], + 'menuitem' => [ + 'blog_category' => 'Categoría del blog', + 'all_blog_categories' => 'Todas las categorías del blog', + 'blog_post' => 'Publicación del blog', + 'all_blog_posts' => 'Todas las publicaciones del blog', + 'category_blog_posts' => 'Publicaciones del blog por categorías' + ], + 'settings' => [ + 'category_title' => 'Lista de Categorías', + 'category_description' => 'Muestra en la página una lista de las categorías.', + 'category_slug' => 'Identificador de la categoría', + 'category_slug_description' => "Localiza una categoría utilizando el identificador proporcionado. Esta propiedad es utilizada dentro del parcial que viene por defecto en el componente para marcar la categoría activa.", + 'category_display_empty' => 'Mostrar categorías vacías', + 'category_display_empty_description' => 'Mostrar categorías que no tienen ninguna publicación.', + 'category_page' => 'Página de categorías', + 'category_page_description' => 'Nombre del archivo de página utilizado para los enlaces de categorías. Esta propiedad es utilizada dentro del parcial que viene por defecto en el componente.', + 'post_title' => 'Publicación', + 'post_description' => 'Muestra una publicación en la página.', + 'post_slug' => 'Identificador de la publicación', + 'post_slug_description' => "Se buscará la publicación utilizando el valor del identificador proporcionado.", + 'post_category' => 'Página de categoría', + 'post_category_description' => 'Nombre del archivo de página utilizado para los enlaces de categorías. Esta propiedad es utilizada dentro del parcial que viene por defecto en el componente.', + 'posts_title' => 'Lista de publicaciones', + 'posts_description' => 'Muestra una lista de las últimas publicaciones en la página.', + 'posts_pagination' => 'Número de página', + 'posts_pagination_description' => 'Este valor se utiliza para determinar en que página se encuentra el usuario.', + 'posts_filter' => 'Filtro de categoría', + 'posts_filter_description' => 'Ingrese un identificador de categoría o parámetro URL. Se utilizará para filtrar las publicaciones. Deje el campo vacío para mostrar todas las publicaciones.', + 'posts_per_page' => 'Publicaciones por página', + 'posts_per_page_validation' => 'Formato inválido para el valor de publicaciones por página', + 'posts_no_posts' => 'Mensaje cuando no hay publicaciones', + 'posts_no_posts_description' => 'Mensaje que se mostrará en la lista de publicaciones del blog cuando no haya ningúno. Esta propiedad es utilizada dentro del parcial que viene por defecto en el componente.', + 'posts_no_posts_default' => 'No se encontraron publicaciones.', + 'posts_order' => 'Ordenar publicaciones por', + 'posts_order_description' => 'Atributo mediante el cual se deberán ordenar las publicaciones', + 'posts_category' => 'Página de Categoría', + 'posts_category_description' => 'Nombre del archivo de página utilizado para los enlaces de categoría "Publicado en". Esta propiedad es utilizada dentro del parcial que viene por defecto en el componente.', + 'posts_post' => 'Página de las publicaciones', + 'posts_post_description' => 'Nombre del archivo de página utilizado para los enlaces "Saber más". Esta propiedad es utilizada dentro del parcial que viene por defecto en el componente.', + 'posts_except_post' => 'Exceptuar publicación', + 'posts_except_post_description' => 'Ingrese una ID/URL o variable que contenga una ID/URL de la publicación que se quiera excluir', + 'posts_except_post_validation' => 'La publicación a excluir debe ser una ID/URL, o una lista separada por comas de IDs/URLs', + 'posts_except_categories' => 'Excluir categorías', + 'posts_except_categories_description' => 'Introduce una lista separada por comas de IDs/URLs de categorías con las categorías a excluir.', + 'posts_except_categories_validation' => 'Las categorías excluidas deben ser una URL de categoría o una lista separada por comas', + 'rssfeed_blog' => 'Página del blog', + 'rssfeed_blog_description' => 'Nombre del archivo de página principal para generación de enlaces. Esta propiedad es utilizada dentro del parcial que viene por defecto en el componente.', + 'rssfeed_title' => 'RSS Feed', + 'rssfeed_description' => 'Genera un feed de RSS con las publicaciones del blog.', + 'group_links' => 'Enlaces', + 'group_exceptions' => 'Excepciones' + ], + 'sorting' => [ + 'title_asc' => 'Título (ascendiente)', + 'title_desc' => 'Título (descendiente)', + 'created_asc' => 'Creado (ascendiente)', + 'created_desc' => 'Creado (descendiente)', + 'updated_asc' => 'Editado (ascendiente)', + 'updated_desc' => 'Editado (descendiente)', + 'published_asc' => 'Publicado (ascendiente)', + 'published_desc' => 'Publicado (descendiente)', + 'random' => 'Aleatorio' + ], + 'import' => [ + 'update_existing_label' => 'Editar publicaciones existentes', + 'update_existing_comment' => 'Selecciona este check para actualizar las publicaciones con exactamente la misma ID, título o URL.', + 'auto_create_categories_label' => 'Crear categorías especificadas en el archivo a importar', + 'auto_create_categories_comment' => 'Debes hacer coincidir la columna Categoría para usar esta funcionalidad, sino selecciona la categoría por defecto para para usar para los elementos de abajo.', + 'categories_label' => 'Categorías', + 'categories_comment' => 'Selecciona las categorías a las que pertenecerán las publicaciones importadas (opcional).', + 'default_author_label' => 'Autor de publicación por defecto (opcional)', + 'default_author_comment' => 'La importación intentará usar un autor existente si coicide con la columna "Author Email", sino se usará el autor especificado arriba.', + 'default_author_placeholder' => '-- Selecciona Autor/a --' + ] +]; diff --git a/plugins/rainlab/blog/lang/fa/lang.php b/plugins/rainlab/blog/lang/fa/lang.php new file mode 100644 index 00000000..04134519 --- /dev/null +++ b/plugins/rainlab/blog/lang/fa/lang.php @@ -0,0 +1,109 @@ + [ + 'name' => 'وبلاگ', + 'description' => 'پلتفرم قوی برای وبلاگ نویسی' + ], + 'blog' => [ + 'menu_label' => 'وبلاگ', + 'menu_description' => 'مدیریت پست های ارسالی', + 'posts' => 'پست ها', + 'create_post' => 'ایجاد پست جدید', + 'categories' => 'دسته بندی ها', + 'create_category' => 'ایجاد دسته بندی جدید', + 'tab' => 'وبلاگ', + 'access_posts' => 'مدیریت پست های ارسالی', + 'access_categories' => 'مدیریت دسته بندی های وبلاگ', + 'access_other_posts' => 'مدیریت پست های ارسالی سایر کاربران', + 'access_import_export' => 'توانایی واردکردن و خارج کردن پستها', + 'delete_confirm' => 'آیا اطمینان دارید؟', + 'chart_published' => 'منتشر شده', + 'chart_drafts' => 'پیش نویس', + 'chart_total' => 'مجموع' + ], + 'posts' => [ + 'list_title' => 'مدیریت پست های ارسالی', + 'filter_category' => 'دسته بندی', + 'filter_published' => 'مخفی کردن منتشر شده ها', + 'new_post' => 'پست جدید' + ], + 'post' => [ + 'title' => 'عنوان', + 'title_placeholder' => 'عنوان پست جدید', + 'content' => 'محتوی', + 'content_html' => 'محتوی HTML', + 'slug' => 'آدرس', + 'slug_placeholder' => 'آدرس-پست-جدید', + 'categories' => 'دسته بندی ها', + 'author_email' => 'پست الکترونیکی نویسنده', + 'created' => 'ایجاد شده در', + 'created_date' => 'تاریخ ایجاد', + 'updated' => 'به روزرسانی شده در', + 'updated_date' => 'تاریخ به روزرسانی', + 'published' => 'منتشر شده', + 'published_date' => 'تاریخ انتشار', + 'published_validation' => 'لطفا تاریخ انتشار را وارد نمایید', + 'tab_edit' => 'ویرایش', + 'tab_categories' => 'دسته بندی ها', + 'categories_comment' => 'دسته بندی هایی را که پست به آنها تعلق دارد را انتخاب نمایید', + 'categories_placeholder' => 'دسته بندی ای وجود ندارد. ابتدا یک دسته بندی ایجاد نمایید!', + 'tab_manage' => 'مدیریت', + 'published_on' => 'منتشر شده در', + 'excerpt' => 'خلاصه', + 'summary' => 'چکیده', + 'featured_images' => 'تصاویر شاخص', + 'delete_confirm' => 'آیا از حذف این پست اطمینان دارید؟', + 'close_confirm' => 'پست ذخیره نشده است', + 'return_to_posts' => 'بازگشت به لیست پست ها' + ], + 'categories' => [ + 'list_title' => 'مدیریت دسته بندی های وبلاگ', + 'new_category' => 'دسته بندی جدید', + 'uncategorized' => 'بدون دسته بندی' + ], + 'category' => [ + 'name' => 'نام', + 'name_placeholder' => 'نام دسته بندی جدید', + 'slug' => 'آدرس', + 'slug_placeholder' => 'آدرس-جدید-دسته-بندی', + 'posts' => 'پست ها', + 'delete_confirm' => 'آیا از حذف این دسته بندی اطمینان دارید؟', + 'return_to_categories' => 'بازگشت به لیست دسته بندی های وبلاگ', + 'reorder' => 'مرتب سازی دسته بندی ها' + ], + 'settings' => [ + 'category_title' => 'لیست دسته بندی', + 'category_description' => 'نمایش لیست دسته بندی های وبلاگ در صفحه', + 'category_slug' => 'آدرس دسته بندی', + 'category_slug_description' => "دسته بندی وبلاگ توسط آدرس وارد شده جستجو می شود. این عمل توسط ابزار دسته بندی برای برجسته ساختن دسته بندی در حال نمایش استفاده می شود.", + 'category_display_empty' => 'نمایش دسته بندی های خالی', + 'category_display_empty_description' => 'نمایش دسته بندی هایی که هیچ ارسالی در آنها وجود ندارد.', + 'category_page' => 'صفحه دسته بندی', + 'category_page_description' => 'نام صفحه ای که لیست دسته بندی ها در آن نمایش داده می شوند. این گزینه به طور پیشفرض توسط ابزار مورد استفاده قرار میگیرد.', + 'post_title' => 'پست', + 'post_description' => 'نمایش پست در صفحه', + 'post_slug' => 'آدرس پست', + 'post_slug_description' => "پست توسط آدرس وارد شده جستجو میشود.", + 'post_category' => 'صفحه دسته بندی', + 'post_category_description' => 'نام صفحه ای که لیست دسته بندی ها در آن نمایش داده می شوند. این گزینه به طور پیشفرض توسط ابزار مورد استفاده قرار میگیرد.', + 'posts_title' => 'لیست پست ها', + 'posts_description' => 'نمایش لیستی از پستهایی که اخیرا ارسال شده اند در صفحه.', + 'posts_pagination' => 'شماره صفحه', + 'posts_pagination_description' => 'این مقدار جهت تشخیص صفحه ای که کاربر در آن قرار دارد مورد استفاده قرار میگیرد.', + 'posts_filter' => 'فیلتر دسته بندی', + 'posts_filter_description' => 'آدرس دسته بندی ای را که میخواهید پست های آن نمایش داده شوند را وارد نمایید. اگر میخواهید همه پست ها نمایش داده شوند این مقدار را خالی رها کنید.', + 'posts_per_page' => 'تعداد پست ها در هر صفحه', + 'posts_per_page_validation' => 'مقدار ورودی تعداد پست ها در هر صفحه نامعتبر است.', + 'posts_no_posts' => 'پیغام پستی وجود ندارد', + 'posts_no_posts_description' => 'این پیغام در صورتی که پستی جهت نمایش وجود نداشته باشد، نمایش داده می شود.', + 'posts_order' => 'ترتیب پست ها', + 'posts_order_decription' => 'مشخصه ترتیب نمایش پست ها در صفحه', + 'posts_category' => 'صفحه دسته بندی', + 'posts_category_description' => 'نام صفحه دسته بندی برای نمایش پستهای مربوط به آن.', + 'posts_post' => 'صفحه پست', + 'posts_post_description' => 'نام صفحه مربوط به نمایش کامل پست ها جهت لینک ادامه مطلب', + 'posts_except_post' => 'Except post', + 'posts_except_post_description' => 'Enter ID/URL or variable with post ID/URL you want to except', + ] +]; diff --git a/plugins/rainlab/blog/lang/fi/lang.php b/plugins/rainlab/blog/lang/fi/lang.php new file mode 100644 index 00000000..284c9211 --- /dev/null +++ b/plugins/rainlab/blog/lang/fi/lang.php @@ -0,0 +1,163 @@ + [ + 'name' => 'Blogi', + 'description' => 'Vankka bloggausalusta.' + ], + 'blog' => [ + 'menu_label' => 'Blogi', + 'menu_description' => 'Hallitse blogipostauksia', + 'posts' => 'Postaukset', + 'create_post' => 'Blogipostaus', + 'categories' => 'Categories', + 'create_category' => 'Blogikategoria', + 'tab' => 'Blogi', + 'access_posts' => 'Hallitse postauksia', + 'access_categories' => 'Hallitse kategorioita', + 'access_other_posts' => 'Hallitse muiden käyttäjien postauksia', + 'access_import_export' => 'Saa tuoda ja viedä postauksia', + 'access_publish' => 'Saa julkaista postauksia', + 'manage_settings' => 'Manage blog settings', + 'delete_confirm' => 'Olteko varma?', + 'chart_published' => 'Julkaistu', + 'chart_drafts' => 'Luonnokset', + 'chart_total' => 'Yhteensä', + 'settings_description' => 'Hallinnoi blogin asetuksia', + 'show_all_posts_label' => 'Näytä kaikki postaukset ylläpitäjille', + 'show_all_posts_comment' => 'Näytä molemmat sekä julkaistut että julkaisemattomat postaukset ylläpitäjille', + 'tab_general' => 'Yleiset' + ], + 'posts' => [ + 'list_title' => 'Hallitse blogipostauksia', + 'filter_category' => 'Kategoria', + 'filter_published' => 'Julkaistu', + 'filter_date' => 'Päivämäärä', + 'new_post' => 'Uusi postaus', + 'export_post' => 'Vie postaukset', + 'import_post' => 'Tuo postauksia' + ], + 'post' => [ + 'title' => 'Otsikko', + 'title_placeholder' => 'Uuden postauksen otsikko', + 'content' => 'Sisältö', + 'content_html' => 'HTML Sisältö', + 'slug' => 'Slugi', + 'slug_placeholder' => 'uuden-postaukse-slugi', + 'categories' => 'Kategoriat', + 'author_email' => 'Tekijän sähköposti', + 'created' => 'Luotu', + 'created_date' => 'Luomispäivämäärä', + 'updated' => 'Muokattu', + 'updated_date' => 'Muokkauspäivämäärä', + 'published' => 'Julkaistu', + 'published_by' => 'Published by', + 'current_user' => 'Current user', + 'published_date' => 'Julkaisupäivämäärä', + 'published_validation' => 'Määrittele julkaisupäivämäärä', + 'tab_edit' => 'Muokkaa', + 'tab_categories' => 'Kategoriat', + 'categories_comment' => 'Valitse kategoriat joihin postaus kuuluu', + 'categories_placeholder' => 'Kategorioita ei ole, sinun pitäisi luoda ensimmäinen ensin!', + 'tab_manage' => 'Hallitse', + 'published_on' => 'Julkaistu', + 'excerpt' => 'Poiminto', + 'summary' => 'Yhteenveto', + 'featured_images' => 'Esittelykuvat', + 'delete_confirm' => 'Poista tämä postaus?', + 'delete_success' => 'Postaukset poistettu onnistuneesti.', + 'close_confirm' => 'Tämä postaus ei ole tallennettu.', + 'return_to_posts' => 'Palaa postauslistaan' + ], + 'categories' => [ + 'list_title' => 'Hallitse blogikategorioita', + 'new_category' => 'Uusi kategoria', + 'uncategorized' => 'Luokittelematon' + ], + 'category' => [ + 'name' => 'Nimi', + 'name_placeholder' => 'Uuden kategorian nimi', + 'description' => 'Kuvaus', + 'slug' => 'Slugi', + 'slug_placeholder' => 'uuden-kategorian-slugi', + 'posts' => 'Julkaisuja', + 'delete_confirm' => 'Poista tämä kategoria?', + 'delete_success' => 'Kategoriat poistettu onnistuneesti.', + 'return_to_categories' => 'Palaa blogikategorialistaan', + 'reorder' => 'Järjestä kategoriat uudelleen' + ], + 'menuitem' => [ + 'blog_category' => 'Blogikategoria', + 'all_blog_categories' => 'Kaikki blogikategoriat', + 'blog_post' => 'Blogipostaukset', + 'all_blog_posts' => 'Kaikki blogipostaukset', + 'category_blog_posts' => 'Blogin kategorian postaukset' + ], + 'settings' => [ + 'category_title' => 'Kategorialista', + 'category_description' => 'Näyttää listan blogikategorioista sivulla.', + 'category_slug' => 'Kategorian slugi', + 'category_slug_description' => 'Etsii blogikategorian käyttämällä annettua slugi-arvoa. Komponentti käyttää tätä merkitsemään aktiivisen kategorian.', + 'category_display_empty' => 'Näytä tyhjät kategoriat', + 'category_display_empty_description' => 'Näytä kategoriat joilla ei ole yhtään postauksia.', + 'category_page' => 'Kategoriasivu', + 'category_page_description' => 'Kategorialistaussivun tiedostonimi. Oletuskomponenttiosa käyttää tätä ominaisuutta.', + 'post_title' => 'Postaus', + 'post_description' => 'Näyttää blogipostauksen sivulla.', + 'post_slug' => 'Postauksen slugi', + 'post_slug_description' => 'Etsii blogipostauksen käyttämällä annettua slugi-arvoa.', + 'post_category' => 'Kategoriasivu', + 'post_category_description' => 'Kategorialistaussivun tiedostonimi. Oletuskomponenttiosa käyttää tätä ominaisuutta.', + 'posts_title' => 'Lista postauksista', + 'posts_description' => 'Näyttää listan uusimmista blogipostauksista sivulla.', + 'posts_pagination' => 'Sivunumero', + 'posts_pagination_description' => 'Tätä arvoa käytetään määrittämään millä sivulla käyttäjä on.', + 'posts_filter' => 'Kategoriasuodatin', + 'posts_filter_description' => 'Lisää kategorian slugi tai URL parametri, jolla suodattaa postauksia. Jätä tyhjäksi näyttääksesi kaikki postaukset.', + 'posts_per_page' => 'Postauksia per sivu', + 'posts_per_page_validation' => 'Postauksia per sivu -kohta sisältää kelvottoman arvon', + 'posts_no_posts' => 'Ei julkaisuja -viesti', + 'posts_no_posts_description' => 'Viesti, joka näytetään silloin kun postauksia ei ole. Oletuskomponenttiosa käyttää tätä ominaisuutta.', + 'posts_no_posts_default' => 'Ei postauksia', + 'posts_order' => 'Postauksien järjestys', + 'posts_order_description' => 'Attribuutti, jonka mukaan postaukset tulisi järjestää', + 'posts_category' => 'Kategoriasivu', + 'posts_category_description' => 'Kategoriasivun tiedosto "Julkaistu kohteeseen" kategorialinkkejä varten. Oletuskomponenttiosa käyttää tätä ominaisuutta.', + 'posts_post' => 'Postaussivu', + 'posts_post_description' => 'Blogisivun tiedostonimi "Lue lisää" linkkejä varten. Oletuskomponenttiosa käyttää tätä ominaisuutta.', + 'posts_except_post' => 'Poissulje postauksia', + 'posts_except_post_description' => 'Lisää postauksen ID/URL tai muuttuja, jonka haluat poissulkea', + 'posts_except_post_validation' => 'Postaukset poikkeukset täytyy olla yksittäinen slugi tai ID, pilkulla erotettu slugi-lista ja ID:t', + 'posts_except_categories' => 'Poikkeavat kategoriat', + 'posts_except_categories_description' => 'Lisää pilkulla erotettu listaus kategoria slugeista tai listaus kategorioista jotka haluat jättää ulkopuolelle', + 'posts_except_categories_validation' => 'Poikkeavat kategoriat ovat oltava yksittäinen kategoria slugi tai pilkulla erotettu listaus slugeista', + 'rssfeed_blog' => 'Blogisivu', + 'rssfeed_blog_description' => 'Blogisivun tiedostonimi linkkien generointia varten. Oletuskomponenttiosa käyttää tätä ominaisuutta.', + 'rssfeed_title' => 'RSS syöte', + 'rssfeed_description' => 'Generoi RSS syötteen sisältäen postaukset blogista.', + 'group_links' => 'Linkit', + 'group_exceptions' => 'Poikkeukset' + ], + 'sorting' => [ + 'title_asc' => 'Otsikko (ascending)', + 'title_desc' => 'Otsikko (descending)', + 'created_asc' => 'Luotu (ascending)', + 'created_desc' => 'Luotu (descending)', + 'updated_asc' => 'Päivitetty (ascending)', + 'updated_desc' => 'Päivitetty (descending)', + 'published_asc' => 'Julkaistu (ascending)', + 'published_desc' => 'Julkaistu (descending)', + 'random' => 'Satunnainen' + ], + 'import' => [ + 'update_existing_label' => 'Päivitä olemassa olevat postaukset', + 'update_existing_comment' => 'Valitse tämä laatikko päivittääksesi postaukset, joissa on täsmälleen sama ID, otsikko tai slugi.', + 'auto_create_categories_label' => 'Luo tuotavassa tiedostossa määritellyt kategoriat.', + 'auto_create_categories_comment' => 'Sinun tulisi yhdistää Kategoriat-sarake käyttääksesi tätä toiminnallisuutta. Muussa tapauksessa valitse oletuskategoria alapuolelta.', + 'categories_label' => 'Kategoriat', + 'categories_comment' => 'Valitse kategoriat, joihin tuotavat postaukset liitetään (option).', + 'default_author_label' => 'Oletuskirjoittaja (optio)', + 'default_author_comment' => 'Tuonti yrittää käyttää Kirjoittaja tiedon sähköpostia yhdistäessään kirjoittajaa. Muussa tapauksessa käytetään ylempänä määriteltyä.', + 'default_author_placeholder' => '-- valitse kirjoittaja --' + ] +]; diff --git a/plugins/rainlab/blog/lang/fr/lang.php b/plugins/rainlab/blog/lang/fr/lang.php new file mode 100644 index 00000000..c2b86009 --- /dev/null +++ b/plugins/rainlab/blog/lang/fr/lang.php @@ -0,0 +1,166 @@ + [ + 'name' => 'Blog', + 'description' => 'Une plateforme de blog robuste.' + ], + 'blog' => [ + 'menu_label' => 'Blog', + 'menu_description' => 'Gestion d’articles de blog', + 'posts' => 'Articles', + 'create_post' => 'article de blog', + 'categories' => 'Catégories', + 'create_category' => 'catégorie d’articles', + 'tab' => 'Blog', + 'access_posts' => 'Gérer les articles', + 'access_categories' => 'Gérer les catégories', + 'access_other_posts' => 'Gérer les articles d’autres utilisateurs', + 'access_import_export' => 'Autorisé à importer et exporter des articles', + 'access_publish' => 'Autorisé à publier des articles', + 'manage_settings' => 'Gérer les paramètres du blog', + 'delete_confirm' => 'Confirmez-vous la suppression des articles sélectionnés ?', + 'chart_published' => 'Publié', + 'chart_drafts' => 'Brouillons', + 'chart_total' => 'Total', + 'settings_description' => 'Gérer les paramètres du blog', + 'show_all_posts_label' => "Afficher tous les messages aux utilisateurs du panneaux d'administration", + 'show_all_posts_comment' => 'Afficher autant les publications publiées et non publiées sur le site web pour les utilisateurs principaux', + 'tab_general' => 'Général' + ], + 'posts' => [ + 'list_title' => 'Gérer les articles du blog', + 'filter_category' => 'Catégorie', + 'filter_published' => 'Masquer la publication', + 'filter_date' => 'Date', + 'new_post' => 'Nouvel article', + 'export_post' => 'Exporter les articles', + 'import_post' => 'Importer des articles' + ], + 'post' => [ + 'title' => 'Titre', + 'title_placeholder' => 'Titre du nouvel article', + 'content' => 'Contenu', + 'content_html' => 'Contenu HTML', + 'slug' => 'Adresse', + 'slug_placeholder' => 'adresse-du-nouvel-article', + 'categories' => 'Catégories', + 'author_email' => 'Email de l’auteur', + 'created' => 'Créé', + 'created_date' => 'Date de création', + 'updated' => 'Mis a jour', + 'updated_date' => 'Date de mise à jour', + 'published' => 'Publié', + 'published_by' => 'Publié par', + 'current_user' => 'Utilisateur actuel', + 'published_date' => 'Date de publication', + 'published_validation' => 'Veuillez préciser la date de publication', + 'tab_edit' => 'Rédaction', + 'tab_categories' => 'Catégories', + 'categories_comment' => 'Sélectionnez les catégories auxquelles l’article est lié', + 'categories_placeholder' => 'Il n’existe pas encore de catégorie, mais vous pouvez en créer une !', + 'tab_manage' => 'Configuration', + 'published_on' => 'Publié le', + 'excerpt' => 'Extrait', + 'summary' => 'Résumé', + 'featured_images' => 'Image de promotion', + 'delete_confirm' => 'Confirmez-vous la suppression de cet article ?', + 'delete_success' => 'Ces articles ont été supprimés avec succès.', + 'close_confirm' => 'L’article n’est pas enregistré.', + 'return_to_posts' => 'Retour à la liste des articles', + 'posted_byline' => 'Posté dans :categories le :date.', + 'posted_byline_no_categories' => 'Posté le :date.', + 'date_format' => 'd M Y' + ], + 'categories' => [ + 'list_title' => 'Gérer les catégories', + 'new_category' => 'Nouvelle catégorie', + 'uncategorized' => 'Non catégorisé' + ], + 'category' => [ + 'name' => 'Nom', + 'name_placeholder' => 'Nom de la nouvelle catégorie', + 'description' => 'Description', + 'slug' => 'Adresse URL', + 'slug_placeholder' => 'adresse-de-la-nouvelle-catégorie', + 'posts' => 'Articles', + 'delete_confirm' => 'Confirmez-vous la suppression de cette catégorie ?', + 'delete_success' => 'Ces catégories ont été supprimés avec succès.', + 'return_to_categories' => 'Retour à la liste des catégories', + 'reorder' => 'Réorganiser les catégories' + ], + 'menuitem' => [ + 'blog_category' => 'Catégories du blog', + 'all_blog_categories' => 'Toutes les catégories du blog', + 'blog_post' => 'Articles du blog', + 'all_blog_posts' => 'Tous les articles du blog', + 'category_blog_posts' => "Articles d'une catégorie du blog" + ], + 'settings' => [ + 'category_title' => 'Liste des catégories', + 'category_description' => 'Afficher une liste des catégories sur la page.', + 'category_slug' => 'Adresse URL de la catégorie', + 'category_slug_description' => 'Adresse URL d’accès à la catégorie. Cette propriété est utilisée par le partial par défaut du composant pour marquer la catégorie courante comme active.', + 'category_display_empty' => 'Afficher les catégories vides.', + 'category_display_empty_description' => 'Afficher les catégories qui ne sont liés à aucun article.', + 'category_page' => 'Page des catégories', + 'category_page_description' => 'Nom de la page des catégories pour les liens de catégories. Cette propriété est utilisée par le partial par défaut du composant.', + 'post_title' => 'Article', + 'post_description' => 'Affiche un article de blog sur la page.', + 'post_slug' => 'Adresse URL de l’article', + 'post_slug_description' => 'Adresse URL d’accès à l’article.', + 'post_category' => 'Page des catégories', + 'post_category_description' => 'Nom de la page des catégories pour les liens de catégories. Cette propriété est utilisée par le partial par défaut du composant.', + 'posts_title' => 'Liste d’articles', + 'posts_description' => 'Affiche une liste des derniers articles de blog sur la page.', + 'posts_pagination' => 'Numéro de page', + 'posts_pagination_description' => 'Cette valeur est utilisée pour déterminer à quelle page l’utilisateur se trouve.', + 'posts_filter' => 'Filtre des catégories', + 'posts_filter_description' => 'Entrez une adresse de catégorie ou un paramètre d’URL pour filter les articles. Laissez vide pour afficher tous les articles.', + 'posts_per_page' => 'Articles par page', + 'posts_per_page_validation' => 'Format du nombre d’articles par page incorrect', + 'posts_no_posts' => 'Message en l’absence d’articles', + 'posts_no_posts_description' => 'Message à afficher dans la liste d’articles lorsqu’il n’y a aucun article. Cette propriété est utilisée par le partial par défaut du composant.', + 'posts_no_posts_default' => 'Aucun article trouvé', + 'posts_order' => 'Ordre des articles', + 'posts_order_description' => 'Attribut selon lequel les articles seront ordonnés', + 'posts_category' => 'Page de catégorie', + 'posts_category_description' => 'Nom du fichier de la page de catégorie pour les liens de catégorie "Publié dans". Cette propriété est utilisée par le composant par défaut du modèle partiel.', + 'posts_post' => "Page de l'article", + 'posts_post_description' => 'Nom du fichier de la page de l\'article du blog pour les liens "En savoir plus". Cette propriété est utilisée par le composant par défaut du modèle partiel.', + 'posts_except_post' => 'Article exempté', + 'posts_except_post_description' => 'Enter ID/URL or variable with post ID/URL you want to except', + 'posts_category' => 'Page des catégories', + 'posts_category_description' => 'Nom de la page des catégories pour les liens de catégories "Publié dans". Cette propriété est utilisée par le partial par défaut du composant.', + 'posts_post' => 'Page d’article', + 'posts_post_description' => 'Nom de la page d’articles pour les liens "En savoir plus". Cette propriété est utilisée par le partial par défaut du composant.', + 'rssfeed_blog' => 'Page du blog', + 'rssfeed_blog_description' => 'Nom de la page principale du blog pour générer les liens. Cette propriété est utilisé par le composant dans le partial.', + 'rssfeed_title' => 'Flux RSS', + 'rssfeed_description' => 'Génère un Flux RSS contenant les articles du blog.', + 'group_links' => 'Liens', + 'group_exceptions' => 'Exceptions' + ], + 'sorting' => [ + 'title_asc' => 'Titre (ascendant)', + 'title_desc' => 'Titre (descendant)', + 'created_asc' => 'Création le (ascendant)', + 'created_desc' => 'Création (descendant)', + 'updated_asc' => 'Mise à jour (ascendant)', + 'updated_desc' => 'Mise à jour (descendant)', + 'published_asc' => 'Publication (ascendant)', + 'published_desc' => 'Publication (descendant)', + 'random' => 'Aléatoire' + ], + 'import' => [ + 'update_existing_label' => 'Mettre à jour les articles existants', + 'update_existing_comment' => 'Cochez cette case pour mettre à jour les articles qui ont exactement le même identifiant, titre ou slug.', + 'auto_create_categories_label' => "Créer les catégories spécifiées dans le fichier d'importation", + 'auto_create_categories_comment' => 'Vous devez faire correspondre la colonne Catégories pour utiliser cette fonctionnalité. Sinon, sélectionnez les catégories par défaut à utiliser parmi les éléments ci-dessous.', + 'categories_label' => 'Catégories', + 'categories_comment' => 'Sélectionnez les catégories auxquelles appartiendront les articles importées (facultatif).', + 'default_author_label' => 'Auteur par défaut (facultatif)', + 'default_author_comment' => "L'importation tentera d'utiliser un auteur existant si vous correspondez la colonne Email à l'auteur, sinon l'auteur spécifié ci-dessus sera utilisé.", + 'default_author_placeholder' => "-- sélectionnez l'auteur --" + ] +]; diff --git a/plugins/rainlab/blog/lang/hu/lang.php b/plugins/rainlab/blog/lang/hu/lang.php new file mode 100644 index 00000000..e996f609 --- /dev/null +++ b/plugins/rainlab/blog/lang/hu/lang.php @@ -0,0 +1,166 @@ + [ + 'name' => 'Blog', + 'description' => 'Teljeskörű blog alkalmazás.' + ], + 'blog' => [ + 'menu_label' => 'Blog', + 'menu_description' => 'Blog bejegyzések kezelése', + 'posts' => 'Bejegyzések', + 'create_post' => 'blog bejegyzés', + 'categories' => 'Kategóriák', + 'create_category' => 'blog kategória', + 'tab' => 'Blog', + 'access_posts' => 'Blog bejegyzések kezelése', + 'access_categories' => 'Blog kategóriák kezelése', + 'access_other_posts' => 'Más felhasználók bejegyzéseinek kezelése', + 'access_import_export' => 'Bejegyzések importálása és exportálása', + 'access_publish' => 'Blog bejegyzések közzététele', + 'manage_settings' => 'Blog beállítások kezelése', + 'delete_confirm' => 'Törölni akarja a kijelölt bejegyzéseket?', + 'chart_published' => 'Közzétéve', + 'chart_drafts' => 'Piszkozatok', + 'chart_total' => 'Összesen', + 'settings_description' => 'Beállítási lehetőségek.', + 'show_all_posts_label' => 'Az összes bejegyzés mutatása az adminisztrátorok számára', + 'show_all_posts_comment' => 'A közzétett és a még nem publikált bejegyzések is egyaránt meg fognak jelenni az oldal szerkesztőinek.', + 'tab_general' => 'Általános' + ], + 'posts' => [ + 'list_title' => 'Blog bejegyzések', + 'filter_category' => 'Kategória', + 'filter_published' => 'Közzétéve', + 'filter_date' => 'Létrehozva', + 'new_post' => 'Új bejegyzés', + 'export_post' => 'Exportálás', + 'import_post' => 'Importálás' + ], + 'post' => [ + 'title' => 'Cím', + 'title_placeholder' => 'Új bejegyzés címe', + 'content' => 'Szöveges tartalom', + 'content_html' => 'HTML tartalom', + 'slug' => 'Keresőbarát cím', + 'slug_placeholder' => 'uj-bejegyzes-cime', + 'categories' => 'Kategóriák', + 'author_email' => 'Szerző e-mail címe', + 'created' => 'Létrehozva', + 'created_date' => 'Létrehozás dátuma', + 'updated' => 'Módosítva', + 'updated_date' => 'Módosítás dátuma', + 'published' => 'Közzétéve', + 'published_by' => 'Szerző:', + 'current_user' => 'Felhasználó', + 'published_date' => 'Közzététel dátuma', + 'published_validation' => 'Adja meg a közzététel dátumát', + 'tab_edit' => 'Szerkesztés', + 'tab_categories' => 'Kategóriák', + 'categories_comment' => 'Jelölje be azokat a kategóriákat, melyekbe be akarja sorolni a bejegyzést', + 'categories_placeholder' => 'Nincsenek kategóriák, előbb létre kell hoznia egyet!', + 'tab_manage' => 'Kezelés', + 'published_on' => 'Közzététel dátuma', + 'excerpt' => 'Kivonat', + 'summary' => 'Összegzés', + 'featured_images' => 'Kiemelt képek', + 'delete_confirm' => 'Valóban törölni akarja ezt a bejegyzést?', + 'delete_success' => 'Sikeresen törölve lettek a bejegyzések.', + 'close_confirm' => 'A bejegyzés nem került mentésre.', + 'return_to_posts' => 'Vissza a bejegyzésekhez', + 'posted_byline' => 'Publikálva: :date, itt: :categories', + 'posted_byline_no_categories' => 'Publikálva: :date.', + 'date_format' => 'Y.M.d.', + ], + 'categories' => [ + 'list_title' => 'Blog kategóriák', + 'new_category' => 'Új kategória', + 'uncategorized' => 'Nincs kategorizálva' + ], + 'category' => [ + 'name' => 'Név', + 'name_placeholder' => 'Új kategória neve', + 'description' => 'Leírás', + 'slug' => 'Keresőbarát cím', + 'slug_placeholder' => 'uj-kategoria-neve', + 'posts' => 'Bejegyzések', + 'delete_confirm' => 'Valóban törölni akarja ezt a kategóriát?', + 'delete_success' => 'Sikeresen törölve lettek a kategóriák.', + 'return_to_categories' => 'Vissza a kategóriákhoz', + 'reorder' => 'Kategóriák sorrendje' + ], + 'menuitem' => [ + 'blog_category' => 'Blog kategória', + 'all_blog_categories' => 'Összes blog kategória', + 'blog_post' => 'Blog bejegyzés', + 'all_blog_posts' => 'Összes blog bejegyzés', + 'category_blog_posts' => 'Blog kategória bejegyzések' + ], + 'settings' => [ + 'category_title' => 'Blog kategória lista', + 'category_description' => 'A blog kategóriákat listázza ki a lapon.', + 'category_slug' => 'Cím paraméter neve', + 'category_slug_description' => 'A webcím útvonal paramétere a jelenlegi kategória keresőbarát címe alapján való kereséséhez. Az alapértelmezett komponensrész ezt a tulajdonságot használja a jelenleg aktív kategória megjelöléséhez.', + 'category_display_empty' => 'Üres kategóriák kijelzése', + 'category_display_empty_description' => 'Azon kategóriák megjelenítése, melyekben nincs egy bejegyzés sem.', + 'category_page' => 'Kategória lap', + 'category_page_description' => 'A kategória hivatkozások kategória lap fájljának neve. Az alapértelmezett komponensrész használja ezt a tulajdonságot.', + 'post_title' => 'Blog bejegyzés', + 'post_description' => 'Egy blog bejegyzést jelez ki a lapon.', + 'post_slug' => 'Cím paraméter neve', + 'post_slug_description' => 'A webcím útvonal paramétere a bejegyzés keresőbarát címe alapján való kereséséhez.', + 'post_category' => 'Kategória lap', + 'post_category_description' => 'A kategória hivatkozások kategória lap fájljának neve. Az alapértelmezett komponensrész használja ezt a tulajdonságot.', + 'posts_title' => 'Blog bejegyzések', + 'posts_description' => 'A közzétett blog bejegyzések listázása a honlapon.', + 'posts_pagination' => 'Lapozósáv paraméter neve', + 'posts_pagination_description' => 'A lapozósáv lapjai által használt, várt paraméter neve.', + 'posts_filter' => 'Kategória szűrő', + 'posts_filter_description' => 'Adja meg egy kategória keresőbarát címét vagy webcím paraméterét a bejegyzések szűréséhez. Hagyja üresen az összes bejegyzés megjelenítéséhez.', + 'posts_per_page' => 'Bejegyzések laponként', + 'posts_per_page_validation' => 'A laponkénti bejegyzések értéke érvénytelen formátumú', + 'posts_no_posts' => 'Üzenet ha nincs bejegyzés', + 'posts_no_posts_description' => 'A blog bejegyzés listában kijelezendő üzenet abban az esetben, ha nincsenek bejegyzések. Az alapértelmezett komponensrész használja ezt a tulajdonságot.', + 'posts_no_posts_default' => 'Nem található bejegyzés', + 'posts_order' => 'Bejegyzések sorrendje', + 'posts_order_description' => 'Jellemző, ami alapján rendezni kell a bejegyzéseket', + 'posts_category' => 'Kategória lap', + 'posts_category_description' => 'A "Kategória" kategória hivatkozások kategória lap fájljának neve. Az alapértelmezett komponensrész használja ezt a tulajdonságot.', + 'posts_post' => 'Bejegyzéslap', + 'posts_post_description' => 'A "Tovább olvasom" hivatkozások blog bejegyzéslap fájljának neve. Az alapértelmezett komponensrész használja ezt a tulajdonságot.', + 'posts_except_post' => 'Bejegyzés kizárása', + 'posts_except_post_description' => 'Adja meg annak a bejegyzésnek az azonosítóját vagy webcímét, amit nem akar megjeleníteni a listázáskor.', + 'posts_except_post_validation' => 'A kivételnek webcímnek, illetve azonosítónak, vagy pedig ezeknek a vesszővel elválasztott felsorolásának kell lennie.', + 'posts_except_categories' => 'Kategória kizárása', + 'posts_except_categories_description' => 'Adja meg azoknak a kategóriáknak a webcímét vesszővel elválasztva, amiket nem akar megjeleníteni a listázáskor.', + 'posts_except_categories_validation' => 'A kivételnek webcímnek, vagy pedig ezeknek a vesszővel elválasztott felsorolásának kell lennie.', + 'rssfeed_blog' => 'Blog oldal', + 'rssfeed_blog_description' => 'Annak a lapnak a neve, ahol listázódnak a blog bejegyzések. Ezt a beállítást használja alapértelmezetten a blog komponens is.', + 'rssfeed_title' => 'RSS hírfolyam', + 'rssfeed_description' => 'A bloghoz tartozó RSS hírfolyam generálása.', + 'group_links' => 'Hivatkozások', + 'group_exceptions' => 'Kivételek' + ], + 'sorting' => [ + 'title_asc' => 'Név (növekvő)', + 'title_desc' => 'Név (csökkenő)', + 'created_asc' => 'Létrehozva (növekvő)', + 'created_desc' => 'Létrehozva (csökkenő)', + 'updated_asc' => 'Frissítve (növekvő)', + 'updated_desc' => 'Frissítve (csökkenő)', + 'published_asc' => 'Publikálva (növekvő)', + 'published_desc' => 'Publikálva (csökkenő)', + 'random' => 'Véletlenszerű' + ], + 'import' => [ + 'update_existing_label' => 'Meglévő bejegyzések frissítése', + 'update_existing_comment' => 'Két bejegyzés akkor számít ugyanannak, ha megegyezik az azonosító számuk, a címük vagy a webcímük.', + 'auto_create_categories_label' => 'Az import fájlban megadott kategóriák létrehozása', + 'auto_create_categories_comment' => 'A funkció használatához meg kell felelnie a Kategóriák oszlopnak, különben az alábbi elemekből válassza ki az alapértelmezett kategóriákat.', + 'categories_label' => 'Kategóriák', + 'categories_comment' => 'Válassza ki azokat a kategóriákat, amelyekhez az importált bejegyzések tartoznak (nem kötelező).', + 'default_author_label' => 'Alapértelmezett szerző (nem kötelező)', + 'default_author_comment' => 'A rendszer megpróbál egy meglévő felhasználót társítani a bejegyzéshez az Email oszlop alapján. Amennyiben ez nem sikerül, az itt megadott szerzőt fogja alapul venni.', + 'default_author_placeholder' => '-- válasszon felhasználót --' + ] +]; diff --git a/plugins/rainlab/blog/lang/it/lang.php b/plugins/rainlab/blog/lang/it/lang.php new file mode 100644 index 00000000..0eade81a --- /dev/null +++ b/plugins/rainlab/blog/lang/it/lang.php @@ -0,0 +1,107 @@ + [ + 'name' => 'Blog', + 'description' => 'Una solida piattaforma di blogging.' + ], + 'blog' => [ + 'menu_label' => 'Blog', + 'menu_description' => 'Gestisci i post', + 'posts' => 'Post', + 'create_post' => 'post del blog', + 'categories' => 'Categorie', + 'create_category' => 'categorie del blog', + 'tab' => 'Blog', + 'access_posts' => 'Gestisci i post', + 'access_categories' => 'Gestisci le categorie', + 'access_other_posts' => 'Gestisci i post di altri utenti', + 'access_import_export' => 'Permesso ad importare ed esportare i post', + 'delete_confirm' => 'Sei sicuro?', + 'chart_published' => 'Pubblicato', + 'chart_drafts' => 'Bozze', + 'chart_total' => 'Totale' + ], + 'posts' => [ + 'list_title' => 'Gestisci i post', + 'category' => 'Categoria', + 'hide_published' => 'Nascondi pubblicati', + 'new_post' => 'Nuovo post' + ], + 'post' => [ + 'title' => 'Titolo', + 'title_placeholder' => 'Titolo del nuovo post', + 'content' => 'Contenuto', + 'content_html' => 'Contenuto HTML', + 'slug' => 'Slug', + 'slug_placeholder' => 'slug-del-nuovo-post', + 'categories' => 'Categorie', + 'author_email' => 'Email dell\'autore', + 'created' => 'Creato', + 'created_date' => 'Data di creazione', + 'updated' => 'Aggiornato', + 'updated_date' => 'Data di aggiornamento', + 'published' => 'Pubblicato', + 'published_date' => 'Data di pubblicazione', + 'published_validation' => 'Per favore fornisci la data di pubblicazione', + 'tab_edit' => 'Modifica', + 'tab_categories' => 'Categorie', + 'categories_comment' => 'Seleziona le categorie a cui appartiene il post', + 'categories_placeholder' => 'Non ci sono categorie, per iniziare dovresti crearne una!', + 'tab_manage' => 'Gestisci', + 'published_on' => 'Pubblicato il', + 'excerpt' => 'Estratto', + 'summary' => 'Riassunto', + 'featured_images' => 'Immagini in evidenza', + 'delete_confirm' => 'Vuoi veramente cancellare questo post?', + 'close_confirm' => 'Questo post non è salvato.', + 'return_to_posts' => 'Ritorna all\'elenco dei post' + ], + 'categories' => [ + 'list_title' => 'Gestisci le categorie del blog', + 'new_category' => 'Nuova categoria', + 'uncategorized' => 'Non categorizzato' + ], + 'category' => [ + 'name' => 'Nome', + 'name_placeholder' => 'Nome della nuova categoria', + 'slug' => 'Slug', + 'slug_placeholder' => 'slug-nuova-categoria', + 'posts' => 'Post', + 'delete_confirm' => 'Vuoi veramente cancellare questa categoria?', + 'return_to_categories' => 'Ritorna all\'elenco delle categorie del blog', + 'reorder' => 'Riordino Categorie' + ], + 'settings' => [ + 'category_title' => 'Elenco Categorie', + 'category_description' => 'Mostra un\'elenco delle categorie del blog sulla pagina.', + 'category_slug' => 'Slug categoria', + 'category_slug_description' => "Cerca la categoria del blog usando lo slug fornito. Questa proprietà è usata dal componente parziale di default per segnare la categoria attualmente usata.", + 'category_display_empty' => 'Mostra categorie vuote', + 'category_display_empty_description' => 'Mostra categorie che non hanno alcun post.', + 'category_page' => 'Pagina delle categorie', + 'category_page_description' => 'Nome del file della pagina delle categorie contenente i link delle categorie. Questa proprietà è usata dal componente parziale di default.', + 'post_title' => 'Post', + 'post_description' => 'Mostra un post sulla pagina.', + 'post_slug' => 'Slug del post', + 'post_slug_description' => "Cerca il post con lo slug fornito.", + 'post_category' => 'Pagina delle categorie', + 'post_category_description' => 'Nome del file della pagina delle categorie contenente i link delle categorie. Questa proprietà è usata dal componente parziale di default.', + 'posts_title' => 'Elenco dei post', + 'posts_description' => 'Mostra un\'elenco degli ultimi post sulla pagina.', + 'posts_pagination' => 'Numero di pagina', + 'posts_pagination_description' => 'Questo valore è usato per determinare su quale pagina è l\'utente.', + 'posts_filter' => 'Filtro delle categorie', + 'posts_filter_description' => 'Inserisci lo slug di una categoria o un parametro dell\'URL con il quale filtrare i post. Lascia vuoto per mostrare tutti i post.', + 'posts_per_page' => 'Post per pagina', + 'posts_per_page_validation' => 'Il valore di post per pagina ha un formato non valido ', + 'posts_no_posts' => 'Messaggio per l\'assenza di post', + 'posts_no_posts_description' => 'Messaggio da mostrare nell\'elenco dei post in caso non ce ne siano. Questa proprietà è usata dal componente parziale di default.', + 'posts_order' => 'Ordine dei post', + 'posts_order_description' => 'Attributo sul quale i post dovrebbero esser ordinati', + 'posts_category' => 'Pagina delle categorie', + 'posts_category_description' => 'Nome del file per la pagina delle categorie per i link "Postato in" alle categorie. Questa proprietà è usata dal componente parziale di default.', + 'posts_post' => 'Pagina del post', + 'posts_post_description' => 'Nome del file per la pagina del post per i link "Scopri di più". Questa proprietà è usata dal componente parziale di default.' + ] +]; diff --git a/plugins/rainlab/blog/lang/ja/lang.php b/plugins/rainlab/blog/lang/ja/lang.php new file mode 100644 index 00000000..be6d0b48 --- /dev/null +++ b/plugins/rainlab/blog/lang/ja/lang.php @@ -0,0 +1,100 @@ + [ + 'name' => 'ブログ', + 'description' => 'ロバストなブログプラットフォームです。' + ], + 'blog' => [ + 'menu_label' => 'ブログ', + 'menu_description' => 'ブログの投稿管理', + 'posts' => '投稿', + 'create_post' => '投稿の追加', + 'categories' => 'カテゴリ', + 'create_category' => 'カテゴリの追加', + 'tab' => 'ブログ', + 'access_posts' => '投稿の管理', + 'access_categories' => 'カテゴリの管理', + 'access_other_posts' => '他ユーザーの投稿の管理', + 'delete_confirm' => '削除していいですか?', + 'chart_published' => '公開済み', + 'chart_drafts' => '下書き', + 'chart_total' => '合計' + ], + 'posts' => [ + 'list_title' => '投稿の管理', + 'filter_category' => 'カテゴリ', + 'filter_published' => '下書きのみ', + 'new_post' => '投稿を追加' + ], + 'post' => [ + 'title' => 'タイトル', + 'title_placeholder' => 'タイトルを入力してください', + 'slug' => 'スラッグ', + 'slug_placeholder' => 'new-post-slug−123', + 'categories' => 'カテゴリ', + 'created' => '作成日', + 'updated' => '更新日', + 'published' => '公開する', + 'published_validation' => '投稿の公開日を指定してください。', + 'tab_edit' => '編集', + 'tab_categories' => 'カテゴリ', + 'categories_comment' => '投稿を関連付けるカテゴリを選択してください。(複数選択可)', + 'categories_placeholder' => 'まだカテゴリがありません。先に作成してください。', + 'tab_manage' => '管理', + 'published_on' => '公開日', + 'excerpt' => '投稿の抜粋', + 'featured_images' => 'アイキャッチ画像', + 'delete_confirm' => '削除していいですか?', + 'close_confirm' => '投稿は保存されていません。', + 'return_to_posts' => '投稿一覧に戻る' + ], + 'categories' => [ + 'list_title' => 'カテゴリ管理', + 'new_category' => 'カテゴリの追加', + 'uncategorized' => '未分類' + ], + 'category' => [ + 'name' => '名前', + 'name_placeholder' => 'カテゴリ名をつけてください', + 'slug' => 'スラッグ', + 'slug_placeholder' => 'new-category-slug-123', + 'posts' => '投稿数', + 'delete_confirm' => '削除していいですか?', + 'return_to_categories' => 'カテゴリ一覧に戻る' + ], + 'settings' => [ + 'category_title' => 'カテゴリリスト', + 'category_description' => 'ページ内にカテゴリリストを表示します。', + 'category_slug' => 'カテゴリスラッグ', + 'category_slug_description' => "表示するカテゴリのスラッグを指定します。この項目はコンポーネントのデフォルトパーシャルで使用されます。", + 'category_display_empty' => '空のカテゴリの表示', + 'category_display_empty_description' => 'この項目がチェックされている場合、投稿が0件のカテゴリもリストに表示します。', + 'category_page' => 'カテゴリページ', + 'category_page_description' => 'カテゴリページへのリンクを生成するために、カテゴリページのファイル名を指定します。この項目はコンポーネントのデフォルトパーシャルで使用されます。', + 'post_title' => '投稿', + 'post_description' => 'ページ内に投稿を表示します。', + 'post_slug' => '投稿スラッグ', + 'post_slug_description' => "表示する投稿のスラッグを指定します。特定の投稿のスラッグか、URLパラメータ(:slug)を指定できます。", + 'post_category' => 'カテゴリページ', + 'post_category_description' => 'カテゴリリンクを生成するために、カテゴリページのファイル名を指定します。拡張子(.htm)は省いてください。この項目はコンポーネントのデフォルトパーシャルで使用されます。', + 'posts_title' => '投稿リスト', + 'posts_description' => 'ページ内に新しい投稿のリストを表示します。', + 'posts_pagination' => 'ページ番号', + 'posts_pagination_description' => 'ページ番号を指定します。URLパラメータ(:page)を指定できます。', + 'posts_filter' => 'カテゴリフィルタ', + 'posts_filter_description' => '投稿リストのフィルタを指定します。カテゴリのスラッグかURLパラメータ(:slug)を指定できます。空の場合、すべての投稿が表示されます。', + 'posts_per_page' => '1ページに表示する投稿数を指定します。', + 'posts_per_page_validation' => '1ページに表示する投稿数の形式が正しくありません。', + 'posts_no_posts' => '0件時メッセージ', + 'posts_no_posts_description' => 'この投稿リストが0件の場合に表示するメッセージを指定します。この項目はコンポーネントのデフォルトパーシャルで使用されます。', + 'posts_order' => '並び順', + 'posts_order_description' => '投稿リスト内の並び順を指定します。', + 'posts_category' => 'カテゴリページ', + 'posts_category_description' => 'カテゴリリンクを生成するために、カテゴリページのファイル名を指定します。この項目はコンポーネントのデフォルトパーシャルで使用されます。', + 'posts_post' => '投稿ページ', + 'posts_post_description' => '"Learn more"リンクを生成するため、投稿ページのファイル名を指定します。拡張子(.htm)は省いてください。この項目はコンポーネントのデフォルトパーシャルで使用されます。', + 'posts_except_post' => 'Except post', + 'posts_except_post_description' => 'Enter ID/URL or variable with post ID/URL you want to except', + ] +]; diff --git a/plugins/rainlab/blog/lang/nb-no/lang.php b/plugins/rainlab/blog/lang/nb-no/lang.php new file mode 100644 index 00000000..d0336326 --- /dev/null +++ b/plugins/rainlab/blog/lang/nb-no/lang.php @@ -0,0 +1,99 @@ + [ + 'name' => 'Blogg', + 'description' => 'En robust bloggeplattform.', + ], + 'blog' => [ + 'menu_label' => 'Blogg', + 'menu_description' => 'Administrer blogginnlegg', + 'posts' => 'Innlegg', + 'create_post' => 'innlegg', + 'categories' => 'Kategorier', + 'create_category' => 'kategori', + 'access_posts' => 'Administrer blogginnleggene', + 'access_categories' => 'Administrer bloggkategorier', + 'access_other_posts' => 'Administrere andre brukere sine blogginnlegg', + 'delete_confirm' => 'Er du sikker?', + 'chart_published' => 'Publisert', + 'chart_drafts' => 'Utkast', + 'chart_total' => 'Totalt', + ], + 'posts' => [ + 'list_title' => 'Administrer blogginnlegg', + 'filter_category' => 'Kategori', + 'filter_published' => 'Skjul publiserte', + 'new_post' => 'Nytt innlegg', + ], + 'post' => [ + 'title' => 'Tittel', + 'title_placeholder' => 'Innleggets tittel', + 'slug' => 'Slug', + 'slug_placeholder' => 'innleggets-tittel', + 'categories' => 'Kategorier', + 'created' => 'Opprettet', + 'updated' => 'Oppdatert', + 'published' => 'Publisert', + 'published_validation' => 'Velg en dato når innlegget skal publiseres', + 'tab_edit' => 'Endre', + 'tab_categories' => 'Kategorier', + 'categories_comment' => 'Velg hvilke kategorier innlegget tilhører', + 'categories_placeholder' => 'Det finnes ingen kategorier! Vennligst opprett en først.', + 'tab_manage' => 'Egenskaper', + 'published_on' => 'Publiseringsdato', + 'excerpt' => 'Utdrag', + 'featured_images' => 'Utvalgte bilder', + 'delete_confirm' => 'Vil du virkelig slette dette innlegget?', + 'close_confirm' => 'Innlegget er ikke lagret.', + 'return_to_posts' => 'Tilbake til innleggsliste', + ], + 'categories' => [ + 'list_title' => 'Administrer bloggkategorier', + 'new_category' => 'Ny kategori', + 'uncategorized' => 'Uten kategori', + ], + 'category' => [ + 'name' => 'Navn', + 'name_placeholder' => 'Kategoriens navn', + 'slug' => 'Slug', + 'slug_placeholder' => 'kategoriens-navn', + 'posts' => 'Innlegg', + 'delete_confirm' => 'Vil du virkelig slette denne kategorien?', + 'return_to_categories' => 'Tilbake til kategorilisten', + ], + 'settings' => [ + 'category_title' => 'Category List', + 'category_description' => 'Displays a list of blog categories on the page.', + 'category_slug' => 'Category slug', + 'category_slug_description' => "Look up the blog category using the supplied slug value. This property is used by the default component partial for marking the currently active category.", + 'category_display_empty' => 'Display empty categories', + 'category_display_empty_description' => 'Show categories that do not have any posts.', + 'category_page' => 'Category page', + 'category_page_description' => 'Name of the category page file for the category links. This property is used by the default component partial.', + 'post_title' => 'Post', + 'post_description' => 'Displays a blog post on the page.', + 'post_slug' => 'Post slug', + 'post_slug_description' => "Look up the blog post using the supplied slug value.", + 'post_category' => 'Category page', + 'post_category_description' => 'Name of the category page file for the category links. This property is used by the default component partial.', + 'posts_title' => 'Post List', + 'posts_description' => 'Displays a list of latest blog posts on the page.', + 'posts_pagination' => 'Page number', + 'posts_pagination_description' => 'This value is used to determine what page the user is on.', + 'posts_filter' => 'Category filter', + 'posts_filter_description' => 'Enter a category slug or URL parameter to filter the posts by. Leave empty to show all posts.', + 'posts_per_page' => 'Posts per page', + 'posts_per_page_validation' => 'Invalid format of the posts per page value', + 'posts_no_posts' => 'No posts message', + 'posts_no_posts_description' => 'Message to display in the blog post list in case if there are no posts. This property is used by the default component partial.', + 'posts_order' => 'Post order', + 'posts_order_description' => 'Attribute on which the posts should be ordered', + 'posts_category' => 'Category page', + 'posts_category_description' => 'Name of the category page file for the "Posted into" category links. This property is used by the default component partial.', + 'posts_post' => 'Post page', + 'posts_post_description' => 'Name of the blog post page file for the "Learn more" links. This property is used by the default component partial.', + 'posts_except_post' => 'Except post', + 'posts_except_post_description' => 'Enter ID/URL or variable with post ID/URL you want to except', + ], +]; diff --git a/plugins/rainlab/blog/lang/nl/lang.php b/plugins/rainlab/blog/lang/nl/lang.php new file mode 100644 index 00000000..1d5b7828 --- /dev/null +++ b/plugins/rainlab/blog/lang/nl/lang.php @@ -0,0 +1,113 @@ + [ + 'name' => 'Blog', + 'description' => 'A robust blogging platform.' + ], + 'blog' => [ + 'menu_label' => 'Blog', + 'menu_description' => 'Beheer blog artikelen', + 'posts' => 'Artikelen', + 'create_post' => 'Artikel', + 'categories' => 'Categorieën', + 'create_category' => 'blog categorie', + 'tab' => 'Blog', + 'access_posts' => 'Blog artikelen beheren', + 'access_categories' => 'Blog categorieën beheren', + 'access_other_posts' => 'Beheren van blog artikelen van gebruikers', + 'access_import_export' => 'Toegang tot importeren en exporteren van artikelen', + 'delete_confirm' => 'Weet je het zeker?', + 'chart_published' => 'Gepubliceerd', + 'chart_drafts' => 'Concepten', + 'chart_total' => 'Totaal' + ], + 'posts' => [ + 'list_title' => 'Beheren van blog artikelen', + 'filter_category' => 'Categorie', + 'filter_published' => 'Verberg gepubliceerd', + 'new_post' => 'Nieuw artikel' + ], + 'post' => [ + 'title' => 'Titel', + 'title_placeholder' => 'Titel van artikel', + 'content' => 'Inhoud', + 'content_html' => 'HTML Inhoud', + 'slug' => 'Slug', + 'slug_placeholder' => 'nieuw-artikel-slug', + 'categories' => 'Categorieën', + 'author_email' => 'E-mail auteur', + 'created' => 'Aangemaakt', + 'created_date' => 'Aangemaakt op', + 'updated' => 'Bijgewerkt', + 'updated_date' => 'Bijgewerkt op', + 'published' => 'Gepubliceerd', + 'published_by' => 'Gepubliceerd door', + 'current_user' => 'Huidige gebruiker', + 'published_date' => 'Gepubliceerd op', + 'published_validation' => 'Graag een publicatie datum opgeven', + 'tab_edit' => 'Bewerken', + 'tab_categories' => 'Categorieën', + 'categories_comment' => 'Selecteer een categorie waarbij het artikel hoort', + 'categories_placeholder' => 'Er zijn geen categorieën, maak eerst een categorie aan!', + 'tab_manage' => 'Beheer', + 'published_on' => 'Gepubliceerd op', + 'excerpt' => 'Samenvatting', + 'summary' => 'Samenvatting', + 'featured_images' => 'Uitgelichte afbeelding', + 'delete_confirm' => 'Weet je zeker dat je dit artikel wilt verwijderen?', + 'close_confirm' => 'Artikel is nog niet opgeslagen.', + 'return_to_posts' => 'Terug naar artikel overzicht', + 'posted_byline' => 'Gepubliceerd in :categories op :date.', + 'posted_byline_no_categories' => 'Gepubliceerd op :date.', + 'date_format' => 'd, M, Y', + ], + 'categories' => [ + 'list_title' => 'Beheer blog categorieën', + 'new_category' => 'Nieuwe categorie', + 'uncategorized' => 'Ongecategoriseerd' + ], + 'category' => [ + 'name' => 'Naam', + 'name_placeholder' => 'Naam categorie', + 'slug' => 'Slug', + 'slug_placeholder' => 'nieuw-categorie-slug', + 'posts' => 'Artikelen', + 'delete_confirm' => 'Weet je zeker dat je deze categorie wilt verwijderen?', + 'return_to_categories' => 'Terug naar categorie overzicht' + ], + 'settings' => [ + 'category_title' => 'Categorie overzicht', + 'category_description' => 'Geeft een lijst weer van alle categorieën op de pagina.', + 'category_slug' => 'Categorie slug', + 'category_slug_description' => 'Haal blog categorie op a.h.v. de opgegeven slug. Deze waarde wordt standaard gebruikt voor de partial om de actieve categorie te markeren.', + 'category_display_empty' => 'Geef lege categorieën weer', + 'category_display_empty_description' => 'Geef categorieën weer die geen artikelen hebben.', + 'category_page' => 'Categorie pagina', + 'category_page_description' => 'Naam van categorie pagina bestand voor de categorie links. Deze waarde wordt standaard gebruikt door de partial.', + 'post_title' => 'Artikel', + 'post_description' => 'Geef een artikel weer op de pagina.', + 'post_slug' => 'Artikel slug', + 'post_slug_description' => 'Haal een artikel op a.h.v. de opgegeven slug.', + 'post_category' => 'Categorie pagina', + 'post_category_description' => 'Naam van categorie pagina bestand voor de categorie links. Deze waarde wordt standaard gebruikt door de partial.', + 'posts_title' => 'Artikel overzicht', + 'posts_description' => 'Geeft een lijst van de nieuwste artikelen weer op de pagina.', + 'posts_pagination' => 'Pagina nummer', + 'posts_pagination_description' => 'Deze waarde wordt gebruikt om te kijken op welke pagina de gebruiker is.', + 'posts_filter' => 'Categorie filter', + 'posts_filter_description' => 'Geef een categorie slug of URL param om de artikelen hier op te kunnen filteren. Leeg laten als alle artikelen getoond moeten worden.', + 'posts_per_page' => 'Artikelen per pagina', + 'posts_per_page_validation' => 'Ongeldig formaat voor het aantal artikelen per pagina', + 'posts_no_posts' => 'Geen artikelen melding', + 'posts_no_posts_description' => 'Deze tekst wordt getoond als er geen artikelen zijn. Deze waarde wordt standaard gebruikt door de partial.', + 'posts_order' => 'Volgorde artikelen', + 'posts_order_description' => 'Kolom waar de artikelen op gesorteerd moeten worden', + 'posts_category' => 'Categorie pagina', + 'posts_category_description' => 'Naam van categorie pagina bestand voor gekoppeld artikel overzichts pagina. Deze waarde wordt standaard gebruikt door de partial.', + 'posts_post' => 'Artikel pagina', + 'posts_post_description' => 'Naam van blog pagina bestand voor de "Lees meer" links. Deze waarde wordt standaard gebruikt door de partial.', + 'posts_except_post' => 'Except post', + 'posts_except_post_description' => 'Enter ID/URL or variable with post ID/URL you want to except', + ] +]; diff --git a/plugins/rainlab/blog/lang/pl/lang.php b/plugins/rainlab/blog/lang/pl/lang.php new file mode 100644 index 00000000..69548989 --- /dev/null +++ b/plugins/rainlab/blog/lang/pl/lang.php @@ -0,0 +1,159 @@ + [ + 'name' => 'Blog', + 'description' => 'Solidna platforma blogera', + ], + 'blog' => [ + 'menu_label' => 'Blog', + 'menu_description' => 'Zarządzaj postami na blogu', + 'posts' => 'Posty', + 'create_post' => 'Utwórz post', + 'categories' => 'Kategorie', + 'create_category' => 'Utwórz kategorię', + 'tab' => 'Blog', + 'access_posts' => 'Zarządzaj postami', + 'access_categories' => 'Zarządzaj kategoriami na blogu', + 'access_other_posts' => 'Zarządzaj postami innych użytkowników', + 'access_import_export' => 'Zarządzaj importowaniem i eksportowaniem postów', + 'access_publish' => 'Publikuj posty', + 'manage_settings' => 'Zarządzaj ustawieniami bloga', + 'delete_confirm' => 'Czy jesteś pewien?', + 'chart_published' => 'Opublikowane', + 'chart_drafts' => 'Szkice', + 'chart_total' => 'Łącznie', + 'settings_description' => 'Zarządzaj ustawieniami bloga', + 'show_all_posts_label' => 'Pokaż wszystkie posty użytkownikom backendu', + 'show_all_posts_comment' => 'Wyświetl opublikowane i nieopublikowany posty na stronie dla użytkowników backendu', + 'tab_general' => 'Ogólne', + ], + 'posts' => [ + 'list_title' => 'Zarządzaj postami', + 'filter_category' => 'Kategoria', + 'filter_published' => 'Ukryj opublikowane', + 'filter_date' => 'Date', + 'new_post' => 'Nowy post', + 'export_post' => 'Eksportuj posty', + 'import_post' => 'Importuj posty', + ], + 'post' => [ + 'title' => 'Tytuł', + 'title_placeholder' => 'Tytuł nowego posta', + 'content' => 'Zawartość', + 'content_html' => 'Zawartość HTML', + 'slug' => 'Alias', + 'slug_placeholder' => 'alias-nowego-postu', + 'categories' => 'Kategorie', + 'author_email' => 'Email autora', + 'created' => 'Utworzony', + 'created_date' => 'Data utworzenia', + 'updated' => 'Zaktualizowany', + 'updated_date' => 'Data aktualizacji', + 'published' => 'Opublikowany', + 'published_date' => 'Data publikacji', + 'published_validation' => 'Proszę określić datę publikacji', + 'tab_edit' => 'Edytuj', + 'tab_categories' => 'Kategorie', + 'categories_comment' => 'Wybierz kategorie do których post należy', + 'categories_placeholder' => 'Nie ma żadnej kategorii, powinieneś utworzyć przynajmniej jedną.', + 'tab_manage' => 'Zarządzaj', + 'published_on' => 'Opublikowane', + 'excerpt' => 'Zalążek', + 'summary' => 'Summary', + 'featured_images' => 'Załączone grafiki', + 'delete_confirm' => 'Czy naprawdę chcesz usunąć ten post?', + 'delete_success' => 'Posty zostały pomyślnie usunięte.', + 'close_confirm' => 'Ten post nie jest zapisany.', + 'return_to_posts' => 'Wróć do listy postów', + ], + 'categories' => [ + 'list_title' => 'Zarządzaj kategoriami postów', + 'new_category' => 'Nowa kategoria', + 'uncategorized' => 'Bez kategorii', + ], + 'category' => [ + 'name' => 'Nazwa', + 'name_placeholder' => 'Nazwa nowej kategorii', + 'description' => 'Opis', + 'slug' => 'Alias', + 'slug_placeholder' => 'alias-nowej-kategorii', + 'posts' => 'Posty', + 'delete_confirm' => 'Czy naprawdę chcesz usunąć tę kategorię?', + 'delete_success' => 'Kategorie zostały pomyślnie usunięte.', + 'return_to_categories' => 'Wróć do listy kategorii', + 'reorder' => 'Zmień kolejnośc kategorii', + ], + 'menuitem' => [ + 'blog_category' => 'Kategorie', + 'all_blog_categories' => 'Wszystkie kategorie', + 'blog_post' => 'Post na bloga', + 'all_blog_posts' => 'Wszystkie posty', + 'category_blog_posts' => 'Posty w kategorii', + ], + 'settings' => [ + 'category_title' => 'Lista kategorii', + 'category_description' => 'Wyświetla listę blogowych kategorii na stronie.', + 'category_slug' => 'Alias kategorii', + 'category_slug_description' => 'Look up the blog category using the supplied slug value. This property is used by the default component partial for marking the currently active category.', + 'category_display_empty' => 'Pokaż puste kategorie', + 'category_display_empty_description' => 'Pokazuje kategorie, które nie posiadają postów', + 'category_page' => 'Strona kategorii', + 'category_page_description' => 'Nazwa strony kategorii gdzie są pokazywane linki. Ten parametr jest domyślnie używany przez komponent.', + 'post_title' => 'Post', + 'post_description' => 'Wyświetla pojedynczy post na stronie.', + 'post_slug' => 'Alias postu', + 'post_slug_description' => 'Szuka post po nazwie aliasu.', + 'post_category' => 'Strona kategorii', + 'post_category_description' => 'Nazwa strony kategorii gdzie są pokazywane linki. Ten parametr jest domyślnie używany przez komponent.', + 'posts_title' => 'Lista postów', + 'posts_description' => 'Wyświetla kilka ostatnich postów.', + 'posts_pagination' => 'Numer strony', + 'posts_pagination_description' => 'Ta wartość odpowiada za odczytanie numeru strony.', + 'posts_filter' => 'Filtr kategorii', + 'posts_filter_description' => 'Wprowadź alias kategorii lub adres URL aby filtrować posty. Pozostaw puste aby pokazać wszystkie.', + 'posts_per_page' => 'Ilość postów na strone', + 'posts_per_page_validation' => 'Nieprawidłowa wartość ilości postów na strone', + 'posts_no_posts' => 'Komunikat o braku postów', + 'posts_no_posts_description' => 'Wiadomość, która ukaże się kiedy komponent nie odnajdzie postów. Ten parametr jest domyślnie używany przez komponent.', + 'posts_no_posts_default' => 'Nie znaleziono postów', + 'posts_order' => 'Kolejność postów', + 'posts_order_description' => 'Parametr przez który mają być sortowane posty', + 'posts_category' => 'Strona kategorii', + 'posts_category_description' => 'Nazwa strony kategorii w wyświetlaniu linków "Posted into" [Opublikowano w]. Ten parametr jest domyślnie używany przez komponent.', + 'posts_post' => 'Strona postu', + 'posts_post_description' => 'Nazwa strony postu dla linków "Learn more" [Czytaj więcej]. Ten parametr jest domyślnie używany przez komponent.', + 'posts_except_post' => 'Wyklucz posty', + 'posts_except_post_description' => 'Wprowadź ID/URL lub zmienną z ID/URL postu, który chcesz wykluczyć', + 'posts_except_post_validation' => 'Wartość pola wykluczenia postów musi być pojedynczym ID/aliasem lub listą ID/aliasów rozdzieloną przecinkami', + 'posts_except_categories' => 'Wyklucz kategorie', + 'posts_except_categories_description' => 'Wprowadź listę aliasów kategorii rozdzieloną przecinkami lub zmienną zawierającą taką listę', + 'posts_except_categories_validation' => 'Wartośc pola wykluczenia kategorii musi być pojedynczym aliasem lub listą aliasów rozdzielonych przecinkami', + 'rssfeed_blog' => 'Strona bloga', + 'rssfeed_blog_description' => 'Nazwa strony głównej bloga do generowania linków. Używane przez domyślny fragment komponentu.', + 'rssfeed_title' => 'Kanał RSS', + 'rssfeed_description' => 'Generuje kanał RSS zawierający posty z bloga.', + 'group_links' => 'Linki', + 'group_exceptions' => 'Wyjątki', + ], + 'sorting' => [ + 'title_asc' => 'Tytuł (rosnąco)', + 'title_desc' => 'Tytuł (malejąco)', + 'created_asc' => 'Data utworzenia (rosnąco)', + 'created_desc' => 'Data utworzenia (malejąco)', + 'updated_asc' => 'Data aktualizacji (rosnąco)', + 'updated_desc' => 'Data aktualizacji (malejąco)', + 'published_asc' => 'Data publikacji (rosnąco)', + 'published_desc' => 'Data publikacji (malejąco)', + 'random' => 'Losowo', + ], + 'import' => [ + 'update_existing_label' => 'Aktualizuj istniejące wpisy', + 'update_existing_comment' => 'Zaznacz to pole, aby zaktualizować posty, które mają taki sam identyfikator (ID), tytuł lub alias.', + 'auto_create_categories_label' => 'Utwórz kategorie podane w pliku', + 'auto_create_categories_comment' => 'Aby skorzystać z tej funkcji powinieneś dopasować kolumnę Kategorii. W przeciwnym wypadku wybierz domyślną kategorię do użycia poniżej.', + 'categories_label' => 'Kategorie', + 'categories_comment' => 'Wybierz kategorię, do której będą należeć zaimportowane posty (opcjonalne).', + 'default_author_label' => 'Domyślny autor postów (opcjonalne)', + 'default_author_comment' => 'Import spróbuje dopasować istniejącego autora na podstawie kolumny email. W przypadku niepowodzenia zostanie użyty autor wybrany powyżej.', + 'default_author_placeholder' => '-- wybierz autora --', + ], +]; diff --git a/plugins/rainlab/blog/lang/pt-br/lang.php b/plugins/rainlab/blog/lang/pt-br/lang.php new file mode 100644 index 00000000..98037ce2 --- /dev/null +++ b/plugins/rainlab/blog/lang/pt-br/lang.php @@ -0,0 +1,124 @@ + [ + 'name' => 'Blog', + 'description' => 'A plataforma de blogs robusta.' + ], + 'blog' => [ + 'menu_label' => 'Blog', + 'menu_description' => 'Gerencie os posts do blog', + 'posts' => 'Posts', + 'create_post' => 'Blog post', + 'categories' => 'Categorias', + 'create_category' => 'Blog categoria', + 'tab' => 'Blog', + 'access_posts' => 'Gerencie os posts do blog', + 'access_categories' => 'Gerenciar as categorias de blog', + 'access_other_posts' => 'Gerencie outros posts de usuários do blog', + 'access_import_export' => 'Permissão para importação e exportação de mensagens', + 'access_publish' => 'Permitido publicar posts', + 'delete_confirm' => 'Você tem certeza?', + 'chart_published' => 'Publicados', + 'chart_drafts' => 'Rascunhos', + 'chart_total' => 'Total' + ], + 'posts' => [ + 'list_title' => 'Gerencie os posts do blog', + 'filter_category' => 'Categoria', + 'filter_published' => 'Esconder publicados', + 'filter_date' => 'Data', + 'new_post' => 'Novo post', + 'export_post' => 'Exportar posts', + 'import_post' => 'Importar posts' + ], + 'post' => [ + 'title' => 'Título', + 'title_placeholder' => 'Novo título do post', + 'content' => 'Conteúdo', + 'content_html' => 'HTML Conteúdo', + 'slug' => 'Slug', + 'slug_placeholder' => 'slug-do-post', + 'categories' => 'Categorias', + 'author_email' => 'Autor Email', + 'created' => 'Criado', + 'created_date' => 'Data de criação', + 'updated' => 'Atualizado', + 'updated_date' => 'Data de atualização', + 'published' => 'Publicado', + 'published_date' => 'Data de publicação', + 'published_validation' => 'Por favor, especifique a data de publicação', + 'tab_edit' => 'Editar', + 'tab_categories' => 'Categorias', + 'categories_comment' => 'Selecione as categorias do blog que o post pertence.', + 'categories_placeholder' => 'Não há categorias, você deve criar um primeiro!', + 'tab_manage' => 'Gerenciar', + 'published_on' => 'Publicado em', + 'excerpt' => 'Resumo', + 'summary' => 'Resumo', + 'featured_images' => 'Imagens destacadas', + 'delete_confirm' => 'Você realmente deseja excluir este post?', + 'close_confirm' => 'O post não foi salvo.', + 'return_to_posts' => 'Voltar à lista de posts' + ], + 'categories' => [ + 'list_title' => 'Gerenciar as categorias do blog', + 'new_category' => 'Nova categoria', + 'uncategorized' => 'Sem categoria' + ], + 'category' => [ + 'name' => 'Nome', + 'name_placeholder' => 'Novo nome para a categoria', + 'description' => 'Descrição', + 'slug' => 'Slug', + 'slug_placeholder' => 'novo-slug-da-categoria', + 'posts' => 'Posts', + 'delete_confirm' => 'Você realmente quer apagar esta categoria?', + 'return_to_categories' => 'Voltar para a lista de categorias do blog', + 'reorder' => 'Reordenar Categorias' + ], + 'menuitem' => [ + 'blog_category' => 'Blog categoria', + 'all_blog_categories' => 'Todas as categorias de blog', + 'blog_post' => 'Blog post', + 'all_blog_posts' => 'Todas as postagens do blog' + ], + 'settings' => [ + 'category_title' => 'Lista de categoria', + 'category_description' => 'Exibe uma lista de categorias de blog na página.', + 'category_slug' => 'Slug da categoria', + 'category_slug_description' => "Olhe para cima, a categoria do blog já está usando o valor fornecido! Esta propriedade é usada pelo componente default parcial para a marcação da categoria atualmente ativa.", + 'category_display_empty' => 'xibir categorias vazias', + 'category_display_empty_description' => 'Mostrar categorias que não tem nenhum post.', + 'category_page' => 'Página da categoria', + 'category_page_description' => 'Nome do arquivo de página da categoria para os links de categoria. Esta propriedade é usada pelo componente default parcial.', + 'post_title' => 'Post', + 'post_description' => 'Exibe um post na página.', + 'post_slug' => 'Post slug', + 'post_slug_description' => "Procure o post do blog usando o valor do slug fornecido.", + 'post_category' => 'Página da categoria', + 'post_category_description' => 'Nome do arquivo de página da categoria para os links de categoria. Esta propriedade é usada pelo componente default parcial.', + 'posts_title' => 'Lista de posts', + 'posts_description' => 'Exibe uma lista de últimas postagens na página.', + 'posts_pagination' => 'Número da pagina', + 'posts_pagination_description' => 'Esse valor é usado para determinar qual página o usuário está.', + 'posts_filter' => 'Filtro de categoria', + 'posts_filter_description' => 'Digite um slug de categoria ou parâmetro de URL para filtrar as mensagens. Deixe em branco para mostrar todas as mensagens.', + 'posts_per_page' => 'Posts por página', + 'posts_per_page_validation' => 'Formato inválido das mensagens por valor de página', + 'posts_no_posts' => 'Nenhuma mensagem de posts', + 'posts_no_posts_description' => 'Mensagem para exibir na lista post no caso, se não há mensagens. Esta propriedade é usada pelo componente default parcial.', + 'posts_order' => 'Orde posts', + 'posts_order_decription' => 'Atributo em que as mensagens devem ser ordenados', + 'posts_category' => 'Página de Categoria', + 'posts_category_description' => 'Nome do arquivo de página da categoria para os links de categoria. Esta propriedade é usada pelo componente default parcial.', + 'posts_post' => 'Página de posts', + 'posts_post_description' => 'Nome do arquivo post página para os "Saiba mais" links. Esta propriedade é usada pelo componente default parcial.', + 'posts_except_post' => 'Except post', + 'posts_except_post_description' => 'Enter ID/URL or variable with post ID/URL you want to except', + 'rssfeed_blog' => 'Página do Blog', + 'rssfeed_blog_description' => 'Nome do arquivo principal da página do blog para geração de links. Essa propriedade é usada pelo componente padrão parcial.', + 'rssfeed_title' => 'RSS Feed', + 'rssfeed_description' => 'Gera um feed RSS que contém posts do blog.' + ] +]; diff --git a/plugins/rainlab/blog/lang/ru/lang.php b/plugins/rainlab/blog/lang/ru/lang.php new file mode 100644 index 00000000..4a8be1b7 --- /dev/null +++ b/plugins/rainlab/blog/lang/ru/lang.php @@ -0,0 +1,159 @@ + [ + 'name' => 'Блог', + 'description' => 'Надежная блоговая-платформа.' + ], + 'blog' => [ + 'menu_label' => 'Блог', + 'menu_description' => 'Управление Блогом', + 'posts' => 'Записи', + 'create_post' => 'записи', + 'categories' => 'Категории', + 'create_category' => 'категории', + 'tab' => 'Блог', + 'access_posts' => 'Управление записями блога', + 'access_categories' => 'Управление категориями блога', + 'access_other_posts' => 'Управление записями других пользователей', + 'access_import_export' => 'Разрешено импортировать и экспортировать записи', + 'access_publish' => 'Разрешено публиковать записи', + 'manage_settings' => 'Управление настройками блога', + 'delete_confirm' => 'Вы уверены, что хотите сделать это?', + 'chart_published' => 'Опубликовано', + 'chart_drafts' => 'Черновики', + 'chart_total' => 'Всего', + 'settings_description' => 'Управление настройками блога', + 'show_all_posts_label' => 'Показывать все записи для внутренних (бэкенд) пользователей', + 'show_all_posts_comment' => 'Показывать опубликованные и неопубликованные записи на фронтенде для внутренних (бэкенд) пользователей', + 'tab_general' => 'Основные' + ], + 'posts' => [ + 'list_title' => 'Управление записями блога', + 'filter_category' => 'Категория', + 'filter_published' => 'Скрыть опубликованные', + 'filter_date' => 'Дата', + 'new_post' => 'Новая запись', + 'export_post' => 'Экспорт записей', + 'import_post' => 'Импорт записей' + ], + 'post' => [ + 'title' => 'Заголовок', + 'title_placeholder' => 'Новый заголовок записи', + 'content' => 'Контент', + 'content_html' => 'HTML Контент', + 'slug' => 'URL записи', + 'slug_placeholder' => 'new-post-slug', + 'categories' => 'Категории', + 'author_email' => 'Email автора', + 'created' => 'Создано', + 'created_date' => 'Дата создания', + 'updated' => 'Обновлено', + 'updated_date' => 'Дата обновления', + 'published' => 'Опубликовано', + 'published_date' => 'Дата публикации', + 'published_validation' => 'Пожалуйста, укажите дату публикации.', + 'tab_edit' => 'Редактор', + 'tab_categories' => 'Категории', + 'categories_comment' => 'Выберите категории, к которым относится эта запись', + 'categories_placeholder' => 'Не найдено ни одной категории, создайте хотя бы одну!', + 'tab_manage' => 'Управление', + 'published_on' => 'Опубликовано', + 'excerpt' => 'Отрывок', + 'summary' => 'Резюме', + 'featured_images' => 'Тематические изображения', + 'delete_confirm' => 'Вы действительно хотите удалить эту запись?', + 'delete_success' => 'Эти записи успешно удалены.', + 'close_confirm' => 'Запись не была сохранена.', + 'return_to_posts' => 'Вернуться к списку записей' + ], + 'categories' => [ + 'list_title' => 'Управление категориями блога', + 'new_category' => 'Новая категория', + 'uncategorized' => 'Без категории' + ], + 'category' => [ + 'name' => 'Название', + 'name_placeholder' => 'Новое имя категории', + 'description' => 'Описание', + 'slug' => 'URL адрес', + 'slug_placeholder' => 'new-category-slug', + 'posts' => 'Записи', + 'delete_confirm' => 'Вы действительно хотите удалить эту категорию?', + 'delete_success' => 'Эти категории успешно удалены.', + 'return_to_categories' => 'Вернуться к списку категорий', + 'reorder' => 'Порядок категорий' + ], + 'menuitem' => [ + 'blog_category' => 'Категория блога', + 'all_blog_categories' => 'Все категории блога', + 'blog_post' => 'Запись блога', + 'all_blog_posts' => 'Все записи блога', + 'category_blog_posts' => 'Записи категории блога' + ], + 'settings' => [ + 'category_title' => 'Список категорий блога', + 'category_description' => 'Отображает список категорий на странице.', + 'category_slug' => 'Параметр URL', + 'category_slug_description' => 'Параметр маршрута, используемый для поиска в текущей категории по URL. Это свойство используется по умолчанию компонентом Фрагменты для маркировки активной категории.', + 'category_display_empty' => 'Пустые категории', + 'category_display_empty_description' => 'Отображать категории, которые не имеют записей.', + 'category_page' => 'Страница категорий', + 'category_page_description' => 'Название страницы категорий. Это свойство используется по умолчанию компонентом Фрагменты.', + 'post_title' => 'Запись блога', + 'post_description' => 'Отображение записи блога', + 'post_slug' => 'Параметр URL', + 'post_slug_description' => 'Параметр маршрута, необходимый для выбора конкретной записи.', + 'post_category' => 'Страница категорий', + 'post_category_description' => 'Название страницы категорий. Это свойство используется по умолчанию компонентом Фрагменты.', + 'posts_title' => 'Список записей блога', + 'posts_description' => 'Отображает список последних записей блога на странице.', + 'posts_pagination' => 'Параметр постраничной навигации', + 'posts_pagination_description' => 'Параметр, необходимый для постраничной навигации.', + 'posts_filter' => 'Фильтр категорий', + 'posts_filter_description' => 'Введите URL категории или параметр URL-адреса для фильтрации записей. Оставьте пустым, чтобы посмотреть все записи.', + 'posts_per_page' => 'Записей на странице', + 'posts_per_page_validation' => 'Недопустимый Формат. Ожидаемый тип данных - действительное число.', + 'posts_no_posts' => 'Отсутствие записей', + 'posts_no_posts_description' => 'Сообщение, отображаемое в блоге, если отсутствуют записи. Это свойство используется по умолчанию компонентом Фрагменты.', + 'posts_no_posts_default' => 'Записей не найдено', + 'posts_order' => 'Сортировка', + 'posts_order_description' => 'Атрибут, по которому будут сортироваться записи.', + 'posts_category' => 'Страница категорий', + 'posts_category_description' => 'Название категории на странице записи "размещена в категории". Это свойство используется по умолчанию компонентом Фрагменты.', + 'posts_post' => 'Страница записи', + 'posts_post_description' => 'Название страницы для ссылки "подробнее". Это свойство используется по умолчанию компонентом Фрагменты.', + 'posts_except_post' => 'Кроме записи', + 'posts_except_post_description' => 'Введите ID/URL или переменную с ID/URL записи, которую вы хотите исключить', + 'posts_except_categories' => 'Кроме категорий', + 'posts_except_categories_description' => 'Введите разделенный запятыми список URL категорий или переменную со списком категорий, которые вы хотите исключить', + 'rssfeed_blog' => 'Страница блога', + 'rssfeed_blog_description' => 'Имя основного файла страницы блога для генерации ссылок. Это свойство используется по умолчанию компонентом Фрагменты.', + 'rssfeed_title' => 'RSS Feed', + 'rssfeed_description' => 'Создает RSS-канал, содержащий записи из блога.', + 'group_links' => 'Ссылки', + 'group_exceptions' => 'Исключения' + ], + 'sorting' => [ + 'title_asc' => 'Заголовок (по возрастанию)', + 'title_desc' => 'Заголовок (по убыванию)', + 'created_asc' => 'Создано (по возрастанию)', + 'created_desc' => 'Создано (по убыванию)', + 'updated_asc' => 'Обновлено (по возрастанию)', + 'updated_desc' => 'Обновлено (по убыванию)', + 'published_asc' => 'Опубликовано (по возрастанию)', + 'published_desc' => 'Опубликовано (по убыванию)', + 'random' => 'Случайно' + ], + 'import' => [ + 'update_existing_label' => 'Обновить существующие записи', + 'update_existing_comment' => 'Установите этот флажок, чтобы обновлять записи имеющие одинаковый ID, title или URL.', + 'auto_create_categories_label' => 'Создать категории указанные в импортируемом файле', + 'auto_create_categories_comment' => 'Вы должны сопоставить столбец Категории, чтобы использовать эту функцию. В противном случае выберите для назначения категорию по умолчанию из пунктов ниже.', + 'categories_label' => 'Категории', + 'categories_comment' => 'Выберите категории, к которым будут принадлежать импортированные записи (необязательно).', + 'default_author_label' => 'Автор записи по умолчанию (необязательно)', + 'default_author_comment' => 'Импорт попытается использовать существующего автора, если он соответствуете столбцу Email автора, в противном случае используется указанный выше автор.', + 'default_author_placeholder' => '-- выберите автора --' + ] +]; diff --git a/plugins/rainlab/blog/lang/sk/lang.php b/plugins/rainlab/blog/lang/sk/lang.php new file mode 100644 index 00000000..57567787 --- /dev/null +++ b/plugins/rainlab/blog/lang/sk/lang.php @@ -0,0 +1,154 @@ + [ + 'name' => 'Blog', + 'description' => 'Robustná blogová platforma.' + ], + 'blog' => [ + 'menu_label' => 'Blog', + 'menu_description' => 'Správa blogových príspevkov', + 'posts' => 'Príspevky', + 'create_post' => 'Príspevok', + 'categories' => 'Kategórie', + 'create_category' => 'Kategórie príspevkov', + 'tab' => 'Blog', + 'access_posts' => 'Správa blogových príspevkov', + 'access_categories' => 'Správa blogových kategórií', + 'access_other_posts' => 'Správa blogových príspevkov ostatných užívateľov', + 'access_import_export' => 'Možnosť importu a exportu príspevkov', + 'access_publish' => 'Možnosť publikovať príspevky', + 'delete_confirm' => 'Ste si istý?', + 'chart_published' => 'PublikovanéPublished', + 'chart_drafts' => 'Koncepty', + 'chart_total' => 'Celkom' + ], + 'posts' => [ + 'list_title' => 'Správa blogových príspevkov', + 'filter_category' => 'Kategória', + 'filter_published' => 'Publikované', + 'filter_date' => 'Dátum', + 'new_post' => 'Nový príspevok', + 'export_post' => 'Exportovať príspevky', + 'import_post' => 'Importovať príspevky' + ], + 'post' => [ + 'title' => 'Názov', + 'title_placeholder' => 'Názov nového príspevku', + 'content' => 'Obsah', + 'content_html' => 'HTML Obsah', + 'slug' => 'URL príspevku', + 'slug_placeholder' => 'url-nového-príspevku', + 'categories' => 'Kategórie', + 'author_email' => 'Email autora', + 'created' => 'Vytvorené', + 'created_date' => 'Dátum vytvorenia', + 'updated' => 'Upravené', + 'updated_date' => 'Dátum upravenia', + 'published' => 'Publikované', + 'published_date' => 'Dátum publikovania', + 'published_validation' => 'Prosím zvoľte dátum publikovania príspevku', + 'tab_edit' => 'Upraviť', + 'tab_categories' => 'Kategórie', + 'categories_comment' => 'Vyberte kategórie do ktorých tento príspevok patrí', + 'categories_placeholder' => 'Neexistujú žiadne kategórie, najprv nejakú vytvorte!', + 'tab_manage' => 'Nastavenia', + 'published_on' => 'Dátum publikovania', + 'excerpt' => 'Výňatok príspevku', + 'summary' => 'Zhrnutie', + 'featured_images' => 'Obrázky', + 'delete_confirm' => 'Zmazať tento príspevok?', + 'delete_success' => 'Vybrané príspevky boli úspešne odstránené.', + 'close_confirm' => 'Príspevok nie je uložený.', + 'return_to_posts' => 'Späť na zoznam príspevkov' + ], + 'categories' => [ + 'list_title' => 'Správa blogových kategórií', + 'new_category' => 'Nová kategória', + 'uncategorized' => 'Nezaradené' + ], + 'category' => [ + 'name' => 'Názov', + 'name_placeholder' => 'Názov novej kategórie', + 'description' => 'Popis', + 'slug' => 'URL kategórie', + 'slug_placeholder' => 'url-novej-kategórie', + 'posts' => 'Počet príspevkov', + 'delete_confirm' => 'Zmazať túto kategóriu?', + 'delete_success' => 'Vybrané kategórie boli úspešne odstránené.', + 'return_to_categories' => 'Späť na zoznam kategórií', + 'reorder' => 'Zmeniť poradie kategórií' + ], + 'menuitem' => [ + 'blog_category' => 'Blogová kategória', + 'all_blog_categories' => 'Všetky blogové kategórie', + 'blog_post' => 'Blogové príspevky', + 'all_blog_posts' => 'Všetky blogové príspevky', + 'category_blog_posts' => 'Blogové príspevky v kategórií' + ], + 'settings' => [ + 'category_title' => 'Zoznam kategórií', + 'category_description' => 'Zobrazí zoznam blogových kategórií na stránke.', + 'category_slug' => 'URL kategórie', + 'category_slug_description' => "Nájde blogovú kategóriu s týmto URL. Používa sa pre zobrazenie aktívnej kategórie.", + 'category_display_empty' => 'Zobraziť prázdne kategórie', + 'category_display_empty_description' => 'Zobrazí kategórie, ktoré nemajú žiadne príspevky.', + 'category_page' => 'Stránka kategórie', + 'category_page_description' => 'Názov stránky kategórie kam budú smerovať odkazy na kategóriu. Táto hodnota je použitá v predvolenej čiastočnej stránke komponentu.', + 'post_title' => 'Príspevok', + 'post_description' => 'Zobrazí blogový príspevok na stránke.', + 'post_slug' => 'URL príspevku', + 'post_slug_description' => "Nájde blogový príspevok s týmto URL.", + 'post_category' => 'Stránka kategórie', + 'post_category_description' => 'Názov stránky kategórie kam budú smerovať odkazy na kategóriu. Táto hodnota je použitá v predvolenej čiastočnej stránke komponentu.', + 'posts_title' => 'Zoznam príspevkov', + 'posts_description' => 'Zobrazí zoznam blogových príspevkov na stránke.', + 'posts_pagination' => 'číslo stránky', + 'posts_pagination_description' => 'Táto hodnota je použitá na určenie na akej stránke sa užívateľ nachádza.', + 'posts_filter' => 'Filter kategórií', + 'posts_filter_description' => 'Zadajte URL kategórie alebo URL parameter na filtrovanie príspevkov. Nechajte prázdne pre zobrazenie všetkých príspevkov.', + 'posts_per_page' => 'Príspevkov na stránku', + 'posts_per_page_validation' => 'Neplatný formát hodnoty počtu príspevkov na stránku', + 'posts_no_posts' => 'Správa prázdnej stránky', + 'posts_no_posts_description' => 'Správa, ktorá bude zobrazená v zozname príspevkov v prípade, že nie sú žiadne na zobrazenie. Táto hodnota je použitá v predvolenej čiastočnej stránke komponentu.', + 'posts_no_posts_default' => 'Nenašli sa žiadne príspevky', + 'posts_order' => 'Zoradenie príspevkov', + 'posts_order_description' => 'Atribút podľa ktorého budú príspevky zoradené', + 'posts_category' => 'Stránka kategórie', + 'posts_category_description' => 'Názov stránky kategórie kam budú smerovať odkazy "Vložené do". Táto hodnota je použitá v predvolenej čiastočnej stránke komponentu.', + 'posts_post' => 'Stránka príspevku', + 'posts_post_description' => 'Názov stránky príspevku kam budú smerovať odkazy "Zistiť viac". Táto hodnota je použitá v predvolenej čiastočnej stránke komponentu.', + 'posts_except_post' => 'Okrem príspevku', + 'posts_except_post_description' => 'Zadajte ID/URL alebo premennú s ID/URL príspevku, ktorý chcete vylúčiť', + 'posts_except_categories' => 'Okrem kategórií', + 'posts_except_categories_description' => 'Zadajte zoznam kategórií oddelený čiarkami alebo premennú s týmto zoznamom, ktoré chcete vylúčiť', + 'rssfeed_blog' => 'Stránka blogu', + 'rssfeed_blog_description' => 'Názov hlavnej stránky blogu na generovanie odkazov. Táto hodnota je použitá v predvolenej čiastočnej stránke komponentu.', + 'rssfeed_title' => 'RSS Kanál', + 'rssfeed_description' => 'Vygeneruje RSS kanál, ktorý obsahuje blogové príspevky.', + 'group_links' => 'Odkazy', + 'group_exceptions' => 'Výnimky' + ], + 'sorting' => [ + 'title_asc' => 'Názov (vzostupne)', + 'title_desc' => 'Názov (zostupne)', + 'created_asc' => 'Vytvorené (vzostupne)', + 'created_desc' => 'Vytvorené (zostupne)', + 'updated_asc' => 'Upravené (vzostupne)', + 'updated_desc' => 'Upravené (zostupne)', + 'published_asc' => 'Publikované (vzostupne)', + 'published_desc' => 'Publikované (zostupne)', + 'random' => 'Náhodne' + ], + 'import' => [ + 'update_existing_label' => 'Aktualizovať existujúce príspevky', + 'update_existing_comment' => 'Začiarknutím tohto políčka aktualizujte príspevky, ktoré majú presne to isté ID, titul alebo URL príspevku.', + 'auto_create_categories_label' => 'Vytvoriť kategórie zadané v importovanom súbore', + 'auto_create_categories_comment' => 'Ak chcete túto funkciu použiť, mali by sa zhodovať so stĺpcom Kategórie, inak vyberte predvolené kategórie, ktoré chcete použiť z nižšie uvedených položiek.', + 'categories_label' => 'Kategórie', + 'categories_comment' => 'Vyberte kategórie, do ktorých budú patriť importované príspevky (voliteľné).', + 'default_author_label' => 'Predvolený autor príspevku (voliteľné)', + 'default_author_comment' => 'Import sa pokúsi použiť existujúceho autora, ak sa zhoduje so stĺpcom e-mail, inak sa použije vyššie uvedený autor.', + 'default_author_placeholder' => '-- vyberte autora --' + ] +]; diff --git a/plugins/rainlab/blog/lang/sl/lang.php b/plugins/rainlab/blog/lang/sl/lang.php new file mode 100644 index 00000000..d0c3c12d --- /dev/null +++ b/plugins/rainlab/blog/lang/sl/lang.php @@ -0,0 +1,163 @@ + [ + 'name' => 'Blog', + 'description' => 'Robustna platforma za bloganje.', + ], + 'blog' => [ + 'menu_label' => 'Blog', + 'menu_description' => 'Upravljanje bloga', + 'posts' => 'Objave', + 'create_post' => 'Blog objava', + 'categories' => 'Kategorije', + 'create_category' => 'Blog kategorija', + 'tab' => 'Blog', + 'access_posts' => 'Upravljanje blog objav', + 'access_categories' => 'Upravljanje blog kategorij', + 'access_other_posts' => 'Upravljanje objav drugih uporabnikov', + 'access_import_export' => 'Dovoljenje za uvoz in izvoz objav', + 'access_publish' => 'Dovoljenje za objavljanje objav', + 'manage_settings' => 'Urejanje nastavitev bloga', + 'delete_confirm' => 'Ali ste prepričani?', + 'chart_published' => 'Objavljeno', + 'chart_drafts' => 'Osnutki', + 'chart_total' => 'Skupaj', + 'settings_description' => 'Urejanje nastavitev bloga', + 'show_all_posts_label' => 'Administratorjem prikaži vse objave', + 'show_all_posts_comment' => 'Na spletni strani prikaži administratorjem vse objavljene in neobjavljene objave.', + 'tab_general' => 'Splošno', + ], + 'posts' => [ + 'list_title' => 'Urejanje blog objav', + 'filter_category' => 'Kategorija', + 'filter_published' => 'Objavljeno', + 'filter_date' => 'Datum', + 'new_post' => 'Nova objava', + 'export_post' => 'Izvoz objav', + 'import_post' => 'Uvoz objav', + ], + 'post' => [ + 'title' => 'Naslov', + 'title_placeholder' => 'Naslov nove objave', + 'content' => 'Vsebina', + 'content_html' => 'HTML vsebina', + 'slug' => 'Povezava', + 'slug_placeholder' => 'povezava-nove-objave', + 'categories' => 'Kategorije', + 'author_email' => 'E-pošta avtorja', + 'created' => 'Ustvarjeno', + 'created_date' => 'Ustvarjeno dne', + 'updated' => 'Posodobljeno', + 'updated_date' => 'Posodobljeno dne', + 'published' => 'Objavljeno', + 'published_by' => 'Objavil', + 'current_user' => 'Trenutni uporabnik', + 'published_date' => 'Datum objave', + 'published_validation' => 'Prosimo, podajte datum objave', + 'tab_edit' => 'Upravljanje', + 'tab_categories' => 'Kategorije', + 'categories_comment' => 'Izberite kategorije, v katere spada objava', + 'categories_placeholder' => 'Ni najdenih kategorij, ustvarite vsaj eno kategorijo!', + 'tab_manage' => 'Urejanje', + 'published_on' => 'Datum objave', + 'excerpt' => 'Izvleček', + 'summary' => 'Povzetek', + 'featured_images' => 'Uporabljene slike', + 'delete_confirm' => 'Želite izbrisati to objavo?', + 'delete_success' => 'Te objave so bile uspešno izbrisane.', + 'close_confirm' => 'Ta objava ni shranjena.', + 'return_to_posts' => 'Vrnite se na seznam objav', + ], + 'categories' => [ + 'list_title' => 'Upravljanje blog kategorij', + 'new_category' => 'Nova kategorija', + 'uncategorized' => 'Nekategorizirano', + ], + 'category' => [ + 'name' => 'Naslov', + 'name_placeholder' => 'Naslov nove kategorije', + 'description' => 'Opis', + 'slug' => 'Povezava', + 'slug_placeholder' => 'povezava-nove-kategorije', + 'posts' => 'Objave', + 'delete_confirm' => 'Želite izbrisati to kategorijo?', + 'delete_success' => 'Te kategorije so bile uspešno izbrisane.', + 'return_to_categories' => 'Vrnite se na seznam blog kategorij', + 'reorder' => 'Spremenite vrstni red kategorij', + ], + 'menuitem' => [ + 'blog_category' => 'Blog kategorija', + 'all_blog_categories' => 'Vse blog kategorije', + 'blog_post' => 'Blog objava', + 'all_blog_posts' => 'Vse blog objave', + 'category_blog_posts' => 'Objave v kategoriji', + ], + 'settings' => [ + 'category_title' => 'Seznam kategorij', + 'category_description' => 'Prikaži seznam blog kategorij na strani.', + 'category_slug' => 'Povezava kategorije', + 'category_slug_description' => 'Omogoča pregled blog kategorije na podani povezavi. Ta lastnost je uporabljena v privzeti predlogi komponente za označevanje trenutno aktivne kategorije.', + 'category_display_empty' => 'Prikaži prazne kategorije', + 'category_display_empty_description' => 'Prikaže kategorije brez objav.', + 'category_page' => 'Stran s kategorijo', + 'category_page_description' => 'Naslov strani blog kategorij za ustvarjanje povezav. Ta lastnost je uporabljena v privzeti predlogi komponente.', + 'post_title' => 'Objava', + 'post_description' => 'Prikaži blog objavo na strani.', + 'post_slug' => 'Povezava objave', + 'post_slug_description' => "Omogoča pregled blog objave na podani povezavi.", + 'post_category' => 'Stran s kategorijo', + 'post_category_description' => 'Naslov strani blog kategorije za ustvarjanje povezav. Ta lastnost je uporabljena v privzeti predlogi komponente.', + 'posts_title' => 'Seznam objav', + 'posts_description' => 'Prikaži seznam najnovejših blog objav na strani.', + 'posts_pagination' => 'Številka strani', + 'posts_pagination_description' => 'Ta vrednost se uporablja za določitev, na kateri strani se nahaja uporabnik.', + 'posts_filter' => 'Filter kategorij', + 'posts_filter_description' => 'Vnesite povezavo kategorije ali URL parameter za filtriranje objav. Pustite prazno za prikaz vseh objav.', + 'posts_per_page' => 'Število objav na strani', + 'posts_per_page_validation' => 'Neveljaven format števila objav na strani', + 'posts_no_posts' => 'Sporočilo brez objav', + 'posts_no_posts_description' => 'Sporočilo, ki se prikaže na seznamu blog objav, če ni nobenih objav. Ta lastnost je uporabljena v privzeti predlogi komponente.', + 'posts_no_posts_default' => 'Ni najdenih objav', + 'posts_order' => 'Vrstni red objav', + 'posts_order_description' => 'Lastnost, glede na katero naj bodo razvrščene objave', + 'posts_category' => 'Stran s kategorijo', + 'posts_category_description' => 'Naslov strani blog kategorije za povezave "Objavljeno v". Ta lastnost je uporabljena v privzeti predlogi komponente.', + 'posts_post' => 'Stran z objavo', + 'posts_post_description' => 'Naslov strani blog objave za povezave "Preberi več". Ta lastnost je uporabljena v privzeti predlogi komponente.', + 'posts_except_post' => 'Izvzete objave', + 'posts_except_post_description' => 'Vnesite ID/URL objave ali spremenljivko z ID-jem/URL-jem objave, ki jo želite izvzeti. Za določitev večjega števila objav lahko uporabite seznam, ločen z vejicami.', + 'posts_except_post_validation' => 'Izvzete objave morajo biti posamezna povezava ali ID objave ali pa seznam povezav oz. ID-jev objav, ločen z vejicami.', + 'posts_except_categories' => 'Izvzete kategorije', + 'posts_except_categories_description' => 'Vnesite seznam povezav kategorij, ločen z vejicami ali pa spremenljivko, ki vključuje takšen seznam kategorij, ki jih želite izvzeti.', + 'posts_except_categories_validation' => 'Izvzete kategorije morajo biti posamezna povezava kategorije ali pa seznam povezav kategorij, ločen z vejicami.', + 'rssfeed_blog' => 'Stran z blogom', + 'rssfeed_blog_description' => 'Naslov glavne strani bloga za ustvarjanje povezav. Ta lastnost je uporabljena v privzeti predlogi komponente.', + 'rssfeed_title' => 'RSS vir', + 'rssfeed_description' => 'Ustvari vir RSS, ki vsebuje objave iz bloga.', + 'group_links' => 'Povezave', + 'group_exceptions' => 'Izjeme', + ], + 'sorting' => [ + 'title_asc' => 'Naslov (naraščajoče)', + 'title_desc' => 'Naslov (padajoče)', + 'created_asc' => 'Ustvarjeno (naraščajoče)', + 'created_desc' => 'Ustvarjeno (padajoče)', + 'updated_asc' => 'Posodobljeno (naraščajoče)', + 'updated_desc' => 'Posodobljeno (padajoče)', + 'published_asc' => 'Objavljeno (naraščajoče)', + 'published_desc' => 'Objavljeno (padajoče)', + 'random' => 'Naključno', + ], + 'import' => [ + 'update_existing_label' => 'Posodobi obstoječe objave', + 'update_existing_comment' => 'Označite kvadratek, če želite posodobiti objave, ki imajo popolnoma enak ID, naslov ali povezavo.', + 'auto_create_categories_label' => 'Ustvari kategorije, določene v uvozni datoteki', + 'auto_create_categories_comment' => "Za uporabo te možnosti morate ali povezati stolpec 'Kategorije' ali pa označiti privzete kategorije za uporabo iz spodnjega seznama.", + 'categories_label' => 'Kategorije', + 'categories_comment' => 'Izberite kategorije, na katere bodo povezavne uvožene objave (neobvezno).', + 'default_author_label' => 'Privzeti avtor objave (neobvezno)', + 'default_author_comment' => "Uvoz bo poskusil uporabiti obstoječega avtorja, če povežete stolpec 'E-pošta avtorja', sicer se bo uporabil zgoraj navedeni avtor.", + 'default_author_placeholder' => '-- izberite avtorja --', + ], +]; diff --git a/plugins/rainlab/blog/lang/tr/lang.php b/plugins/rainlab/blog/lang/tr/lang.php new file mode 100644 index 00000000..25f23f97 --- /dev/null +++ b/plugins/rainlab/blog/lang/tr/lang.php @@ -0,0 +1,161 @@ + [ + 'name' => 'Blog', + 'description' => 'Sağlam blog platformu.', + ], + 'blog' => [ + 'menu_label' => 'Blog', + 'menu_description' => 'Blog Gönderilerini Yönet', + 'posts' => 'Gönderiler', + 'create_post' => 'Blog gönderisi', + 'categories' => 'Kategoriler', + 'create_category' => 'Blog kategorisi', + 'tab' => 'Blog', + 'access_posts' => 'Gönderileri yönetebilsin', + 'access_categories' => 'Blog kategorilerini yönetebilsin', + 'access_other_posts' => 'Diğer kullanıcıların gönderilerini yönetebilsin', + 'access_import_export' => 'Gönderileri içeri/dışarı aktarabilsin', + 'access_publish' => 'Gönderi yayınlayabilsin', + 'manage_settings' => 'Blog ayarlarını yönet', + 'delete_confirm' => 'Emin misiniz?', + 'chart_published' => 'Yayınlandı', + 'chart_drafts' => 'Taslaklar', + 'chart_total' => 'Toplam', + 'settings_description' => 'Blog ayarlarını yönet', + 'show_all_posts_label' => 'Tüm gönderileri yönetim paneli kullanıcılarına göster', + 'show_all_posts_comment' => 'Hem yayınlanmış hem de yayınlanmamış gönderileri önyüzde yönetim paneli kullanıcılarına göster.', + 'tab_general' => 'Genel', + ], + 'posts' => [ + 'list_title' => 'Blog gönderilerini yönet', + 'filter_category' => 'Kategori', + 'filter_published' => 'Yayınlanan', + 'filter_date' => 'Tarih', + 'new_post' => 'Yeni gönderi', + 'export_post' => 'Gönderileri dışarı aktar', + 'import_post' => 'Gönderileri içeri aktar', + ], + 'post' => [ + 'title' => 'Başlık', + 'title_placeholder' => 'Yeni gönderi başlığı', + 'content' => 'İçerik', + 'content_html' => 'HTML İçeriği', + 'slug' => 'Kısa URL', + 'slug_placeholder' => 'yeni-gonderi-basligi', + 'categories' => 'Kategoriler', + 'author_email' => 'Yazar E-mail', + 'created' => 'Oluşturuldu', + 'created_date' => 'Oluşturulma tarihi', + 'updated' => 'Güncellendi', + 'updated_date' => 'Güncellenme tarihi', + 'published' => 'Yayınlandı', + 'published_date' => 'Yayınlanma tarihi', + 'published_validation' => 'Lütfen yayınlama tarihini belirtiniz', + 'tab_edit' => 'Düzenle', + 'tab_categories' => 'Kategoriler', + 'categories_comment' => 'Gönderinin ait olduğu kategorileri seçiniz', + 'categories_placeholder' => 'Kategori yok, öncelikle bir kategori oluşturmalısınız!', + 'tab_manage' => 'Yönet', + 'published_on' => 'Yayınlandı', + 'excerpt' => 'Alıntı', + 'summary' => 'Özet', + 'featured_images' => 'Öne Çıkan Görseller', + 'delete_confirm' => 'Bu yazıyı silmek istiyor musunuz?', + 'delete_success' => 'Gönderi(ler) silindi.', + 'close_confirm' => 'Gönderi kaydedilmedi.', + 'return_to_posts' => 'Gönderi listesine dön', + ], + 'categories' => [ + 'list_title' => 'Blog kategorilerini yönet', + 'new_category' => 'Yeni kategori', + 'uncategorized' => 'Kategorisiz', + ], + 'category' => [ + 'name' => 'İsim', + 'name_placeholder' => 'Yeni kategori adı', + 'description' => 'Açıklama', + 'slug' => 'Kısa URL', + 'slug_placeholder' => 'yeni-kategori-basligi', + 'posts' => 'Gönderiler', + 'delete_confirm' => 'Bu kategoriyi silmek istiyor musunuz?', + 'delete_success' => 'Kategori(ler) silindi.', + 'return_to_categories' => 'Kategori listesine dön', + 'reorder' => 'Kategorileri yeniden sırala', + ], + 'menuitem' => [ + 'blog_category' => 'Blog kategorisi', + 'all_blog_categories' => 'Tüm blog kategorileri', + 'blog_post' => 'Blog gönderisi', + 'all_blog_posts' => 'Tüm blog gönderileri', + 'category_blog_posts' => 'Blog kategori gönderileri', + ], + 'settings' => [ + 'category_title' => 'Kategori Listesi', + 'category_description' => 'Kategorilerin listesini sayfada göster.', + 'category_slug' => 'Kategori Kısa URL', + 'category_slug_description' => 'Verilen kısa URLi kullanarak blog kategorisini görüntüle. Bu özellik şu anki aktif kategoriyi işaretlemek için varsayılan kısmi bileşeni tarafından kullanılır', + 'category_display_empty' => 'Boş kategorileri göster', + 'category_display_empty_description' => 'Herhangi bir gönderi olmayan kategorileri göster.', + 'category_page' => 'Kategori sayfası', + 'category_page_description' => 'Kategori bağlantıları için kategori sayfası dosyasının adı. Bu özellik varsayılan kısmi bileşeni tarafından kullanılır.', + 'post_title' => 'Gönderi', + 'post_description' => 'Sayfada bir blog gönderisi gösterir.', + 'post_slug' => 'Gönderi Kısa URL', + 'post_slug_description' => 'Verilen kısa URL ile blog gönderisine bakın.', + 'post_category' => 'Kategori sayfası', + 'post_category_description' => 'Kategori bağlantıları için kategori sayfası dosyasının adı. Bu özellik varsayılan kısmi bileşeni tarafından kullanılır.', + 'posts_title' => 'Gönderi listesi', + 'posts_description' => 'Sayfada son blog gönderilerinin listesini gösterir.', + 'posts_pagination' => 'Sayfa numarası', + 'posts_pagination_description' => 'Bu değer kullanıcının hangi sayfada olduğunu belirlemek için kullanılır.', + 'posts_filter' => 'Kategori filtresi', + 'posts_filter_description' => 'Gönderileri filtrelemek için kategori kısa URLsi ya da URL parametresi girin. Tüm gönderiler için boş bırakın.', + 'posts_per_page' => 'Sayfa başına gönderi', + 'posts_per_page_validation' => 'Sayfa başına gönderi için geçersiz format', + 'posts_no_posts' => 'Gönderi mesajı yok', + 'posts_no_posts_description' => 'Eğer bir gönderi yoksa gönderi listesinde görüntülenecek mesaj. Bu özellik varsayılan kısmi bileşeni tarafından kullanılır.', + 'posts_no_posts_default' => 'Gönderi yok', + 'posts_order' => 'Gönderi Sırası', + 'posts_order_description' => 'Gönderilerin sıralama türü', + 'posts_category' => 'Kategori sayfası', + 'posts_category_description' => '"Yayınlanan" kategori bağlantıları için kategori sayfası dosyasının adı. Bu özellik varsayılan kısmi bileşeni tarafından kullanılır.', + 'posts_post' => 'Gönderi sayfası', + 'posts_post_description' => '"Daha fazla bilgi edinin" bağlantıları için gönderi sayfası dosyasının adı. Bu özellik varsayılan kısmi bileşeni tarafından kullanılır.', + 'posts_except_post' => 'Hariç tutulacak gönderi', + 'posts_except_post_description' => 'Hariç tutmak istediğiniz gönderinin ID/URL ini veya ID/URL içeren bir değişken girin. Birden çok gönderi belirtmek için virgülle ayrılmış liste kullanabilirsiniz.', + 'posts_except_post_validation' => 'Hariç tutulacak gönderi değeri tek bir kısa URL veya ID, veya virgülle ayrılmış kısa URL veya ID listesi olmalıdır.', + 'posts_except_categories' => 'Hariç tutulacak kategoriler', + 'posts_except_categories_description' => 'Hariç tutmak istediğiniz kategori listesini içeren virgülle ayrılmış bir kategori listesi veya listeyi içeren bir değişken girin.', + 'posts_except_categories_validation' => 'Hariç tutulacak kategoriler değeri tek bir kısa URL, veya virgülle ayrılmış kısa URL listesi olmalıdır.', + 'rssfeed_blog' => 'Blog sayfası', + 'rssfeed_blog_description' => 'Linkleri üretmek için ana blog sayfasının adı. Bu özellik, varsayılan kısmi bileşeni tarafından kullanılır.', + 'rssfeed_title' => 'RSS Beslemesi', + 'rssfeed_description' => 'Blog içerisindeki gönderileri veren RSS beslemesi oluşturur.', + 'group_links' => 'Linkler', + 'group_exceptions' => 'Hariç olanlar', + ], + 'sorting' => [ + 'title_asc' => 'Başlık (a-z)', + 'title_desc' => 'Başlık (z-a)', + 'created_asc' => 'Oluşturulma (yeniden eskiye)', + 'created_desc' => 'Oluşturulma (eskiden yeniye)', + 'updated_asc' => 'Güncellenme (yeniden eskiye)', + 'updated_desc' => 'Güncellenme (eskiden yeniye)', + 'published_asc' => 'Yayınlanma (yeniden eskiye)', + 'published_desc' => 'Yayınlanma (eskiden yeniye)', + 'random' => 'Rastgele', + ], + 'import' => [ + 'update_existing_label' => 'Mevcut gönderileri güncelle', + 'update_existing_comment' => 'Tam olarak aynı ID, başlık veya kısa URL içeren gönderileri güncellemek için bu kutuyu işaretleyin.', + 'auto_create_categories_label' => 'İçe aktarma dosyasında bulunan kategorileri oluştur', + 'auto_create_categories_comment' => 'Bu özelliği kullanmak için Kategoriler sütununu eşleştirmelisiniz, aksi takdirde aşağıdaki öğelerden kullanılacak varsayılan kategorileri seçmelisiniz.', + 'categories_label' => 'Kategoriler', + 'categories_comment' => 'İçe aktarılan gönderilerin ait olacağı kategorileri seçin (isteğe bağlı).', + 'default_author_label' => 'Varsayılan gönderi yazarı (isteğe bağlı)', + 'default_author_comment' => 'İçe aktarma, Yazar E-postası sütunuyla eşleşirse mevcut bir yazarı gönderi için kullanmaya çalışır, aksi takdirde yukarıda belirtilen yazar seçilir.', + 'default_author_placeholder' => '-- yazar seçin --', + ], +]; \ No newline at end of file diff --git a/plugins/rainlab/blog/lang/zh-cn/lang.php b/plugins/rainlab/blog/lang/zh-cn/lang.php new file mode 100644 index 00000000..f1e2e3de --- /dev/null +++ b/plugins/rainlab/blog/lang/zh-cn/lang.php @@ -0,0 +1,124 @@ + [ + 'name' => '博客', + 'description' => '一个强大的博客平台.' + ], + 'blog' => [ + 'menu_label' => '博客', + 'menu_description' => '管理博客帖子', + 'posts' => '帖子', + 'create_post' => '博客帖子', + 'categories' => '分类', + 'create_category' => '博客分类', + 'tab' => '博客', + 'access_posts' => '管理博客帖子', + 'access_categories' => '管理博客分类', + 'access_other_posts' => '管理其他用户帖子', + 'access_import_export' => '允许导入和导出', + 'access_publish' => '允许发布帖子', + 'delete_confirm' => '你确定?', + 'chart_published' => '已发布', + 'chart_drafts' => '草稿', + 'chart_total' => '总数' + ], + 'posts' => [ + 'list_title' => '管理博客帖子', + 'filter_category' => '分类', + 'filter_published' => '发布', + 'filter_date' => '日期', + 'new_post' => '创建帖子', + 'export_post' => '导出帖子', + 'import_post' => '导入帖子' + ], + 'post' => [ + 'title' => '标题', + 'title_placeholder' => '新帖子标题', + 'content' => '内容', + 'content_html' => 'HTML 内容', + 'slug' => '别名', + 'slug_placeholder' => 'new-post-slug', + 'categories' => '分类', + 'author_email' => '作者邮箱', + 'created' => '创建时间', + 'created_date' => '创建日期', + 'updated' => '更新时间', + 'updated_date' => '更新日期', + 'published' => '发布时间', + 'published_date' => '发布日期', + 'published_validation' => '请指定发布日期', + 'tab_edit' => '编辑', + 'tab_categories' => '分类', + 'categories_comment' => '选择帖子属于那个分类', + 'categories_placeholder' => '没有分类,你应该先创建一个分类!', + 'tab_manage' => '管理', + 'published_on' => '发布于', + 'excerpt' => '摘录', + 'summary' => '总结', + 'featured_images' => '特色图片', + 'delete_confirm' => '确定删除该帖子?', + 'close_confirm' => '该帖子未保存.', + 'return_to_posts' => '返回帖子列表' + ], + 'categories' => [ + 'list_title' => '管理博客分类', + 'new_category' => '新分类', + 'uncategorized' => '未分类' + ], + 'category' => [ + 'name' => '名称', + 'name_placeholder' => '新分类名称', + 'description' => '描述', + 'slug' => '别名', + 'slug_placeholder' => 'new-category-slug', + 'posts' => '帖子', + 'delete_confirm' => '确定删除分类?', + 'return_to_categories' => '返回博客分类列表', + 'reorder' => '重新排序分类' + ], + 'menuitem' => [ + 'blog_category' => '博客分类', + 'all_blog_categories' => '所有博客分类', + 'blog_post' => '博客帖子', + 'all_blog_posts' => '所有博客帖子' + ], + 'settings' => [ + 'category_title' => '分类列表', + 'category_description' => '在页面上显示帖子分类列表.', + 'category_slug' => '分类别名', + 'category_slug_description' => "用分类别名查找博客分类. 该值被默认组件的partial用来激活当前分类.", + 'category_display_empty' => '显示空的分类', + 'category_display_empty_description' => '显示没有帖子的分类.', + 'category_page' => '分类页', + 'category_page_description' => '用来生成分类链接的分类页面文件名称. 该属性被默认组件partial所使用.', + 'post_title' => '帖子', + 'post_description' => '在页面上显示博客帖子.', + 'post_slug' => '帖子别名', + 'post_slug_description' => "用帖子别名查找博客帖子.", + 'post_category' => '分类页', + 'post_category_description' => '用来生成分类链接的分类页面文件名称. 该属性被默认组件partial所使用.', + 'posts_title' => '帖子列表', + 'posts_description' => '在页面上显示最近发布的博客帖子列表.', + 'posts_pagination' => '页数', + 'posts_pagination_description' => '该值用来判定用户所在页面.', + 'posts_filter' => '过滤分类', + 'posts_filter_description' => '输入分类的别名(slug)或者URL参数来过滤帖子. 留空显示所有帖子.', + 'posts_per_page' => '每页帖子数', + 'posts_per_page_validation' => '每一页帖子数量的值的格式错误', + 'posts_no_posts' => '没有帖子的消息', + 'posts_no_posts_description' => '如果博客帖子列表中一个帖子都没有要显示的提示消息. 该属性被默认组件partial所使用.', + 'posts_order' => '帖子排序', + 'posts_order_description' => '帖子排序的属性', + 'posts_category' => '分类页', + 'posts_category_description' => '用来生成"发布到"分类链接的分类页文件名称. 该属性被默认组件partial所使用.', + 'posts_post' => '帖子页', + 'posts_post_description' => ' 查看帖子的"详情"的页面文件. 该属性被默认组件partial所使用.', + 'posts_except_post' => '排除的帖子', + 'posts_except_post_description' => '输入帖子的ID/URL或者变量来排除你不想看见的帖子', + 'rssfeed_blog' => '博客页面', + 'rssfeed_blog_description' => '生成博客帖子首页面文件名称. 该属性被默认组件partial所使用.', + 'rssfeed_title' => 'RSS Feed', + 'rssfeed_description' => '从博客生成一个包含帖子的RSS Feed.' + ] +]; diff --git a/plugins/rainlab/blog/models/Category.php b/plugins/rainlab/blog/models/Category.php new file mode 100644 index 00000000..7f833552 --- /dev/null +++ b/plugins/rainlab/blog/models/Category.php @@ -0,0 +1,315 @@ + 'required', + 'slug' => 'required|between:3,64|unique:rainlab_blog_categories', + 'code' => 'nullable|unique:rainlab_blog_categories', + ]; + + /** + * @var array Attributes that support translation, if available. + */ + public $translatable = [ + 'name', + 'description', + ['slug', 'index' => true] + ]; + + protected $guarded = []; + + public $belongsToMany = [ + 'posts' => ['RainLab\Blog\Models\Post', + 'table' => 'rainlab_blog_posts_categories', + 'order' => 'published_at desc', + 'scope' => 'isPublished' + ], + 'posts_count' => ['RainLab\Blog\Models\Post', + 'table' => 'rainlab_blog_posts_categories', + 'scope' => 'isPublished', + 'count' => true + ] + ]; + + public function beforeValidate() + { + // Generate a URL slug for this model + if (!$this->exists && !$this->slug) { + $this->slug = Str::slug($this->name); + } + } + + public function afterDelete() + { + $this->posts()->detach(); + } + + public function getPostCountAttribute() + { + return optional($this->posts_count->first())->count ?? 0; + } + + /** + * Count posts in this and nested categories + * @return int + */ + public function getNestedPostCount() + { + return $this->post_count + $this->children->sum(function ($category) { + return $category->getNestedPostCount(); + }); + } + + /** + * Sets the "url" attribute with a URL to this object + * + * @param string $pageName + * @param Cms\Classes\Controller $controller + * + * @return string + */ + public function setUrl($pageName, $controller) + { + $params = [ + 'id' => $this->id, + 'slug' => $this->slug + ]; + + return $this->url = $controller->pageUrl($pageName, $params, false); + } + + /** + * Handler for the pages.menuitem.getTypeInfo event. + * Returns a menu item type information. The type information is returned as array + * with the following elements: + * - references - a list of the item type reference options. The options are returned in the + * ["key"] => "title" format for options that don't have sub-options, and in the format + * ["key"] => ["title"=>"Option title", "items"=>[...]] for options that have sub-options. Optional, + * required only if the menu item type requires references. + * - nesting - Boolean value indicating whether the item type supports nested items. Optional, + * false if omitted. + * - dynamicItems - Boolean value indicating whether the item type could generate new menu items. + * Optional, false if omitted. + * - cmsPages - a list of CMS pages (objects of the Cms\Classes\Page class), if the item type requires a CMS page reference to + * resolve the item URL. + * @param string $type Specifies the menu item type + * @return array Returns an array + */ + public static function getMenuTypeInfo($type) + { + $result = []; + + if ($type == 'blog-category') { + $result = [ + 'references' => self::listSubCategoryOptions(), + 'nesting' => true, + 'dynamicItems' => true + ]; + } + + if ($type == 'all-blog-categories') { + $result = [ + 'dynamicItems' => true + ]; + } + + if ($result) { + $theme = Theme::getActiveTheme(); + + $pages = CmsPage::listInTheme($theme, true); + $cmsPages = []; + foreach ($pages as $page) { + if (!$page->hasComponent('blogPosts')) { + continue; + } + + /* + * Component must use a category filter with a routing parameter + * eg: categoryFilter = "{{ :somevalue }}" + */ + $properties = $page->getComponentProperties('blogPosts'); + if (!isset($properties['categoryFilter']) || !preg_match('/{{\s*:/', $properties['categoryFilter'])) { + continue; + } + + $cmsPages[] = $page; + } + + $result['cmsPages'] = $cmsPages; + } + + return $result; + } + + protected static function listSubCategoryOptions() + { + $category = self::getNested(); + + $iterator = function($categories) use (&$iterator) { + $result = []; + + foreach ($categories as $category) { + if (!$category->children) { + $result[$category->id] = $category->name; + } + else { + $result[$category->id] = [ + 'title' => $category->name, + 'items' => $iterator($category->children) + ]; + } + } + + return $result; + }; + + return $iterator($category); + } + + /** + * Handler for the pages.menuitem.resolveItem event. + * Returns information about a menu item. The result is an array + * with the following keys: + * - url - the menu item URL. Not required for menu item types that return all available records. + * The URL should be returned relative to the website root and include the subdirectory, if any. + * Use the Url::to() helper to generate the URLs. + * - isActive - determines whether the menu item is active. Not required for menu item types that + * return all available records. + * - items - an array of arrays with the same keys (url, isActive, items) + the title key. + * The items array should be added only if the $item's $nesting property value is TRUE. + * @param \RainLab\Pages\Classes\MenuItem $item Specifies the menu item. + * @param \Cms\Classes\Theme $theme Specifies the current theme. + * @param string $url Specifies the current page URL, normalized, in lower case + * The URL is specified relative to the website root, it includes the subdirectory name, if any. + * @return mixed Returns an array. Returns null if the item cannot be resolved. + */ + public static function resolveMenuItem($item, $url, $theme) + { + $result = null; + + if ($item->type == 'blog-category') { + if (!$item->reference || !$item->cmsPage) { + return; + } + + $category = self::find($item->reference); + if (!$category) { + return; + } + + $pageUrl = self::getCategoryPageUrl($item->cmsPage, $category, $theme); + if (!$pageUrl) { + return; + } + + $pageUrl = Url::to($pageUrl); + + $result = []; + $result['url'] = $pageUrl; + $result['isActive'] = $pageUrl == $url; + $result['mtime'] = $category->updated_at; + + if ($item->nesting) { + $iterator = function($categories) use (&$iterator, &$item, &$theme, $url) { + $branch = []; + + foreach ($categories as $category) { + + $branchItem = []; + $branchItem['url'] = self::getCategoryPageUrl($item->cmsPage, $category, $theme); + $branchItem['isActive'] = $branchItem['url'] == $url; + $branchItem['title'] = $category->name; + $branchItem['mtime'] = $category->updated_at; + + if ($category->children) { + $branchItem['items'] = $iterator($category->children); + } + + $branch[] = $branchItem; + } + + return $branch; + }; + + $result['items'] = $iterator($category->children); + } + } + elseif ($item->type == 'all-blog-categories') { + $result = [ + 'items' => [] + ]; + + $categories = self::with('posts_count')->orderBy('name')->get(); + foreach ($categories as $category) { + try { + $postCount = $category->posts_count->first()->count ?? null; + if ($postCount === 0) { + continue; + } + } + catch (\Exception $ex) {} + + $categoryItem = [ + 'title' => $category->name, + 'url' => self::getCategoryPageUrl($item->cmsPage, $category, $theme), + 'mtime' => $category->updated_at + ]; + + $categoryItem['isActive'] = $categoryItem['url'] == $url; + + $result['items'][] = $categoryItem; + } + } + + return $result; + } + + /** + * Returns URL of a category page. + * + * @param $pageCode + * @param $category + * @param $theme + */ + protected static function getCategoryPageUrl($pageCode, $category, $theme) + { + $page = CmsPage::loadCached($theme, $pageCode); + if (!$page) { + return; + } + + $properties = $page->getComponentProperties('blogPosts'); + if (!isset($properties['categoryFilter'])) { + return; + } + + /* + * Extract the routing parameter name from the category filter + * eg: {{ :someRouteParam }} + */ + if (!preg_match('/^\{\{([^\}]+)\}\}$/', $properties['categoryFilter'], $matches)) { + return; + } + + $paramName = substr(trim($matches[1]), 1); + $url = CmsPage::url($page->getBaseFileName(), [$paramName => $category->slug]); + + return $url; + } +} diff --git a/plugins/rainlab/blog/models/Post.php b/plugins/rainlab/blog/models/Post.php new file mode 100644 index 00000000..5f7ee6cf --- /dev/null +++ b/plugins/rainlab/blog/models/Post.php @@ -0,0 +1,708 @@ + 'required', + 'slug' => ['required', 'regex:/^[a-z0-9\/\:_\-\*\[\]\+\?\|]*$/i', 'unique:rainlab_blog_posts'], + 'content' => 'required', + 'excerpt' => '' + ]; + + /** + * @var array Attributes that support translation, if available. + */ + public $translatable = [ + 'title', + 'content', + 'content_html', + 'excerpt', + 'metadata', + ['slug', 'index' => true] + ]; + + /** + * @var array Attributes to be stored as JSON + */ + protected $jsonable = ['metadata']; + + /** + * The attributes that should be mutated to dates. + * @var array + */ + protected $dates = ['published_at']; + + /** + * The attributes on which the post list can be ordered. + * @var array + */ + public static $allowedSortingOptions = [ + 'title asc' => 'rainlab.blog::lang.sorting.title_asc', + 'title desc' => 'rainlab.blog::lang.sorting.title_desc', + 'created_at asc' => 'rainlab.blog::lang.sorting.created_asc', + 'created_at desc' => 'rainlab.blog::lang.sorting.created_desc', + 'updated_at asc' => 'rainlab.blog::lang.sorting.updated_asc', + 'updated_at desc' => 'rainlab.blog::lang.sorting.updated_desc', + 'published_at asc' => 'rainlab.blog::lang.sorting.published_asc', + 'published_at desc' => 'rainlab.blog::lang.sorting.published_desc', + 'random' => 'rainlab.blog::lang.sorting.random' + ]; + + /* + * Relations + */ + public $belongsTo = [ + 'user' => BackendUser::class + ]; + + public $belongsToMany = [ + 'categories' => [ + Category::class, + 'table' => 'rainlab_blog_posts_categories', + 'order' => 'name' + ] + ]; + + public $attachMany = [ + 'featured_images' => [\System\Models\File::class, 'order' => 'sort_order'], + 'content_images' => \System\Models\File::class + ]; + + /** + * @var array The accessors to append to the model's array form. + */ + protected $appends = ['summary', 'has_summary']; + + public $preview = null; + + /** + * Limit visibility of the published-button + * + * @param $fields + * @param null $context + * @return void + */ + public function filterFields($fields, $context = null) + { + if (!isset($fields->published, $fields->published_at)) { + return; + } + + $user = BackendAuth::getUser(); + + if (!$user->hasAnyAccess(['rainlab.blog.access_publish'])) { + $fields->published->hidden = true; + $fields->published_at->hidden = true; + } + else { + $fields->published->hidden = false; + $fields->published_at->hidden = false; + } + } + + public function afterValidate() + { + if ($this->published && !$this->published_at) { + throw new ValidationException([ + 'published_at' => Lang::get('rainlab.blog::lang.post.published_validation') + ]); + } + } + + public function getUserOptions() + { + $options = []; + + foreach (BackendUser::all() as $user) { + $options[$user->id] = $user->fullname . ' ('.$user->login.')'; + } + + return $options; + } + + public function beforeSave() + { + if (empty($this->user)) { + $user = BackendAuth::getUser(); + if (!is_null($user)) { + $this->user = $user->id; + } + } + $this->content_html = self::formatHtml($this->content); + } + + /** + * Sets the "url" attribute with a URL to this object. + * @param string $pageName + * @param Controller $controller + * @param array $params Override request URL parameters + * + * @return string + */ + public function setUrl($pageName, $controller, $params = []) + { + $params = array_merge([ + 'id' => $this->id, + 'slug' => $this->slug, + ], $params); + + if (empty($params['category'])) { + $params['category'] = $this->categories->count() ? $this->categories->first()->slug : null; + } + + // Expose published year, month and day as URL parameters. + if ($this->published) { + $params['year'] = $this->published_at->format('Y'); + $params['month'] = $this->published_at->format('m'); + $params['day'] = $this->published_at->format('d'); + } + + return $this->url = $controller->pageUrl($pageName, $params); + } + + /** + * Used to test if a certain user has permission to edit post, + * returns TRUE if the user is the owner or has other posts access. + * @param BackendUser $user + * @return bool + */ + public function canEdit(BackendUser $user) + { + return ($this->user_id == $user->id) || $user->hasAnyAccess(['rainlab.blog.access_other_posts']); + } + + public static function formatHtml($input, $preview = false) + { + $result = Markdown::parse(trim($input)); + + // Check to see if the HTML should be cleaned from potential XSS + $user = BackendAuth::getUser(); + if (!$user || !$user->hasAccess('backend.allow_unsafe_markdown')) { + $result = Html::clean($result); + } + + if ($preview) { + $result = str_replace('
    ', '
    ', $result);
    +        }
    +
    +        $result = TagProcessor::instance()->processTags($result, $preview);
    +
    +        return $result;
    +    }
    +
    +    //
    +    // Scopes
    +    //
    +
    +    public function scopeIsPublished($query)
    +    {
    +        return $query
    +            ->whereNotNull('published')
    +            ->where('published', true)
    +            ->whereNotNull('published_at')
    +            ->where('published_at', '<', Carbon::now())
    +        ;
    +    }
    +
    +    /**
    +     * Lists posts for the frontend
    +     *
    +     * @param        $query
    +     * @param  array $options Display options
    +     * @return Post
    +     */
    +    public function scopeListFrontEnd($query, $options)
    +    {
    +        /*
    +         * Default options
    +         */
    +        extract(array_merge([
    +            'page'             => 1,
    +            'perPage'          => 30,
    +            'sort'             => 'created_at',
    +            'categories'       => null,
    +            'exceptCategories' => null,
    +            'category'         => null,
    +            'search'           => '',
    +            'published'        => true,
    +            'exceptPost'       => null
    +        ], $options));
    +
    +        $searchableFields = ['title', 'slug', 'excerpt', 'content'];
    +
    +        if ($published) {
    +            $query->isPublished();
    +        }
    +
    +        /*
    +         * Except post(s)
    +         */
    +        if ($exceptPost) {
    +            $exceptPosts = (is_array($exceptPost)) ? $exceptPost : [$exceptPost];
    +            $exceptPostIds = [];
    +            $exceptPostSlugs = [];
    +
    +            foreach ($exceptPosts as $exceptPost) {
    +                $exceptPost = trim($exceptPost);
    +
    +                if (is_numeric($exceptPost)) {
    +                    $exceptPostIds[] = $exceptPost;
    +                } else {
    +                    $exceptPostSlugs[] = $exceptPost;
    +                }
    +            }
    +
    +            if (count($exceptPostIds)) {
    +                $query->whereNotIn('id', $exceptPostIds);
    +            }
    +            if (count($exceptPostSlugs)) {
    +                $query->whereNotIn('slug', $exceptPostSlugs);
    +            }
    +        }
    +
    +        /*
    +         * Sorting
    +         */
    +        if (in_array($sort, array_keys(static::$allowedSortingOptions))) {
    +            if ($sort == 'random') {
    +                $query->inRandomOrder();
    +            } else {
    +                @list($sortField, $sortDirection) = explode(' ', $sort);
    +
    +                if (is_null($sortDirection)) {
    +                    $sortDirection = "desc";
    +                }
    +
    +                $query->orderBy($sortField, $sortDirection);
    +            }
    +        }
    +
    +        /*
    +         * Search
    +         */
    +        $search = trim($search);
    +        if (strlen($search)) {
    +            $query->searchWhere($search, $searchableFields);
    +        }
    +
    +        /*
    +         * Categories
    +         */
    +        if ($categories !== null) {
    +            $categories = is_array($categories) ? $categories : [$categories];
    +            $query->whereHas('categories', function($q) use ($categories) {
    +                $q->withoutGlobalScope(NestedTreeScope::class)->whereIn('id', $categories);
    +            });
    +        }
    +
    +        /*
    +         * Except Categories
    +         */
    +        if (!empty($exceptCategories)) {
    +            $exceptCategories = is_array($exceptCategories) ? $exceptCategories : [$exceptCategories];
    +            array_walk($exceptCategories, 'trim');
    +
    +            $query->whereDoesntHave('categories', function ($q) use ($exceptCategories) {
    +                $q->withoutGlobalScope(NestedTreeScope::class)->whereIn('slug', $exceptCategories);
    +            });
    +        }
    +
    +        /*
    +         * Category, including children
    +         */
    +        if ($category !== null) {
    +            $category = Category::find($category);
    +
    +            $categories = $category->getAllChildrenAndSelf()->lists('id');
    +            $query->whereHas('categories', function($q) use ($categories) {
    +                $q->withoutGlobalScope(NestedTreeScope::class)->whereIn('id', $categories);
    +            });
    +        }
    +
    +        return $query->paginate($perPage, $page);
    +    }
    +
    +    /**
    +     * Allows filtering for specifc categories.
    +     * @param  Illuminate\Query\Builder  $query      QueryBuilder
    +     * @param  array                     $categories List of category ids
    +     * @return Illuminate\Query\Builder              QueryBuilder
    +     */
    +    public function scopeFilterCategories($query, $categories)
    +    {
    +        return $query->whereHas('categories', function($q) use ($categories) {
    +            $q->withoutGlobalScope(NestedTreeScope::class)->whereIn('id', $categories);
    +        });
    +    }
    +
    +    //
    +    // Summary / Excerpt
    +    //
    +
    +    /**
    +     * Used by "has_summary", returns true if this post uses a summary (more tag).
    +     * @return boolean
    +     */
    +    public function getHasSummaryAttribute()
    +    {
    +        $more = Config::get('rainlab.blog::summary_separator', '');
    +        $length = Config::get('rainlab.blog::summary_default_length', 600);
    +
    +        return (
    +            !!strlen(trim($this->excerpt)) ||
    +            strpos($this->content_html, $more) !== false ||
    +            strlen(Html::strip($this->content_html)) > $length
    +        );
    +    }
    +
    +    /**
    +     * Used by "summary", if no excerpt is provided, generate one from the content.
    +     * Returns the HTML content before the  tag or a limited 600
    +     * character version.
    +     *
    +     * @return string
    +     */
    +    public function getSummaryAttribute()
    +    {
    +        $excerpt = $this->excerpt;
    +        if (strlen(trim($excerpt))) {
    +            return $excerpt;
    +        }
    +
    +        $more = Config::get('rainlab.blog::summary_separator', '');
    +
    +        if (strpos($this->content_html, $more) !== false) {
    +            $parts = explode($more, $this->content_html);
    +
    +            return array_get($parts, 0);
    +        }
    +
    +        $length = Config::get('rainlab.blog::summary_default_length', 600);
    +
    +        return Html::limit($this->content_html, $length);
    +    }
    +
    +    //
    +    // Next / Previous
    +    //
    +
    +    /**
    +     * Apply a constraint to the query to find the nearest sibling
    +     *
    +     *     // Get the next post
    +     *     Post::applySibling()->first();
    +     *
    +     *     // Get the previous post
    +     *     Post::applySibling(-1)->first();
    +     *
    +     *     // Get the previous post, ordered by the ID attribute instead
    +     *     Post::applySibling(['direction' => -1, 'attribute' => 'id'])->first();
    +     *
    +     * @param       $query
    +     * @param array $options
    +     * @return
    +     */
    +    public function scopeApplySibling($query, $options = [])
    +    {
    +        if (!is_array($options)) {
    +            $options = ['direction' => $options];
    +        }
    +
    +        extract(array_merge([
    +            'direction' => 'next',
    +            'attribute' => 'published_at'
    +        ], $options));
    +
    +        $isPrevious = in_array($direction, ['previous', -1]);
    +        $directionOrder = $isPrevious ? 'asc' : 'desc';
    +        $directionOperator = $isPrevious ? '>' : '<';
    +
    +        $query->where('id', '<>', $this->id);
    +
    +        if (!is_null($this->$attribute)) {
    +            $query->where($attribute, $directionOperator, $this->$attribute);
    +        }
    +
    +        return $query->orderBy($attribute, $directionOrder);
    +    }
    +
    +    /**
    +     * Returns the next post, if available.
    +     * @return self
    +     */
    +    public function nextPost()
    +    {
    +        return self::isPublished()->applySibling()->first();
    +    }
    +
    +    /**
    +     * Returns the previous post, if available.
    +     * @return self
    +     */
    +    public function previousPost()
    +    {
    +        return self::isPublished()->applySibling(-1)->first();
    +    }
    +
    +    //
    +    // Menu helpers
    +    //
    +
    +    /**
    +     * Handler for the pages.menuitem.getTypeInfo event.
    +     * Returns a menu item type information. The type information is returned as array
    +     * with the following elements:
    +     * - references - a list of the item type reference options. The options are returned in the
    +     *   ["key"] => "title" format for options that don't have sub-options, and in the format
    +     *   ["key"] => ["title"=>"Option title", "items"=>[...]] for options that have sub-options. Optional,
    +     *   required only if the menu item type requires references.
    +     * - nesting - Boolean value indicating whether the item type supports nested items. Optional,
    +     *   false if omitted.
    +     * - dynamicItems - Boolean value indicating whether the item type could generate new menu items.
    +     *   Optional, false if omitted.
    +     * - cmsPages - a list of CMS pages (objects of the Cms\Classes\Page class), if the item type requires a CMS page reference to
    +     *   resolve the item URL.
    +     *
    +     * @param string $type Specifies the menu item type
    +     * @return array Returns an array
    +     */
    +    public static function getMenuTypeInfo($type)
    +    {
    +        $result = [];
    +
    +        if ($type == 'blog-post') {
    +            $references = [];
    +
    +            $posts = self::orderBy('title')->get();
    +            foreach ($posts as $post) {
    +                $references[$post->id] = $post->title;
    +            }
    +
    +            $result = [
    +                'references'   => $references,
    +                'nesting'      => false,
    +                'dynamicItems' => false
    +            ];
    +        }
    +
    +        if ($type == 'all-blog-posts') {
    +            $result = [
    +                'dynamicItems' => true
    +            ];
    +        }
    +
    +        if ($type == 'category-blog-posts') {
    +            $references = [];
    +
    +            $categories = Category::orderBy('name')->get();
    +            foreach ($categories as $category) {
    +                $references[$category->id] = $category->name;
    +            }
    +
    +            $result = [
    +                'references'   => $references,
    +                'dynamicItems' => true
    +            ];
    +        }
    +
    +        if ($result) {
    +            $theme = Theme::getActiveTheme();
    +
    +            $pages = CmsPage::listInTheme($theme, true);
    +            $cmsPages = [];
    +
    +            foreach ($pages as $page) {
    +                if (!$page->hasComponent('blogPost')) {
    +                    continue;
    +                }
    +
    +                /*
    +                 * Component must use a categoryPage filter with a routing parameter and post slug
    +                 * eg: categoryPage = "{{ :somevalue }}", slug = "{{ :somevalue }}"
    +                 */
    +                $properties = $page->getComponentProperties('blogPost');
    +                if (!isset($properties['categoryPage']) || !preg_match('/{{\s*:/', $properties['slug'])) {
    +                    continue;
    +                }
    +
    +                $cmsPages[] = $page;
    +            }
    +
    +            $result['cmsPages'] = $cmsPages;
    +        }
    +
    +        return $result;
    +    }
    +
    +    /**
    +     * Handler for the pages.menuitem.resolveItem event.
    +     * Returns information about a menu item. The result is an array
    +     * with the following keys:
    +     * - url - the menu item URL. Not required for menu item types that return all available records.
    +     *   The URL should be returned relative to the website root and include the subdirectory, if any.
    +     *   Use the Url::to() helper to generate the URLs.
    +     * - isActive - determines whether the menu item is active. Not required for menu item types that
    +     *   return all available records.
    +     * - items - an array of arrays with the same keys (url, isActive, items) + the title key.
    +     *   The items array should be added only if the $item's $nesting property value is TRUE.
    +     *
    +     * @param \RainLab\Pages\Classes\MenuItem $item Specifies the menu item.
    +     * @param \Cms\Classes\Theme $theme Specifies the current theme.
    +     * @param string $url Specifies the current page URL, normalized, in lower case
    +     * The URL is specified relative to the website root, it includes the subdirectory name, if any.
    +     * @return mixed Returns an array. Returns null if the item cannot be resolved.
    +     */
    +    public static function resolveMenuItem($item, $url, $theme)
    +    {
    +        $result = null;
    +
    +        if ($item->type == 'blog-post') {
    +            if (!$item->reference || !$item->cmsPage) {
    +                return;
    +            }
    +
    +            $category = self::find($item->reference);
    +            if (!$category) {
    +                return;
    +            }
    +
    +            $pageUrl = self::getPostPageUrl($item->cmsPage, $category, $theme);
    +            if (!$pageUrl) {
    +                return;
    +            }
    +
    +            $pageUrl = Url::to($pageUrl);
    +
    +            $result = [];
    +            $result['url'] = $pageUrl;
    +            $result['isActive'] = $pageUrl == $url;
    +            $result['mtime'] = $category->updated_at;
    +        }
    +        elseif ($item->type == 'all-blog-posts') {
    +            $result = [
    +                'items' => []
    +            ];
    +
    +            $posts = self::isPublished()
    +                ->orderBy('title')
    +                ->get()
    +            ;
    +
    +            foreach ($posts as $post) {
    +                $postItem = [
    +                    'title' => $post->title,
    +                    'url'   => self::getPostPageUrl($item->cmsPage, $post, $theme),
    +                    'mtime' => $post->updated_at
    +                ];
    +
    +                $postItem['isActive'] = $postItem['url'] == $url;
    +
    +                $result['items'][] = $postItem;
    +            }
    +        }
    +        elseif ($item->type == 'category-blog-posts') {
    +            if (!$item->reference || !$item->cmsPage) {
    +                return;
    +            }
    +
    +            $category = Category::find($item->reference);
    +            if (!$category) {
    +                return;
    +            }
    +
    +            $result = [
    +                'items' => []
    +            ];
    +
    +            $query = self::isPublished()
    +            ->orderBy('title');
    +
    +            $categories = $category->getAllChildrenAndSelf()->lists('id');
    +            $query->whereHas('categories', function($q) use ($categories) {
    +                $q->withoutGlobalScope(NestedTreeScope::class)->whereIn('id', $categories);
    +            });
    +
    +            $posts = $query->get();
    +
    +            foreach ($posts as $post) {
    +                $postItem = [
    +                    'title' => $post->title,
    +                    'url'   => self::getPostPageUrl($item->cmsPage, $post, $theme),
    +                    'mtime' => $post->updated_at
    +                ];
    +
    +                $postItem['isActive'] = $postItem['url'] == $url;
    +
    +                $result['items'][] = $postItem;
    +            }
    +        }
    +
    +        return $result;
    +    }
    +
    +    /**
    +     * Returns URL of a post page.
    +     *
    +     * @param $pageCode
    +     * @param $category
    +     * @param $theme
    +     */
    +    protected static function getPostPageUrl($pageCode, $category, $theme)
    +    {
    +        $page = CmsPage::loadCached($theme, $pageCode);
    +        if (!$page) {
    +            return;
    +        }
    +
    +        $properties = $page->getComponentProperties('blogPost');
    +        if (!isset($properties['slug'])) {
    +            return;
    +        }
    +
    +        /*
    +         * Extract the routing parameter name from the category filter
    +         * eg: {{ :someRouteParam }}
    +         */
    +        if (!preg_match('/^\{\{([^\}]+)\}\}$/', $properties['slug'], $matches)) {
    +            return;
    +        }
    +
    +        $paramName = substr(trim($matches[1]), 1);
    +        $params = [
    +            $paramName => $category->slug,
    +            'year'  => $category->published_at->format('Y'),
    +            'month' => $category->published_at->format('m'),
    +            'day'   => $category->published_at->format('d')
    +        ];
    +        $url = CmsPage::url($page->getBaseFileName(), $params);
    +
    +        return $url;
    +    }
    +}
    diff --git a/plugins/rainlab/blog/models/PostExport.php b/plugins/rainlab/blog/models/PostExport.php
    new file mode 100644
    index 00000000..024d94a7
    --- /dev/null
    +++ b/plugins/rainlab/blog/models/PostExport.php
    @@ -0,0 +1,94 @@
    + [
    +            'Backend\Models\User',
    +            'key' => 'user_id'
    +        ]
    +    ];
    +
    +    public $belongsToMany = [
    +        'post_categories' => [
    +            'RainLab\Blog\Models\Category',
    +            'table'    => 'rainlab_blog_posts_categories',
    +            'key'      => 'post_id',
    +            'otherKey' => 'category_id'
    +        ]
    +    ];
    +
    +    public $hasMany = [
    +        'featured_images' => [
    +            'System\Models\File',
    +            'order' => 'sort_order',
    +            'key' => 'attachment_id',
    +            'conditions' => "field = 'featured_images' AND attachment_type = 'RainLab\\\\Blog\\\\Models\\\\Post'"
    +        ]
    +    ];
    +
    +    /**
    +     * The accessors to append to the model's array form.
    +     * @var array
    +     */
    +    protected $appends = [
    +        'author_email',
    +        'categories',
    +        'featured_image_urls'
    +    ];
    +
    +    public function exportData($columns, $sessionKey = null)
    +    {
    +        $result = self::make()
    +            ->with([
    +                'post_user',
    +                'post_categories',
    +                'featured_images'
    +            ])
    +            ->get()
    +            ->toArray()
    +        ;
    +
    +        return $result;
    +    }
    +
    +    public function getAuthorEmailAttribute()
    +    {
    +        if (!$this->post_user) {
    +            return '';
    +        }
    +
    +        return $this->post_user->email;
    +    }
    +
    +    public function getCategoriesAttribute()
    +    {
    +        if (!$this->post_categories) {
    +            return '';
    +        }
    +
    +        return $this->encodeArrayValue($this->post_categories->lists('name'));
    +    }
    +
    +    public function getFeaturedImageUrlsAttribute()
    +    {
    +        if (!$this->featured_images) {
    +            return '';
    +        }
    +
    +        return $this->encodeArrayValue($this->featured_images->map(function ($image) {
    +            return $image->getPath();
    +        }));
    +    }
    +}
    diff --git a/plugins/rainlab/blog/models/PostImport.php b/plugins/rainlab/blog/models/PostImport.php
    new file mode 100644
    index 00000000..858d9f05
    --- /dev/null
    +++ b/plugins/rainlab/blog/models/PostImport.php
    @@ -0,0 +1,155 @@
    + 'required',
    +        'content' => 'required'
    +    ];
    +
    +    protected $authorEmailCache = [];
    +
    +    protected $categoryNameCache = [];
    +
    +    public function getDefaultAuthorOptions()
    +    {
    +        return AuthorModel::all()->lists('full_name', 'email');
    +    }
    +
    +    public function getCategoriesOptions()
    +    {
    +        return Category::lists('name', 'id');
    +    }
    +
    +    public function importData($results, $sessionKey = null)
    +    {
    +        $firstRow = reset($results);
    +
    +        //  Validation
    +        if ($this->auto_create_categories && !array_key_exists('categories', $firstRow)) {
    +            throw new ApplicationException('Please specify a match for the Categories column.');
    +        }
    +
    +        // Import
    +        foreach ($results as $row => $data) {
    +            try {
    +
    +                if (!$title = array_get($data, 'title')) {
    +                    $this->logSkipped($row, 'Missing post title');
    +                    continue;
    +                }
    +
    +                // Find or create
    +                $post = Post::make();
    +
    +                if ($this->update_existing) {
    +                    $post = $this->findDuplicatePost($data) ?: $post;
    +                }
    +
    +                $postExists = $post->exists;
    +
    +                // Set attributes
    +                $except = ['id', 'categories', 'author_email'];
    +
    +                foreach (array_except($data, $except) as $attribute => $value) {
    +                    if (in_array($attribute, $post->getDates()) && empty($value)) {
    +                        continue;
    +                    }
    +                    $post->{$attribute} = isset($value) ? $value : null;
    +                }
    +
    +                if ($author = $this->findAuthorFromEmail($data)) {
    +                    $post->user_id = $author->id;
    +                }
    +
    +                $post->forceSave();
    +
    +                if ($categoryIds = $this->getCategoryIdsForPost($data)) {
    +                    $post->categories()->sync($categoryIds, false);
    +                }
    +
    +                // Log results
    +                if ($postExists) {
    +                    $this->logUpdated();
    +                }
    +                else {
    +                    $this->logCreated();
    +                }
    +            }
    +            catch (Exception $ex) {
    +                $this->logError($row, $ex->getMessage());
    +            }
    +        }
    +    }
    +
    +    protected function findAuthorFromEmail($data)
    +    {
    +        if (!$email = array_get($data, 'email', $this->default_author)) {
    +            return null;
    +        }
    +
    +        if (isset($this->authorEmailCache[$email])) {
    +            return $this->authorEmailCache[$email];
    +        }
    +
    +        $author = AuthorModel::where('email', $email)->first();
    +        return $this->authorEmailCache[$email] = $author;
    +    }
    +
    +    protected function findDuplicatePost($data)
    +    {
    +        if ($id = array_get($data, 'id')) {
    +            return Post::find($id);
    +        }
    +
    +        $title = array_get($data, 'title');
    +        $post = Post::where('title', $title);
    +
    +        if ($slug = array_get($data, 'slug')) {
    +            $post->orWhere('slug', $slug);
    +        }
    +
    +        return $post->first();
    +    }
    +
    +    protected function getCategoryIdsForPost($data)
    +    {
    +        $ids = [];
    +
    +        if ($this->auto_create_categories) {
    +            $categoryNames = $this->decodeArrayValue(array_get($data, 'categories'));
    +
    +            foreach ($categoryNames as $name) {
    +                if (!$name = trim($name)) {
    +                    continue;
    +                }
    +
    +                if (isset($this->categoryNameCache[$name])) {
    +                    $ids[] = $this->categoryNameCache[$name];
    +                }
    +                else {
    +                    $newCategory = Category::firstOrCreate(['name' => $name]);
    +                    $ids[] = $this->categoryNameCache[$name] = $newCategory->id;
    +                }
    +            }
    +        }
    +        elseif ($this->categories) {
    +            $ids = (array) $this->categories;
    +        }
    +
    +        return $ids;
    +    }
    +}
    diff --git a/plugins/rainlab/blog/models/Settings.php b/plugins/rainlab/blog/models/Settings.php
    new file mode 100644
    index 00000000..2c27a537
    --- /dev/null
    +++ b/plugins/rainlab/blog/models/Settings.php
    @@ -0,0 +1,18 @@
    + ['boolean'],
    +    ];
    +}
    diff --git a/plugins/rainlab/blog/models/category/columns.yaml b/plugins/rainlab/blog/models/category/columns.yaml
    new file mode 100644
    index 00000000..578d846e
    --- /dev/null
    +++ b/plugins/rainlab/blog/models/category/columns.yaml
    @@ -0,0 +1,13 @@
    +# ===================================
    +#  Column Definitions
    +# ===================================
    +
    +columns:
    +
    +    name:
    +        label: rainlab.blog::lang.category.name
    +        searchable: true
    +
    +    post_count:
    +        label: rainlab.blog::lang.category.posts
    +        sortable: false
    diff --git a/plugins/rainlab/blog/models/category/fields.yaml b/plugins/rainlab/blog/models/category/fields.yaml
    new file mode 100644
    index 00000000..adcf1bc3
    --- /dev/null
    +++ b/plugins/rainlab/blog/models/category/fields.yaml
    @@ -0,0 +1,23 @@
    +# ===================================
    +#  Field Definitions
    +# ===================================
    +
    +fields:
    +
    +    name:
    +        label: rainlab.blog::lang.category.name
    +        placeholder: rainlab.blog::lang.category.name_placeholder
    +        span: left
    +
    +    slug:
    +        label: rainlab.blog::lang.category.slug
    +        span: right
    +        placeholder: rainlab.blog::lang.category.slug_placeholder
    +        preset: name
    +
    +    description:
    +        label: 'rainlab.blog::lang.category.description'
    +        size: large
    +        oc.commentPosition: ''
    +        span: full
    +        type: textarea
    diff --git a/plugins/rainlab/blog/models/post/columns.yaml b/plugins/rainlab/blog/models/post/columns.yaml
    new file mode 100644
    index 00000000..6567fcc6
    --- /dev/null
    +++ b/plugins/rainlab/blog/models/post/columns.yaml
    @@ -0,0 +1,36 @@
    +# ===================================
    +#  Column Definitions
    +# ===================================
    +
    +columns:
    +
    +    title:
    +        label: rainlab.blog::lang.post.title
    +        searchable: true
    +
    +    # author:
    +    #   label: Author
    +    #   relation: user
    +    #   select: login
    +    #   searchable: true
    +
    +    categories:
    +        label: rainlab.blog::lang.post.categories
    +        relation: categories
    +        select: name
    +        searchable: true
    +        sortable: false
    +
    +    created_at:
    +        label: rainlab.blog::lang.post.created
    +        type: date
    +        invisible: true
    +
    +    updated_at:
    +        label: rainlab.blog::lang.post.updated
    +        type: date
    +        invisible: true
    +
    +    published_at:
    +        label: rainlab.blog::lang.post.published
    +        type: date
    diff --git a/plugins/rainlab/blog/models/post/fields.yaml b/plugins/rainlab/blog/models/post/fields.yaml
    new file mode 100644
    index 00000000..ed7f5902
    --- /dev/null
    +++ b/plugins/rainlab/blog/models/post/fields.yaml
    @@ -0,0 +1,77 @@
    +# ===================================
    +#  Field Definitions
    +# ===================================
    +
    +fields:
    +    title:
    +        label: rainlab.blog::lang.post.title
    +        span: left
    +        placeholder: rainlab.blog::lang.post.title_placeholder
    +
    +    slug:
    +        label: rainlab.blog::lang.post.slug
    +        span: right
    +        placeholder: rainlab.blog::lang.post.slug_placeholder
    +        preset:
    +            field: title
    +            type: slug
    +
    +    toolbar:
    +        type: partial
    +        path: post_toolbar
    +        cssClass: collapse-visible
    +
    +secondaryTabs:
    +    stretch: true
    +    fields:
    +        content:
    +            tab: rainlab.blog::lang.post.tab_edit
    +            type: RainLab\Blog\FormWidgets\BlogMarkdown
    +            cssClass: field-slim blog-post-preview
    +            stretch: true
    +            mode: split
    +
    +        categories:
    +            tab: rainlab.blog::lang.post.tab_categories
    +            type: relation
    +            commentAbove: rainlab.blog::lang.post.categories_comment
    +            placeholder: rainlab.blog::lang.post.categories_placeholder
    +
    +        published:
    +            tab: rainlab.blog::lang.post.tab_manage
    +            label: rainlab.blog::lang.post.published
    +            span: left
    +            type: checkbox
    +
    +        user:
    +            tab: rainlab.blog::lang.post.tab_manage
    +            label: rainlab.blog::lang.post.published_by
    +            span: right
    +            type: dropdown
    +            emptyOption: rainlab.blog::lang.post.current_user
    +
    +        published_at:
    +            tab: rainlab.blog::lang.post.tab_manage
    +            label: rainlab.blog::lang.post.published_on
    +            span: left
    +            cssClass: checkbox-align
    +            type: datepicker
    +            mode: datetime
    +            trigger:
    +                action: enable
    +                field: published
    +                condition: checked
    +
    +        excerpt:
    +            tab: rainlab.blog::lang.post.tab_manage
    +            label: rainlab.blog::lang.post.excerpt
    +            type: textarea
    +            size: small
    +
    +        featured_images:
    +            tab: rainlab.blog::lang.post.tab_manage
    +            label: rainlab.blog::lang.post.featured_images
    +            type: fileupload
    +            mode: image
    +            imageWidth: 200
    +            imageHeight: 200
    diff --git a/plugins/rainlab/blog/models/post/scopes.yaml b/plugins/rainlab/blog/models/post/scopes.yaml
    new file mode 100644
    index 00000000..6ddfc4c3
    --- /dev/null
    +++ b/plugins/rainlab/blog/models/post/scopes.yaml
    @@ -0,0 +1,43 @@
    +# ===================================
    +# Filter Scope Definitions
    +# ===================================
    +
    +scopes:
    +
    +    category:
    +
    +        # Filter name
    +        label: rainlab.blog::lang.posts.filter_category
    +
    +        # Model Class name
    +        modelClass: RainLab\Blog\Models\Category
    +
    +        # Model attribute to display for the name
    +        nameFrom: name
    +
    +        # Apply query scope
    +        scope: FilterCategories
    +
    +    published:
    +
    +        # Filter name
    +        label: rainlab.blog::lang.posts.filter_published
    +
    +        # Filter type
    +        type: switch
    +
    +        # SQL Conditions
    +        conditions:
    +            - published <> '1'
    +            - published = '1'
    +
    +    created_at:
    +
    +        # Filter name
    +        label: rainlab.blog::lang.posts.filter_date
    +
    +        # Filter type
    +        type: daterange
    +
    +        # SQL Conditions
    +        conditions: created_at >= ':after' AND created_at <= ':before'
    diff --git a/plugins/rainlab/blog/models/postexport/columns.yaml b/plugins/rainlab/blog/models/postexport/columns.yaml
    new file mode 100644
    index 00000000..b500a8fa
    --- /dev/null
    +++ b/plugins/rainlab/blog/models/postexport/columns.yaml
    @@ -0,0 +1,19 @@
    +# ===================================
    +#  Column Definitions
    +# ===================================
    +
    +columns:
    +
    +    id: ID
    +    title: rainlab.blog::lang.post.title
    +    content: rainlab.blog::lang.post.content
    +    content_html: rainlab.blog::lang.post.content_html
    +    excerpt: rainlab.blog::lang.post.excerpt
    +    slug: rainlab.blog::lang.post.slug
    +    categories: rainlab.blog::lang.post.categories
    +    author_email: rainlab.blog::lang.post.author_email
    +    featured_image_urls: rainlab.blog::lang.post.featured_images
    +    created_at: rainlab.blog::lang.post.created_date
    +    updated_at: rainlab.blog::lang.post.updated_date
    +    published: rainlab.blog::lang.post.published
    +    published_at: rainlab.blog::lang.post.published_date
    diff --git a/plugins/rainlab/blog/models/postimport/columns.yaml b/plugins/rainlab/blog/models/postimport/columns.yaml
    new file mode 100644
    index 00000000..a2584ea5
    --- /dev/null
    +++ b/plugins/rainlab/blog/models/postimport/columns.yaml
    @@ -0,0 +1,17 @@
    +# ===================================
    +#  Column Definitions
    +# ===================================
    +
    +columns:
    +
    +    id: ID
    +    title: rainlab.blog::lang.post.title
    +    content: rainlab.blog::lang.post.content
    +    excerpt: rainlab.blog::lang.post.excerpt
    +    slug: rainlab.blog::lang.post.slug
    +    categories: rainlab.blog::lang.post.categories
    +    author_email: rainlab.blog::lang.post.author_email
    +    created_at: rainlab.blog::lang.post.created_date
    +    updated_at: rainlab.blog::lang.post.updated_date
    +    published: rainlab.blog::lang.post.published
    +    published_at: rainlab.blog::lang.post.published_date
    diff --git a/plugins/rainlab/blog/models/postimport/fields.yaml b/plugins/rainlab/blog/models/postimport/fields.yaml
    new file mode 100644
    index 00000000..3e7600ad
    --- /dev/null
    +++ b/plugins/rainlab/blog/models/postimport/fields.yaml
    @@ -0,0 +1,37 @@
    +# ===================================
    +#  Form Field Definitions
    +# ===================================
    +
    +fields:
    +
    +    update_existing:
    +        label: rainlab.blog::lang.import.update_existing_label
    +        comment: rainlab.blog::lang.import.update_existing_comment
    +        type: checkbox
    +        default: true
    +        span: left
    +
    +    auto_create_categories:
    +        label: rainlab.blog::lang.import.auto_create_categories_label
    +        comment: rainlab.blog::lang.import.auto_create_categories_comment
    +        type: checkbox
    +        default: true
    +        span: right
    +
    +    categories:
    +        label: rainlab.blog::lang.import.categories_label
    +        commentAbove: rainlab.blog::lang.import.categories_comment
    +        type: checkboxlist
    +        span: right
    +        cssClass: field-indent
    +        trigger:
    +            action: hide
    +            field: auto_create_categories
    +            condition: checked
    +
    +    default_author:
    +        label: rainlab.blog::lang.import.default_author_label
    +        comment: rainlab.blog::lang.import.default_author_comment
    +        type: dropdown
    +        placeholder: rainlab.blog::lang.import.default_author_placeholder
    +        span: left
    diff --git a/plugins/rainlab/blog/models/settings/fields.yaml b/plugins/rainlab/blog/models/settings/fields.yaml
    new file mode 100644
    index 00000000..3138a595
    --- /dev/null
    +++ b/plugins/rainlab/blog/models/settings/fields.yaml
    @@ -0,0 +1,17 @@
    +tabs:
    +    fields:
    +        show_all_posts:
    +            span: auto
    +            label: rainlab.blog::lang.blog.show_all_posts_label
    +            comment: rainlab.blog::lang.blog.show_all_posts_comment
    +            type: switch
    +            default: 1
    +            tab: rainlab.blog::lang.blog.tab_general
    +
    +        use_legacy_editor:
    +            span: auto
    +            label: rainlab.blog::lang.blog.use_legacy_editor_label
    +            comment: rainlab.blog::lang.blog.use_legacy_editor_comment
    +            type: switch
    +            default: 0
    +            tab: rainlab.blog::lang.blog.tab_general
    diff --git a/plugins/rainlab/blog/updates/categories_add_nested_fields.php b/plugins/rainlab/blog/updates/categories_add_nested_fields.php
    new file mode 100644
    index 00000000..c5a2bd21
    --- /dev/null
    +++ b/plugins/rainlab/blog/updates/categories_add_nested_fields.php
    @@ -0,0 +1,34 @@
    +integer('parent_id')->unsigned()->index()->nullable();
    +            $table->integer('nest_left')->nullable();
    +            $table->integer('nest_right')->nullable();
    +            $table->integer('nest_depth')->nullable();
    +        });
    +
    +        foreach (CategoryModel::all() as $category) {
    +            $category->setDefaultLeftAndRight();
    +            $category->save();
    +        }
    +    }
    +
    +    public function down()
    +    {
    +    }
    +
    +}
    \ No newline at end of file
    diff --git a/plugins/rainlab/blog/updates/create_categories_table.php b/plugins/rainlab/blog/updates/create_categories_table.php
    new file mode 100644
    index 00000000..988a9d44
    --- /dev/null
    +++ b/plugins/rainlab/blog/updates/create_categories_table.php
    @@ -0,0 +1,41 @@
    +engine = 'InnoDB';
    +            $table->increments('id');
    +            $table->string('name')->nullable();
    +            $table->string('slug')->nullable()->index();
    +            $table->string('code')->nullable();
    +            $table->text('description')->nullable();
    +            $table->integer('parent_id')->unsigned()->index()->nullable();
    +            $table->integer('nest_left')->nullable();
    +            $table->integer('nest_right')->nullable();
    +            $table->integer('nest_depth')->nullable();
    +            $table->timestamps();
    +        });
    +
    +        Schema::create('rainlab_blog_posts_categories', function($table)
    +        {
    +            $table->engine = 'InnoDB';
    +            $table->integer('post_id')->unsigned();
    +            $table->integer('category_id')->unsigned();
    +            $table->primary(['post_id', 'category_id']);
    +        });
    +    }
    +
    +    public function down()
    +    {
    +        Schema::dropIfExists('rainlab_blog_categories');
    +        Schema::dropIfExists('rainlab_blog_posts_categories');
    +    }
    +
    +}
    diff --git a/plugins/rainlab/blog/updates/create_posts_table.php b/plugins/rainlab/blog/updates/create_posts_table.php
    new file mode 100644
    index 00000000..8ec081b3
    --- /dev/null
    +++ b/plugins/rainlab/blog/updates/create_posts_table.php
    @@ -0,0 +1,32 @@
    +engine = 'InnoDB';
    +            $table->increments('id');
    +            $table->integer('user_id')->unsigned()->nullable()->index();
    +            $table->string('title')->nullable();
    +            $table->string('slug')->index();
    +            $table->text('excerpt')->nullable();
    +            $table->longText('content')->nullable();
    +            $table->longText('content_html')->nullable();
    +            $table->timestamp('published_at')->nullable();
    +            $table->boolean('published')->default(false);
    +            $table->timestamps();
    +        });
    +    }
    +
    +    public function down()
    +    {
    +        Schema::dropIfExists('rainlab_blog_posts');
    +    }
    +
    +}
    diff --git a/plugins/rainlab/blog/updates/posts_add_metadata.php b/plugins/rainlab/blog/updates/posts_add_metadata.php
    new file mode 100644
    index 00000000..ccd96bb3
    --- /dev/null
    +++ b/plugins/rainlab/blog/updates/posts_add_metadata.php
    @@ -0,0 +1,31 @@
    +mediumText('metadata')->nullable();
    +        });
    +    }
    +
    +    public function down()
    +    {
    +        if (Schema::hasColumn('rainlab_blog_posts', 'metadata')) {
    +            Schema::table('rainlab_blog_posts', function ($table) {
    +                $table->dropColumn('metadata');
    +            });
    +        }
    +    }
    +
    +}
    diff --git a/plugins/rainlab/blog/updates/seed_all_tables.php b/plugins/rainlab/blog/updates/seed_all_tables.php
    new file mode 100644
    index 00000000..7be9344f
    --- /dev/null
    +++ b/plugins/rainlab/blog/updates/seed_all_tables.php
    @@ -0,0 +1,34 @@
    + 'First blog post',
    +            'slug' => 'first-blog-post',
    +            'content' => '
    +This is your first ever **blog post**! It might be a good idea to update this post with some more relevant content.
    +
    +You can edit this content by selecting **Blog** from the administration back-end menu.
    +
    +*Enjoy the good times!*
    +            ',
    +            'excerpt' => 'The first ever blog post is here. It might be a good idea to update this post with some more relevant content.',
    +            'published_at' => Carbon::now(),
    +            'published' => true
    +        ]);
    +
    +        Category::create([
    +            'name' => trans('rainlab.blog::lang.categories.uncategorized'),
    +            'slug' => 'uncategorized',
    +        ]);
    +    }
    +
    +}
    diff --git a/plugins/rainlab/blog/updates/update_timestamp_nullable.php b/plugins/rainlab/blog/updates/update_timestamp_nullable.php
    new file mode 100644
    index 00000000..7de7961c
    --- /dev/null
    +++ b/plugins/rainlab/blog/updates/update_timestamp_nullable.php
    @@ -0,0 +1,20 @@
    + 'Notify',
    +            'description' => 'Notification Services',
    +            'author' => 'Alexey Bobkov, Samuel Georges',
    +            'icon' => 'icon-bullhorn'
    +        ];
    +    }
    +
    +    /**
    +     * registerSettings
    +     */
    +    public function registerSettings()
    +    {
    +        return [
    +            'notifications' => [
    +                'label' => 'Notification Rules',
    +                'description' => 'Manage the events and actions that trigger notifications.',
    +                'category' => SettingsManager::CATEGORY_NOTIFICATIONS,
    +                'icon' => 'icon-bullhorn',
    +                'url' => Backend::url('rainlab/notify/notifications'),
    +                'permissions' => ['rainlab.notify.manage_notifications'],
    +                'order' => 600,
    +                'keywords' => 'notify'
    +            ],
    +        ];
    +    }
    +
    +    /**
    +     * registerNotificationRules
    +     */
    +    public function registerNotificationRules()
    +    {
    +        return [
    +            'groups' => [],
    +            'events' => [],
    +            'actions' => [
    +                \RainLab\Notify\NotifyRules\SaveDatabaseAction::class,
    +                \RainLab\Notify\NotifyRules\SendMailTemplateAction::class,
    +            ],
    +            'conditions' => [
    +                \RainLab\Notify\NotifyRules\ExecutionContextCondition::class,
    +            ],
    +        ];
    +    }
    +
    +    /**
    +     * registerPermissions
    +     */
    +    public function registerPermissions()
    +    {
    +        return [
    +            'rainlab.notify.manage_notifications' => [
    +                'tab' => SettingsManager::CATEGORY_NOTIFICATIONS,
    +                'label' => 'Notifications management'
    +            ],
    +        ];
    +    }
    +}
    diff --git a/plugins/rainlab/notify/README.md b/plugins/rainlab/notify/README.md
    new file mode 100644
    index 00000000..d1edf67c
    --- /dev/null
    +++ b/plugins/rainlab/notify/README.md
    @@ -0,0 +1,400 @@
    +# Notification engine
    +
    +**Plugin is currently in Beta status. Proceed with caution.**
    +
    +Adds support for sending notifications across a variety of different channels, including mail, SMS and Slack.
    +
    +Notifications are managed in the back-end area by navigating to *Settings > Notification rules*.
    +
    +## Notification workflow
    +
    +When a notification fires, it uses the following workflow:
    +
    +1. Plugin registers associated actions, conditions and events using `registerNotificationRules`
    +1. A notification class is bound to a system event using `Notifier::bindEvent`
    +1. A system event is fired `Event::fire`
    +1. The parameters of the event are captured, along with any global context parameters
    +1. A command is pushed on the queue to process the notification `Queue::push`
    +1. The command finds all notification rules using the notification class and triggers them
    +1. The notification conditions are checked and only proceed if met
    +1. The notification actions are triggered
    +
    +Here is an example of a plugin registering notification rules. The `groups` definition will create containers that are used to better organise events. The `presets` definition specifies notification rules defined by the system.
    +
    +```php
    +public function registerNotificationRules()
    +{
    +    return [
    +        'events' => [
    +            \RainLab\User\NotifyRules\UserActivatedEvent::class,
    +        ],
    +        'actions' => [
    +            \RainLab\User\NotifyRules\SaveToDatabaseAction::class,
    +        ],
    +        'conditions' => [
    +            \RainLab\User\NotifyRules\UserAttributeCondition::class
    +        ],
    +        'groups' => [
    +            'user' => [
    +                'label' => 'User',
    +                'icon' => 'icon-user'
    +            ],
    +        ],
    +        'presets' => '$/rainlab/user/config/notify_presets.yaml',
    +    ];
    +}
    +```
    +
    +Here is an example of triggering a notification. The system event `rainlab.user.activate` is bound to the `UserActivatedEvent` class.
    +
    +```php
    +// Bind to a system event
    +\RainLab\Notify\Classes\Notifier::bindEvents([
    +    'rainlab.user.activate' => \RainLab\User\NotifyRules\UserActivatedEvent::class
    +]);
    +
    +// Fire the system event
    +Event::fire('rainlab.user.activate', [$this]);
    +```
    +
    +Here is an example of registering context parameters, which are available globally to all notifications.
    +
    +```php
    +\RainLab\Notify\Classes\Notifier::instance()->registerCallback(function($manager) {
    +    $manager->registerGlobalParams([
    +        'user' => Auth::getUser()
    +    ]);
    +});
    +```
    +
    +Here is an example of an event preset:
    +
    +```yaml
    +# ===================================
    +#  Event Presets
    +# ===================================
    +
    +welcome_email:
    +    name: Send welcome email to user
    +    event: RainLab\User\NotifyRules\UserRegisteredEvent
    +    items:
    +        - action: RainLab\Notify\NotifyRules\SendMailTemplateAction
    +          mail_template: rainlab.user::mail.welcome
    +          send_to_mode: user
    +    conditions:
    +        - condition: RainLab\Notify\NotifyRules\ExecutionContextCondition
    +          subcondition: environment
    +          operator: is
    +          value: dev
    +          condition_text: Application environment is dev
    +```
    +
    +## Creating Event classes
    +
    +An event class is responsible for preparing the parameters passed to the conditions and actions. The static method `makeParamsFromEvent` will take the arguments provided by the system event and convert them in to parameters.
    +
    +```php
    +class UserActivatedEvent extends \RainLab\Notify\Classes\EventBase
    +{
    +    /**
    +     * @var array Local conditions supported by this event.
    +        */
    +    public $conditions = [
    +        \RainLab\User\NotifyRules\UserAttributeCondition::class
    +    ];
    +
    +    /**
    +     * Returns information about this event, including name and description.
    +     */
    +    public function eventDetails()
    +    {
    +        return [
    +            'name'        => 'Activated',
    +            'description' => 'A user is activated',
    +            'group'       => 'user'
    +        ];
    +    }
    +
    +    /**
    +     * Defines the usable parameters provided by this class.
    +     */
    +    public function defineParams()
    +    {
    +        return [
    +            'name' => [
    +                'title' => 'Name',
    +                'label' => 'Name of the user',
    +            ],
    +            // ...
    +        ];
    +    }
    +
    +    public static function makeParamsFromEvent(array $args, $eventName = null)
    +    {
    +        return [
    +            'user' => array_get($args, 0)
    +        ];
    +    }
    +}
    +```
    +
    +## Creating Action classes
    +
    +Action classes define the final step in a notification and subsequently perform the notification itself. Some examples might be sending and email or writing to the database.
    +
    +```php
    +class SendMailTemplateAction extends \RainLab\Notify\Classes\ActionBase
    +{
    +    /**
    +     * Returns information about this event, including name and description.
    +     */
    +    public function actionDetails()
    +    {
    +        return [
    +            'name'        => 'Compose a mail message',
    +            'description' => 'Send a message to a recipient',
    +            'icon'        => 'icon-envelope'
    +        ];
    +    }
    +
    +    /**
    +     * Field configuration for the action.
    +     */
    +    public function defineFormFields()
    +    {
    +        return 'fields.yaml';
    +    }
    +
    +    public function getText()
    +    {
    +        $template = $this->host->template_name;
    +
    +        return 'Send a message using '.$template;
    +    }
    +
    +    /**
    +     * Triggers this action.
    +     * @param array $params
    +        * @return void
    +        */
    +    public function triggerAction($params)
    +    {
    +        $email = 'test@email.tld';
    +        $template = $this->host->template_name;
    +
    +        Mail::sendTo($email, $template, $params);
    +    }
    +}
    +```
    +
    +A form fields definition file is used to provide form fields when the action is established. These values are accessed from condition using the host model via the `$this->host` property.
    +
    +```yaml
    +# ===================================
    +#  Field Definitions
    +# ===================================
    +
    +fields:
    +
    +    template_name:
    +        label: Template name
    +        type: text
    +```
    +
    +An action may choose to provide no form fields by simply returning false from the `defineFormFields` method.
    +
    +```php
    +public function defineFormFields()
    +{
    +    return false;
    +}
    +```
    +
    +## Creating Condition classes
    +
    +A condition class should specify how it should appear in the user interface, providing a name, title and summary text. It also must declare an `isTrue` method for evaluating whether the condition is true or not.
    +
    +```php
    +class MyCondition extends \RainLab\Notify\Classes\ConditionBase
    +{
    +    /**
    +     * Return either ConditionBase::TYPE_ANY or ConditionBase::TYPE_LOCAL
    +     */
    +    public function getConditionType()
    +    {
    +        // If the condition should appear for all events
    +        return ConditionBase::TYPE_ANY;
    +
    +        // If the condition should appear only for some events
    +        return ConditionBase::TYPE_LOCAL;
    +    }
    +
    +    /**
    +     * Field configuration for the condition.
    +     */
    +    public function defineFormFields()
    +    {
    +        return 'fields.yaml';
    +    }
    +
    +    public function getName()
    +    {
    +        return 'My condition is checked';
    +    }
    +
    +    public function getTitle()
    +    {
    +        return 'My condition';
    +    }
    +
    +    public function getText()
    +    {
    +        $value = $this->host->mycondition;
    +
    +        return 'My condition is '.$value;
    +    }
    +
    +    /**
    +     * Checks whether the condition is TRUE for specified parameters
    +     * @param array $params
    +        * @return bool
    +        */
    +    public function isTrue(&$params)
    +    {
    +        return true;
    +    }
    +}
    +```
    +
    +A form fields definition file is used to provide form fields when the condition is established. These values are accessed from condition using the host model via the `$this->host` property.
    +
    +```yaml
    +# ===================================
    +#  Field Definitions
    +# ===================================
    +
    +fields:
    +
    +    mycondition:
    +        label: My condition
    +        type: dropdown
    +        options:
    +            true: True
    +            false: False
    +```
    +
    +## Model attribute condition classes
    +
    +Model attribute conditions are designed specially for applying conditions to sets of model attributes.
    +
    +```php
    +class UserAttributeCondition extends \RainLab\Notify\Classes\ModelAttributesConditionBase
    +{
    +    protected $modelClass = \RainLab\User\Models\User::class;
    +
    +    public function getGroupingTitle()
    +    {
    +        return 'User attribute';
    +    }
    +
    +    public function getTitle()
    +    {
    +        return 'User attribute';
    +    }
    +
    +    /**
    +     * Checks whether the condition is TRUE for specified parameters
    +     * @param array $params Specifies a list of parameters as an associative array.
    +        * @return bool
    +        */
    +    public function isTrue(&$params)
    +    {
    +        $hostObj = $this->host;
    +
    +        $attribute = $hostObj->subcondition;
    +
    +        if (!$user = array_get($params, 'user')) {
    +            throw new ApplicationException('Error evaluating the user attribute condition: the user object is not found in the condition parameters.');
    +        }
    +
    +        return parent::evalIsTrue($user);
    +    }
    +}
    +```
    +
    +An attributes definition file is used to specify which attributes should be included in the condition.
    +
    +```yaml
    +# ===================================
    +#  Condition Attribute Definitions
    +# ===================================
    +
    +attributes:
    +
    +    name:
    +        label: Name
    +
    +    email:
    +        label: Email address
    +
    +    country:
    +        label: Country
    +        type: relation
    +        relation:
    +            model: RainLab\Location\Models\Country
    +            label: Name
    +            nameFrom: name
    +            keyFrom: id
    +```
    +
    +## Save to database action
    +
    +There is a dedicated table in the database for storing events and their parameters. This table is accessed using the `RainLab\Notify\Models\Notification` model and can be referenced as a relation from your own models. In this example the `MyProject` model contains its own notification channel called `notifications`.
    +
    +```php
    +class MyProject extends Model
    +{
    +    // ...
    +
    +    public $morphMany = [
    +        'my_notifications' => [
    +            \RainLab\Notify\Models\Notification::class,
    +            'name' => 'notifiable'
    +        ]
    +    ];
    +}
    +```
    +
    +This channel should be registered with the `RainLab\Notify\NotifyRules\SaveDatabaseAction` so it appears as a related object when selecting the action.
    +
    +```php
    +SaveDatabaseAction::extend(function ($action) {
    +    $action->addTableDefinition([
    +        'label'    => 'Project activity',
    +        'class'    => MyProject::class,
    +        'relation' => 'my_notifications',
    +        'param'    => 'project'
    +    ]);
    +});
    +```
    +
    +The **label** is shown as the related object, the **class** references the model class, the **relation** refers to the relation name. The **param** defines the parameter name, passed to the triggering event.
    +
    +So essentially if you pass a `project` to the event parameters, or if `project` is a global parameter, a notification model is created with the parameters stored in the `data` attribute. Equivalent to the following code:
    +
    +```php
    +$myproject->my_notifications()->create([
    +    // ...
    +    'data' => $params
    +]);
    +```
    +
    +## Dynamically adding conditions to events
    +
    +Events can be extended to include new local conditions. Simply add the condition class to the event `$conditions` array property.
    +
    +```php
    +UserActivatedEvent::extend(function($event) {
    +    $event->conditions[] = \RainLab\UserPlus\NotifyRules\UserLocationAttributeCondition::class;
    +});
    +```
    diff --git a/plugins/rainlab/notify/classes/ActionBase.php b/plugins/rainlab/notify/classes/ActionBase.php
    new file mode 100644
    index 00000000..c64b09ee
    --- /dev/null
    +++ b/plugins/rainlab/notify/classes/ActionBase.php
    @@ -0,0 +1,168 @@
    + 'Action',
    +            'description' => 'Action Description',
    +            'icon' => 'icon-dot-circle-o'
    +        ];
    +    }
    +
    +    public function __construct($host = null)
    +    {
    +        /*
    +         * Paths
    +         */
    +        $this->viewPath = $this->configPath = $this->guessConfigPathFrom($this);
    +
    +        /*
    +         * Parse the config, if available
    +         */
    +        if ($formFields = $this->defineFormFields()) {
    +            $this->fieldConfig = $this->makeConfig($formFields);
    +        }
    +
    +        if (!$this->host = $host) {
    +            return;
    +        }
    +
    +        $this->boot($host);
    +    }
    +
    +    /**
    +     * Boot method called when the condition class is first loaded
    +     * with an existing model.
    +     * @return array
    +     */
    +    public function boot($host)
    +    {
    +        // Set default data
    +        if (!$host->exists) {
    +            $this->initConfigData($host);
    +        }
    +
    +        // Apply validation rules
    +        $host->rules = array_merge($host->rules, $this->defineValidationRules());
    +    }
    +
    +    public function triggerAction($params)
    +    {
    +    }
    +
    +    public function getTitle()
    +    {
    +        return $this->getActionName();
    +    }
    +
    +    public function getText()
    +    {
    +        return $this->getActionDescription();
    +    }
    +
    +    public function getActionName()
    +    {
    +        return array_get($this->actionDetails(), 'name');
    +    }
    +
    +    public function getActionDescription()
    +    {
    +        return array_get($this->actionDetails(), 'description');
    +    }
    +
    +    public function getActionIcon()
    +    {
    +        return array_get($this->actionDetails(), 'icon', 'icon-dot-circle-o');
    +    }
    +
    +    /**
    +     * Extra field configuration for the condition.
    +     */
    +    public function defineFormFields()
    +    {
    +        return 'fields.yaml';
    +    }
    +
    +    /**
    +     * Determines if this action uses form fields.
    +     * @return bool
    +     */
    +    public function hasFieldConfig()
    +    {
    +        return !!$this->fieldConfig;
    +    }
    +
    +    /**
    +     * Returns the field configuration used by this model.
    +     */
    +    public function getFieldConfig()
    +    {
    +        return $this->fieldConfig;
    +    }
    +
    +    /**
    +     * Initializes configuration data when the condition is first created.
    +     * @param  Model $host
    +     */
    +    public function initConfigData($host) {}
    +
    +    /**
    +     * Defines validation rules for the custom fields.
    +     * @return array
    +     */
    +    public function defineValidationRules()
    +    {
    +        return [];
    +    }
    +
    +    /**
    +     * Spins over types registered in plugin base class with `registerNotificationRules`.
    +     * @return array
    +     */
    +    public static function findActions()
    +    {
    +        $results = [];
    +        $bundles = PluginManager::instance()->getRegistrationMethodValues('registerNotificationRules');
    +
    +        foreach ($bundles as $plugin => $bundle) {
    +            foreach ((array) array_get($bundle, 'actions', []) as $conditionClass) {
    +                if (!class_exists($conditionClass)) {
    +                    continue;
    +                }
    +
    +                $obj = new $conditionClass;
    +                $results[$conditionClass] = $obj;
    +            }
    +        }
    +
    +        return $results;
    +    }
    +}
    diff --git a/plugins/rainlab/notify/classes/CompoundCondition.php b/plugins/rainlab/notify/classes/CompoundCondition.php
    new file mode 100644
    index 00000000..56c7a8be
    --- /dev/null
    +++ b/plugins/rainlab/notify/classes/CompoundCondition.php
    @@ -0,0 +1,178 @@
    +host->condition_type == 0
    +            ? __('ALL of subconditions should be') . ' '
    +            : __('ANY of subconditions should be') . ' ';
    +
    +        $result .= $this->host->condition == 'false' ? 'FALSE' : 'TRUE';
    +
    +        return $result;
    +    }
    +
    +    /**
    +     * getJoinText returns the text to use when joining two rules within.
    +     * @return string
    +     */
    +    public function getJoinText()
    +    {
    +        return $this->host->condition_type == 0 ? 'AND' : 'OR';
    +    }
    +
    +    /**
    +     * Returns a list of condition types (`ConditionBase::TYPE_*` constants)
    +     * that can be added to this compound condition
    +     */
    +    public function getAllowedSubtypes()
    +    {
    +        return [];
    +    }
    +
    +    /**
    +     * defineFormFields
    +     */
    +    public function defineFormFields()
    +    {
    +        return 'fields.yaml';
    +    }
    +
    +    public function initConfigData($host)
    +    {
    +        $host->condition_type = 0;
    +        $host->condition = 'true';
    +    }
    +
    +    public function getConditionOptions()
    +    {
    +        $options = [
    +            'true' => 'TRUE',
    +            'false' => 'FALSE'
    +        ];
    +
    +        return $options;
    +    }
    +
    +    public function getConditionTypeOptions()
    +    {
    +        $options = [
    +            '0' => 'ALL subconditions should meet the requirement',
    +            '1' => 'ANY subconditions should meet the requirement'
    +        ];
    +
    +        return $options;
    +    }
    +
    +    /**
    +     * getChildOptions
    +     */
    +    public function getChildOptions(array $options)
    +    {
    +        extract(array_merge([
    +            'extraRules' => [],
    +        ], $options));
    +
    +        $result = [
    +            'Compound condition' => CompoundCondition::class
    +        ];
    +
    +        $classes = $extraRules + self::findConditionsByType(ConditionBase::TYPE_ANY);
    +
    +        $result = $this->addClassesSubconditions($classes, $result);
    +
    +        return $result;
    +    }
    +
    +    protected function addClassesSubconditions($classes, $list)
    +    {
    +        foreach ($classes as $conditionClass => $obj) {
    +
    +            $subConditions = $obj->listSubconditions();
    +
    +            if ($subConditions) {
    +                $groupName = $obj->getGroupingTitle();
    +
    +                foreach ($subConditions as $name => $subcondition) {
    +                    if (!$groupName) {
    +                        $list[$name] = $conditionClass.':'.$subcondition;
    +                    }
    +                    else {
    +                        if (!array_key_exists($groupName, $list)) {
    +                            $list[$groupName] = [];
    +                        }
    +
    +                        $list[$groupName][$name] = $conditionClass.':'.$subcondition;
    +                    }
    +                }
    +            }
    +            else {
    +                $list[$obj->getName()] = $conditionClass;
    +            }
    +        }
    +
    +        return $list;
    +    }
    +
    +    /**
    +     * Checks whether the condition is TRUE for specified parameters.
    +     *
    +     * @param array $params
    +     * @return bool
    +     */
    +    public function isTrue(&$params)
    +    {
    +        $hostObj = $this->host;
    +
    +        $requiredConditionValue = $hostObj->condition == 'true' ? true : false;
    +
    +        foreach ($hostObj->children as $subcondition) {
    +            $subconditionResult = $subcondition->getConditionObject()->isTrue($params) ? true : false;
    +
    +            /*
    +             * All
    +             */
    +            if ($hostObj->condition_type == 0) {
    +                if ($subconditionResult !== $requiredConditionValue) {
    +                    return false;
    +                }
    +
    +            }
    +            /*
    +             * Any
    +             */
    +            else {
    +                if ($subconditionResult === $requiredConditionValue) {
    +                    return true;
    +                }
    +            }
    +        }
    +
    +        /*
    +         * All
    +         */
    +        if ($hostObj->condition_type == 0) {
    +            return true;
    +        }
    +
    +        return false;
    +    }
    +}
    diff --git a/plugins/rainlab/notify/classes/ConditionBase.php b/plugins/rainlab/notify/classes/ConditionBase.php
    new file mode 100644
    index 00000000..81cfaafc
    --- /dev/null
    +++ b/plugins/rainlab/notify/classes/ConditionBase.php
    @@ -0,0 +1,211 @@
    +viewPath = $this->configPath = $this->guessConfigPathFrom($this);
    +
    +        /*
    +         * Parse the config
    +         */
    +        $this->fieldConfig = $this->makeConfig($this->defineFormFields());
    +
    +        if (!$this->host = $host) {
    +            return;
    +        }
    +
    +        $this->boot($host);
    +    }
    +
    +    /**
    +     * Boot method called when the condition class is first loaded
    +     * with an existing model.
    +     * @return array
    +     */
    +    public function boot($host)
    +    {
    +        // Set default data
    +        if (!$host->exists) {
    +            $this->initConfigData($host);
    +        }
    +
    +        // Apply validation rules
    +        $host->rules = array_merge($host->rules, $this->defineValidationRules());
    +
    +        // Inject view paths to the controller through the Form widget
    +        $host->bindEvent('model.form.filterFields', function($form) {
    +            $form->getController()->addViewPath($this->getViewPaths());
    +        });
    +    }
    +
    +    /**
    +     * Extra field configuration for the condition.
    +     */
    +    public function defineFormFields()
    +    {
    +        return 'fields.yaml';
    +    }
    +
    +    /**
    +     * Initializes configuration data when the condition is first created.
    +     * @param  Model $host
    +     */
    +    public function initConfigData($host) {}
    +
    +    /**
    +     * Defines validation rules for the custom fields.
    +     * @return array
    +     */
    +    public function defineValidationRules()
    +    {
    +        return [];
    +    }
    +
    +    public function getText()
    +    {
    +        return 'Condition Text';
    +    }
    +
    +    /**
    +     * Returns a condition name for displaying in the condition selection drop-down menu
    +     */
    +    public function getName()
    +    {
    +        return 'Condition';
    +    }
    +
    +    /**
    +     * Returns a condition title for displaying in the condition settings form
    +     */
    +    public function getTitle()
    +    {
    +        return 'Condition';
    +    }
    +
    +    /**
    +     * This function should return one of the `ConditionBase::TYPE_*` constants
    +     * depending on a place where the condition is valid
    +     */
    +    public function getConditionType()
    +    {
    +        return ConditionBase::TYPE_ANY;
    +    }
    +
    +    public function listSubconditions()
    +    {
    +        return [];
    +    }
    +
    +    /**
    +     * Returns a title to use for grouping subconditions
    +     * in the Create Condition drop-down menu
    +     */
    +    public function getGroupingTitle()
    +    {
    +        return null;
    +    }
    +
    +    /**
    +     * Returns the field configuration used by this model.
    +     */
    +    public function getFieldConfig()
    +    {
    +        return $this->fieldConfig;
    +    }
    +
    +    /**
    +     * Spins over types registered in plugin base class with `registerNotificationRules`,
    +     * checks if the condition type matches and adds it to an array that is returned.
    +     *
    +     * @param string $type Use `self::TYPE_*` constants
    +     * @return array
    +     */
    +    public static function findConditionsByType($type)
    +    {
    +        $results = [];
    +        $bundles = PluginManager::instance()->getRegistrationMethodValues(static::$registrationMethod);
    +
    +        foreach ($bundles as $plugin => $bundle) {
    +            foreach ((array) array_get($bundle, 'conditions', []) as $conditionClass) {
    +                if (!class_exists($conditionClass)) {
    +                    continue;
    +                }
    +
    +                $obj = new $conditionClass;
    +                if ($obj->getConditionType() != $type) {
    +                    continue;
    +                }
    +
    +                $results[$conditionClass] = $obj;
    +            }
    +        }
    +
    +        return $results;
    +    }
    +
    +    public function setFormFields($fields)
    +    {
    +
    +    }
    +
    +    public function setCustomData()
    +    {
    +    }
    +
    +    public function onPreRender($controller, $widget)
    +    {
    +    }
    +
    +    /**
    +     * Checks whether the condition is TRUE for specified parameters
    +     * @param array $params
    +     * @return bool
    +     */
    +    public function isTrue(&$params)
    +    {
    +        return false;
    +    }
    +
    +    public function getChildOptions(array $options)
    +    {
    +        return [];
    +    }
    +}
    diff --git a/plugins/rainlab/notify/classes/EventBase.php b/plugins/rainlab/notify/classes/EventBase.php
    new file mode 100644
    index 00000000..db6241e0
    --- /dev/null
    +++ b/plugins/rainlab/notify/classes/EventBase.php
    @@ -0,0 +1,237 @@
    + 'Event',
    +            'description' => 'Event description',
    +            'group' => 'groupcode'
    +        ];
    +    }
    +
    +    public function __construct($host = null)
    +    {
    +        $this->host = $host;
    +    }
    +
    +    /**
    +     * Defines the parameters used by this class.
    +     * This method should be used as an override in the extended class.
    +     */
    +    public function defineParams()
    +    {
    +        return [];
    +    }
    +
    +    /**
    +     * Local conditions supported by this event.
    +     */
    +    public function defineConditions()
    +    {
    +        return $this->conditions;
    +    }
    +
    +    /**
    +     * Sets multiple params.
    +     * @param array $params
    +     * @return void
    +     */
    +    public function setParams($params)
    +    {
    +        $this->params = $params;
    +    }
    +
    +    /**
    +     * Returns all params.
    +     * @return array
    +     */
    +    public function getParams()
    +    {
    +        return $this->params;
    +    }
    +
    +    /**
    +     * Generates event parameters based on arguments from the triggering system event.
    +     * @param array $args
    +     * @param string $eventName
    +     * @return void
    +     */
    +    public static function makeParamsFromEvent(array $args, $eventName = null)
    +    {
    +    }
    +
    +    public function getEventName()
    +    {
    +        return Lang::get(array_get($this->eventDetails(), 'name'));
    +    }
    +
    +    public function getEventDescription()
    +    {
    +        return Lang::get(array_get($this->eventDetails(), 'description'));
    +    }
    +
    +    public function getEventGroup()
    +    {
    +        return array_get($this->eventDetails(), 'group');
    +    }
    +
    +    /**
    +     * Resolves an event or action identifier from a class name or object.
    +     * @param mixed Class name or object
    +     * @return string Identifier in format of vendor-plugin-class
    +     */
    +    public function getEventIdentifier()
    +    {
    +        $namespace = Str::normalizeClassName(get_called_class());
    +        if (strpos($namespace, '\\') === null) {
    +            return $namespace;
    +        }
    +
    +        $parts = explode('\\', $namespace);
    +        $class = array_pop($parts);
    +        $slice = array_slice($parts, 1, 2);
    +        $code = strtolower(implode('-', $slice) . '-' . $class);
    +
    +        return $code;
    +    }
    +
    +    /**
    +     * Spins over types registered in plugin base class with `registerNotificationRules`.
    +     * @return array
    +     */
    +    public static function findEvents()
    +    {
    +        $results = [];
    +        $bundles = PluginManager::instance()->getRegistrationMethodValues('registerNotificationRules');
    +
    +        foreach ($bundles as $plugin => $bundle) {
    +            foreach ((array) array_get($bundle, 'events', []) as $conditionClass) {
    +                if (!class_exists($conditionClass)) {
    +                    continue;
    +                }
    +
    +                $obj = new $conditionClass;
    +                $results[$conditionClass] = $obj;
    +            }
    +        }
    +
    +        return $results;
    +    }
    +
    +    public static function findEventGroups()
    +    {
    +        $results = [];
    +        $bundles = PluginManager::instance()->getRegistrationMethodValues('registerNotificationRules');
    +
    +        foreach ($bundles as $plugin => $bundle) {
    +            if ($groups = array_get($bundle, 'groups')) {
    +                $results += $groups;
    +            }
    +        }
    +
    +        return $results;
    +    }
    +
    +    public static function findEventsByGroup($group)
    +    {
    +        $results = [];
    +
    +        foreach (self::findEvents() as $conditionClass => $obj) {
    +            if ($obj->getEventGroup() != $group) {
    +                continue;
    +            }
    +
    +            $results[$conditionClass] = $obj;
    +        }
    +
    +        return $results;
    +    }
    +
    +    public static function findEventByIdentifier($identifier)
    +    {
    +        foreach (self::findEvents() as $class => $obj) {
    +            if ($obj->getEventIdentifier() == $identifier) {
    +                return $obj;
    +            }
    +        }
    +    }
    +
    +    /**
    +     * Spins over preset registered in plugin base class with `registerNotificationRules`.
    +     * @return array
    +     */
    +    public static function findEventPresets()
    +    {
    +        $results = [];
    +        $bundles = PluginManager::instance()->getRegistrationMethodValues('registerNotificationRules');
    +
    +        foreach ($bundles as $plugin => $bundle) {
    +            if (!$presets = array_get($bundle, 'presets')) {
    +                continue;
    +            }
    +
    +            if (!is_array($presets)) {
    +                $presets = Yaml::parse(File::get(File::symbolizePath($presets)));
    +            }
    +
    +            if ($presets && is_array($presets)) {
    +                $results += $presets;
    +            }
    +        }
    +
    +        return $results;
    +    }
    +
    +    public static function findEventPresetsByClass($className)
    +    {
    +        $results = [];
    +
    +        foreach (self::findEventPresets() as $code => $definition) {
    +            if (!$eventClass = array_get($definition, 'event')) {
    +                continue;
    +            }
    +
    +            if ($eventClass != $className) {
    +                continue;
    +            }
    +
    +            $results[$code] = $definition;
    +        }
    +
    +        return $results;
    +    }
    +}
    diff --git a/plugins/rainlab/notify/classes/EventParams.php b/plugins/rainlab/notify/classes/EventParams.php
    new file mode 100644
    index 00000000..cf175773
    --- /dev/null
    +++ b/plugins/rainlab/notify/classes/EventParams.php
    @@ -0,0 +1,59 @@
    +eventClass = $eventClass;
    +
    +        $this->params = $this->serializeParams($params);
    +    }
    +
    +    /**
    +     * Execute the job.
    +     *
    +     * @return void
    +     */
    +    public function handle()
    +    {
    +        $this->delete();
    +
    +        Notifier::instance()->fireEvent($this->eventClass, $this->unserializeParams());
    +    }
    +
    +    protected function serializeParams($params)
    +    {
    +        $result = [];
    +
    +        foreach ($params as $param => $value) {
    +            $result[$param] = $this->getSerializedPropertyValue($value);
    +        }
    +
    +        return $result;
    +    }
    +
    +    protected function unserializeParams()
    +    {
    +        $result = [];
    +
    +        foreach ($this->params as $param => $value) {
    +            $result[$param] = $this->getRestoredPropertyValue($value);
    +        }
    +
    +        return $result;
    +    }
    +}
    diff --git a/plugins/rainlab/notify/classes/ModelAttributesConditionBase.php b/plugins/rainlab/notify/classes/ModelAttributesConditionBase.php
    new file mode 100644
    index 00000000..b6183a6a
    --- /dev/null
    +++ b/plugins/rainlab/notify/classes/ModelAttributesConditionBase.php
    @@ -0,0 +1,743 @@
    + 'is',
    +        'is_not' => 'is not',
    +        'equals_or_greater' => 'equals or greater than',
    +        'equals_or_less' => 'equals or less than',
    +        'contains' => 'contains',
    +        'does_not_contain' => 'does not contain',
    +        'greater' => 'greater than',
    +        'less' => 'less than',
    +        'one_of' => 'is one of',
    +        'not_one_of' => 'is not one of'
    +    ];
    +
    +    protected $modelObj = null;
    +    protected $referenceInfo = null;
    +    protected $modelAttributes = null;
    +
    +    protected static $modelObjCache = [];
    +    protected static $attributeControlTypeCache = [];
    +
    +    public function __construct($host = null)
    +    {
    +        parent::__construct($host);
    +
    +        /*
    +         * This is used as a base class, so register view path from here too
    +         */
    +        $this->addViewPath($this->guessViewPathFrom(__CLASS__));
    +    }
    +
    +    public function initConfigData($host)
    +    {
    +        $host->operator = 'is';
    +    }
    +
    +    public function setCustomData()
    +    {
    +        $this->host->condition_control_type = $this->evalControlType();
    +    }
    +
    +    //
    +    // Definitions
    +    //
    +
    +    /**
    +     * This function should return one of the `ConditionBase::TYPE_*` constants
    +     * depending on a place where the condition is valid
    +     */
    +    public function getConditionType()
    +    {
    +        return ConditionBase::TYPE_LOCAL;
    +    }
    +
    +    public function defineModelAttributes($type = null)
    +    {
    +        return 'attributes.yaml';
    +    }
    +
    +    public function defineValidationRules()
    +    {
    +        return [
    +            'value' => 'required'
    +        ];
    +    }
    +
    +    public function defineFormFields()
    +    {
    +        return plugins_path('rainlab/notify/classes/modelattributesconditionbase/fields.yaml');
    +    }
    +
    +    //
    +    // Text helpers
    +    //
    +
    +    public function getText()
    +    {
    +        $host = $this->host;
    +        $attributes = $this->listModelAttributes();
    +
    +        if (isset($attributes[$host->subcondition])) {
    +            $result = $this->getConditionTextPrefix($host, $attributes);
    +        }
    +        else {
    +            $result = __('Unknown Attribute');
    +        }
    +
    +        $result .= ' '.array_get($this->operators, $host->operator, $host->operator).' ';
    +
    +        $controlType = $this->getValueControlType();
    +
    +        if ($controlType == 'text') {
    +            $result .= $host->value;
    +        }
    +        else {
    +            $textValue = $this->getCustomTextValue();
    +            if ($textValue !== false) {
    +                return $result.' '.$textValue;
    +            }
    +
    +            $referenceInfo = $this->prepareReferenceListInfo();
    +            $modelObj = $referenceInfo->referenceModel;
    +
    +            if (!count($referenceInfo->columns)) {
    +                return $result;
    +            }
    +
    +            if (!strlen($host->value)) {
    +                return $result .= '?';
    +            }
    +
    +            $visibleField = $referenceInfo->primaryColumn;
    +
    +            if ($controlType == 'dropdown') {
    +                $obj = $modelObj->where('id', $host->value)->first();
    +                if ($obj) {
    +                    $result .= e($obj->{$visibleField});
    +                }
    +            }
    +            else {
    +                $ids = explode(',', $host->value);
    +                foreach ($ids as &$id) {
    +                    $id = trim(e($id));
    +                }
    +
    +                $records = $modelObj
    +                    ->whereIn('id', $ids)
    +                    ->orderBy($visibleField)
    +                    ->get();
    +
    +                $recordNames = [];
    +                foreach ($records as $record) {
    +                    $recordNames[] = $record->{$visibleField};
    +                }
    +
    +                $result .= '('.implode(', ', $recordNames).')';
    +            }
    +        }
    +
    +        return $result;
    +    }
    +
    +    protected function getConditionTextPrefix($parametersHost, $attributes)
    +    {
    +        return $attributes[$parametersHost->subcondition];
    +    }
    +
    +    public function getCustomTextValue()
    +    {
    +        return false;
    +    }
    +
    +    //
    +    // Options
    +    //
    +
    +    /**
    +     * getSubconditionOptions
    +     */
    +    public function getSubconditionOptions()
    +    {
    +        return $this->listModelAttributes();
    +    }
    +
    +    /**
    +     * getOperatorOptions
    +     */
    +    public function getOperatorOptions()
    +    {
    +        $hostObj = $this->host;
    +        $options = [];
    +        $attribute = $hostObj->subcondition;
    +
    +        $currentOperatorValue = $hostObj->operator;
    +
    +        $model = $this->getModelObj();
    +        $definitions = $this->listModelAttributeInfo();
    +
    +        if (!isset($definitions[$attribute])) {
    +            $options = ['none' => 'Unknown attribute selected'];
    +        }
    +        else {
    +            $columnType = array_get($definitions[$attribute], 'type');
    +
    +            if ($columnType != ConditionBase::CAST_RELATION) {
    +                if ($columnType == ConditionBase::CAST_STRING) {
    +                    $options = [
    +                        'is' => 'is',
    +                        'is_not' => 'is not',
    +                        'contains' => 'contains',
    +                        'does_not_contain' => 'does not contain'
    +                    ];
    +                }
    +                else {
    +                    $options = [
    +                        'is' => 'is',
    +                        'is_not' => 'is not',
    +                        'equals_or_greater' => 'equals or greater than',
    +                        'equals_or_less' => 'equals or less than',
    +                        'greater' => 'greater than',
    +                        'less' => 'less than'
    +                    ];
    +                }
    +            }
    +            else {
    +                $options = [
    +                    'is' => 'is',
    +                    'is_not' => 'is not',
    +                    'one_of' => 'is one of',
    +                    'not_one_of' => 'is not one of'
    +                ];
    +            }
    +        }
    +
    +        if (!array_key_exists($currentOperatorValue, $options)) {
    +            $keys = array_keys($options);
    +            if (count($keys)) {
    +                $hostObj->operator = $options[$keys[0]];
    +            }
    +            else {
    +                $hostObj->operator = null;
    +            }
    +        }
    +
    +        return $options;
    +    }
    +
    +    public function getValueDropdownOptions()
    +    {
    +        $hostObj = $this->host;
    +        $attribute = $hostObj->subcondition;
    +        $definitions = $this->listModelAttributeInfo();
    +
    +        if (!isset($definitions[$attribute])) {
    +            return [];
    +        }
    +
    +        $columnType = array_get($definitions[$attribute], 'type');
    +
    +        if ($columnType != ConditionBase::CAST_RELATION) {
    +            return [];
    +        }
    +
    +        $referenceInfo = $this->prepareReferenceListInfo();
    +        $referenceModel = $referenceInfo->referenceModel;
    +        $nameFrom = $referenceInfo->primaryColumn;
    +        $keyFrom = $referenceInfo->primaryKey;
    +
    +        // Determine if the model uses a tree trait
    +        $treeTraits = [
    +            \October\Rain\Database\Traits\NestedTree::class,
    +            \October\Rain\Database\Traits\SimpleTree::class
    +        ];
    +        $usesTree = count(array_intersect($treeTraits, class_uses($referenceModel))) > 0;
    +
    +        $results = $referenceModel->get();
    +
    +        return $usesTree
    +            ? $results->listsNested($nameFrom, $keyFrom)
    +            : $results->lists($nameFrom, $keyFrom);
    +    }
    +
    +    //
    +    // Control type
    +    //
    +
    +    protected function evalControlType()
    +    {
    +        $hostObj = $this->host;
    +        $attribute = $hostObj->subcondition;
    +        $operator = $hostObj->operator;
    +
    +        $definitions = $this->listModelAttributeInfo();
    +
    +        if (!isset($definitions[$attribute])) {
    +            return 'text';
    +        }
    +
    +        $columnType = array_get($definitions[$attribute], 'type');
    +
    +        if ($columnType != ConditionBase::CAST_RELATION) {
    +            return 'text';
    +        }
    +        else {
    +            if ($operator == 'is' || $operator == 'is_not') {
    +                return 'dropdown';
    +            }
    +
    +            return 'multi_value';
    +        }
    +    }
    +
    +    public function getValueControlType()
    +    {
    +        if (App::runningInBackend()) {
    +            return $this->evalControlType();
    +        }
    +
    +        $hostObj = $this->host;
    +
    +        if ($controlType = $hostObj->condition_control_type) {
    +            return $controlType;
    +        }
    +
    +        $controlType = $this->evalControlType();
    +
    +        $this->getModelObj()
    +            ->where('id', $hostObj->id)
    +            ->update(['condition_control_type' => $controlType]);
    +
    +        return $hostObj->condition_control_type = $controlType;
    +    }
    +
    +    //
    +    // Attributes
    +    //
    +
    +    public function listSubconditions()
    +    {
    +        $attributes = $this->listModelAttributes();
    +
    +        $result = [];
    +
    +        foreach ($attributes as $name => $code) {
    +            $result[$code] = $name;
    +        }
    +
    +        return $result;
    +    }
    +
    +    /**
    +     * Returns the supported attributes by a condition as an array.
    +     * The key is the attribute and the value is the label.
    +     *
    +     * @return array
    +     */
    +    protected function listModelAttributes()
    +    {
    +        $attributeInfo = $this->listModelAttributeInfo();
    +
    +        foreach ($attributeInfo as $attribute => $info) {
    +            $attributes[$attribute] = array_get($info, 'label');
    +        }
    +
    +        asort($attributes);
    +
    +        return $attributes;
    +    }
    +
    +    protected function listModelAttributeInfo()
    +    {
    +        if ($this->modelAttributes) {
    +            return $this->modelAttributes;
    +        }
    +
    +        $config = $this->makeConfig($this->defineModelAttributes($this->getConditionType()));
    +
    +        $attributes = $config->attributes ?? [];
    +
    +        /*
    +         * Set defaults
    +         */
    +        foreach ($attributes as $attribute => $info) {
    +            if (!isset($info['type'])) {
    +                $attributes[$attribute]['type'] = 'string';
    +            }
    +        }
    +
    +        return $this->modelAttributes = $attributes;
    +    }
    +
    +    //
    +    // Relation based attributes
    +    //
    +
    +    public function getReferencePrimaryColumn($record)
    +    {
    +        $referenceInfo = $this->prepareReferenceListInfo();
    +
    +         return $record->{$referenceInfo->primaryColumn};
    +    }
    +
    +    public function listSelectedReferenceRecords()
    +    {
    +        $referenceInfo = $this->prepareReferenceListInfo();
    +        $model = $referenceInfo->referenceModel;
    +
    +        $value = $this->host->value;
    +        $keys = strlen($value) ? explode(',', $value) : [];
    +
    +        if (count($keys)) {
    +            $model = $model->whereIn('id', $keys);
    +        }
    +        else {
    +            $model = $model->whereRaw('id <> id');
    +        }
    +
    +        $orderField = $referenceInfo->primaryColumn;
    +
    +        return $model->orderBy($orderField)->get();
    +    }
    +
    +    public function prepareReferenceListInfo()
    +    {
    +        if (!is_null($this->referenceInfo)) {
    +            return $this->referenceInfo;
    +        }
    +
    +        $model = $this->getModelObj();
    +        $attribute = $this->host->subcondition;
    +        $definitions = $this->listModelAttributeInfo();
    +        $definition = array_get($definitions, $attribute);
    +
    +        $columns = array_get($definition, 'columns');
    +        $primaryColumn = array_get($definition, 'relation.nameFrom', 'name');
    +
    +        if ($model->hasRelation($attribute)) {
    +            $relationType = $model->getRelationType($attribute);
    +            $relationModel = $model->makeRelation($attribute);
    +            $relationObject = $model->{$attribute}();
    +
    +            // Some simpler relations can specify a custom local or foreign "other" key,
    +            // which can be detected and implemented here automagically.
    +            $primaryKey = in_array($relationType, ['hasMany', 'belongsTo', 'hasOne'])
    +                ? $relationObject->getOtherKey()
    +                : $relationModel->getKeyName();
    +        }
    +        elseif ($relationClass = array_get($definition, 'relation.model')) {
    +            $relationModel = new $relationClass;
    +            $primaryKey = array_get($definition, 'relation.keyFrom', 'id');
    +        }
    +        else {
    +            throw new SystemException(sprintf('Model %s does not contain a relation "%s"', get_class($model), $attribute));
    +        }
    +
    +        if (!$columns) {
    +            $columns = [
    +                $primaryColumn => [
    +                    'label' => array_get($definition, 'relation.label', '?'),
    +                    'searchable' => true
    +                ]
    +            ];
    +        }
    +
    +        $this->referenceInfo = [];
    +        $this->referenceInfo['referenceModel'] = $relationModel;
    +        $this->referenceInfo['primaryKey'] = $primaryKey;
    +        $this->referenceInfo['primaryColumn'] = $primaryColumn;
    +        $this->referenceInfo['columns'] = $columns;
    +
    +        return $this->referenceInfo = (object) $this->referenceInfo;
    +    }
    +
    +    public function onPreRender($controller, $widget)
    +    {
    +        $controlType = $this->getValueControlType();
    +
    +        if ($controlType != 'multi_value') {
    +            return;
    +        }
    +
    +        $selectionColumn = [
    +            '_select_record' => [
    +                'label' => '',
    +                'sortable' => false,
    +                'type' => 'partial',
    +                'width' => '10px',
    +                'path' => plugins_path('rainlab/notify/classes/modelattributesconditionbase/_column_select_record.htm')
    +            ]
    +        ];
    +
    +        $referenceInfo = $this->prepareReferenceListInfo();
    +        $filterModel = $referenceInfo->referenceModel;
    +        $filterColumns = $selectionColumn + $referenceInfo->columns;
    +
    +        /*
    +         * List widget
    +         */
    +        $config = $this->makeConfig();
    +        $config->columns = $filterColumns;
    +        $config->model = $filterModel;
    +        $config->alias = $widget->alias . 'List';
    +        $config->showSetup = false;
    +        $config->showCheckboxes = false;
    +        $config->recordsPerPage = 6;
    +        $listWidget = $controller->makeWidget('Backend\Widgets\Lists', $config);
    +
    +        /*
    +         * Search widget
    +         */
    +        $config = $this->makeConfig();
    +        $config->alias = $widget->alias . 'Search';
    +        $config->growable = false;
    +        $config->prompt = 'backend::lang.list.search_prompt';
    +        $searchWidget = $controller->makeWidget('Backend\Widgets\Search', $config);
    +        $searchWidget->cssClasses[] = 'condition-filter-search';
    +
    +        $listWidget->bindToController();
    +        $searchWidget->bindToController();
    +
    +        /*
    +         * Extend list query
    +         */
    +        $listWidget->bindEvent('list.extendQueryBefore', function ($query) use ($filterModel) {
    +            $this->prepareFilterQuery($query, $filterModel);
    +        });
    +
    +        /*
    +         * Link the Search Widget to the List Widget
    +         */
    +        $listWidget->setSearchTerm($searchWidget->getActiveTerm());
    +
    +        $searchWidget->bindEvent('search.submit', function () use (&$searchWidget, $listWidget) {
    +            $listWidget->setSearchTerm($searchWidget->getActiveTerm());
    +            return $listWidget->onRefresh();
    +        });
    +
    +        $controller->vars['listWidget'] = $listWidget;
    +        $controller->vars['searchWidget'] = $searchWidget;
    +        $controller->vars['filterHostModel'] = $this->host;
    +    }
    +
    +    public function prepareFilterQuery($query, $model)
    +    {
    +    }
    +
    +    //
    +    // Condition check
    +    //
    +
    +    /**
    +     * Checks whether the condition is TRUE for a specified model
    +     * @return bool
    +     */
    +    public function evalIsTrue($model, $customValue = '__no_eval_value__')
    +    {
    +        $hostObj = $this->host;
    +
    +        $operator = $hostObj->operator;
    +        $attribute = $hostObj->subcondition;
    +
    +        $conditionValue = $hostObj->value;
    +        $conditionValue = trim(mb_strtolower($conditionValue));
    +
    +        $controlType = $this->getValueControlType();
    +
    +        if ($controlType == 'text') {
    +            if ($customValue === self::NO_EVAL_VALUE) {
    +                $modelValue = trim(mb_strtolower($model->{$attribute}));
    +            }
    +            else {
    +                $modelValue = trim(mb_strtolower($customValue));
    +            }
    +
    +            if ($operator == 'is') {
    +                return $modelValue == $conditionValue;
    +            }
    +
    +            if ($operator == 'is_not') {
    +                return $modelValue != $conditionValue;
    +            }
    +
    +            if ($operator == 'contains') {
    +                return mb_strpos($modelValue, $conditionValue) !== false;
    +            }
    +
    +            if ($operator == 'does_not_contain') {
    +                return mb_strpos($modelValue, $conditionValue) === false;
    +            }
    +
    +            if ($operator == 'equals_or_greater') {
    +                return $modelValue >= $conditionValue;
    +            }
    +
    +            if ($operator == 'equals_or_less') {
    +                return $modelValue <= $conditionValue;
    +            }
    +
    +            if ($operator == 'greater') {
    +                return $modelValue > $conditionValue;
    +            }
    +
    +            if ($operator == 'less') {
    +                return $modelValue < $conditionValue;
    +            }
    +        }
    +
    +        if ($controlType == 'dropdown') {
    +            if ($customValue === self::NO_EVAL_VALUE) {
    +                $modelValue = $model->{$attribute};
    +            }
    +            else {
    +                $modelValue = $customValue;
    +            }
    +
    +            if ($operator == 'is') {
    +                if ($modelValue == null) {
    +                    return false;
    +                }
    +
    +                if ($modelValue instanceof EloquentModel) {
    +                    return $modelValue->getKey() == $conditionValue;
    +                }
    +
    +                if (
    +                    is_array($modelValue) &&
    +                    count($modelValue) == 1 &&
    +                    array_key_exists(0, $modelValue)
    +                ) {
    +                    return $modelValue[0] == $conditionValue;
    +                }
    +
    +                if ($modelValue instanceof EloquentCollection) {
    +                    if ($modelValue->count() != 1) {
    +                        return false;
    +                    }
    +
    +                    return $modelValue[0]->getKey() == $conditionValue;
    +                }
    +            }
    +
    +            if ($operator == 'is_not') {
    +                if ($modelValue == null) {
    +                    return true;
    +                }
    +
    +                if ($modelValue instanceof EloquentModel) {
    +                    return $modelValue->getKey() != $conditionValue;
    +                }
    +
    +                if (is_array($modelValue)) {
    +                    if (count($modelValue) != 1) {
    +                        return true;
    +                    }
    +
    +                    if (!array_key_exists(0, $modelValue)) {
    +                        return true;
    +                    }
    +
    +                    return $modelValue[0] != $conditionValue;
    +                }
    +
    +                if ($modelValue instanceof EloquentCollection) {
    +                    if (!$modelValue->count() || $modelValue->count() > 1) {
    +                        return true;
    +                    }
    +
    +                    return $modelValue->first()->getKey() != $conditionValue;
    +                }
    +            }
    +        }
    +
    +        if ($controlType == 'multi_value') {
    +            if ($customValue === self::NO_EVAL_VALUE) {
    +                $modelValue = $model->{$attribute};
    +            }
    +            else {
    +                $modelValue = $customValue;
    +            }
    +
    +            if (
    +                (!$modelValue instanceof EloquentCollection) &&
    +                (!$modelValue instanceof EloquentModel) &&
    +                !is_array($modelValue)
    +            ) {
    +                return false;
    +            }
    +
    +            if (strlen($conditionValue)) {
    +                $conditionValues = explode(',', $conditionValue);
    +                foreach ($conditionValues as &$value) {
    +                    $value = trim($value);
    +                }
    +            } else {
    +                $conditionValues = [];
    +            }
    +
    +            if ($modelValue instanceof EloquentCollection) {
    +                $modelKeys = array_keys($modelValue->lists('id', 'id'));
    +            }
    +            elseif ($modelValue instanceof EloquentModel) {
    +                $modelKeys = [$modelValue->getKey()];
    +            }
    +            else {
    +                $modelKeys = $modelValue;
    +            }
    +
    +            if ($operator == 'is') {
    +                $operator = 'one_of';
    +            }
    +            elseif ($operator == 'is_not') {
    +                $operator = 'not_one_of';
    +            }
    +
    +            if ($operator == 'one_of') {
    +                return count(array_intersect($conditionValues, $modelKeys)) ? true : false;
    +            }
    +
    +            if ($operator == 'not_one_of') {
    +                return count(array_intersect($conditionValues, $modelKeys)) ? false : true;
    +            }
    +        }
    +
    +        return false;
    +    }
    +
    +    //
    +    // Helpers
    +    //
    +
    +    public function getModelObj()
    +    {
    +        if ($this->modelObj === null) {
    +            if (array_key_exists($this->modelClass, self::$modelObjCache)) {
    +                $this->modelObj = self::$modelObjCache[$this->modelClass];
    +            }
    +            else {
    +                $this->modelObj = self::$modelObjCache[$this->modelClass] = new $this->modelClass;
    +            }
    +        }
    +
    +        return $this->modelObj;
    +    }
    +}
    diff --git a/plugins/rainlab/notify/classes/Notifier.php b/plugins/rainlab/notify/classes/Notifier.php
    new file mode 100644
    index 00000000..de0f520c
    --- /dev/null
    +++ b/plugins/rainlab/notify/classes/Notifier.php
    @@ -0,0 +1,134 @@
    +registerGlobalParams([...]);
    +     *     });
    +     *
    +     * @param callable $callback A callable function.
    +     */
    +    public function registerCallback(callable $callback)
    +    {
    +        $this->callbacks[] = $callback;
    +    }
    +
    +    /**
    +     * Helper to process callbacks once and once only.
    +     * @return void
    +     */
    +    protected function processCallbacks()
    +    {
    +        if ($this->registered) {
    +            return;
    +        }
    +
    +        foreach ($this->callbacks as $callback) {
    +            $callback($this);
    +        }
    +
    +        $this->registered = true;
    +    }
    +
    +    //
    +    // Event binding
    +    //
    +
    +    public static function bindEvents(array $events)
    +    {
    +        foreach ($events as $event => $class) {
    +            self::bindEvent($event, $class);
    +        }
    +    }
    +
    +    public static function bindEvent($systemEventName, $notifyEventClass)
    +    {
    +        Event::listen($systemEventName, function() use ($notifyEventClass, $systemEventName) {
    +            $params = $notifyEventClass::makeParamsFromEvent(func_get_args(), $systemEventName);
    +
    +            self::instance()->queueEvent($notifyEventClass, $params);
    +        });
    +    }
    +
    +    public function queueEvent($eventClass, array $params)
    +    {
    +        $params += $this->getContextVars();
    +
    +        // Use queue
    +        if (true) {
    +            Queue::push(new EventParams($eventClass, $params));
    +        }
    +        else {
    +            $this->fireEvent($eventClass, $params);
    +        }
    +    }
    +
    +    public function fireEvent($eventClass, array $params)
    +    {
    +        $models = NotificationRuleModel::listRulesForEvent($eventClass);
    +
    +        foreach ($models as $model) {
    +            $model->setParams($params);
    +            $model->triggerRule();
    +        }
    +    }
    +
    +    public function registerGlobalParams(array $params)
    +    {
    +        if (!$this->registeredGlobalParams) {
    +            $this->registeredGlobalParams = [];
    +        }
    +
    +        $this->registeredGlobalParams = $params + $this->registeredGlobalParams;
    +    }
    +
    +    public function getContextVars()
    +    {
    +        $this->processCallbacks();
    +
    +        $globals = $this->registeredGlobalParams ?: [];
    +
    +        return [
    +            'isBackend' => App::runningInBackend() ? 1 : 0,
    +            'isConsole' => App::runningInConsole() ? 1 : 0,
    +            'appLocale' => App::getLocale(),
    +            'sender'    => null // unsafe:BackendAuth::getUser()
    +        ] + $globals;
    +    }
    +}
    diff --git a/plugins/rainlab/notify/classes/ScheduledAction.php b/plugins/rainlab/notify/classes/ScheduledAction.php
    new file mode 100644
    index 00000000..bfde1327
    --- /dev/null
    +++ b/plugins/rainlab/notify/classes/ScheduledAction.php
    @@ -0,0 +1,63 @@
    +action = $action->id;
    +        $this->params = $this->serializeParams($params);
    +    }
    +
    +    /**
    +     * Execute the job.
    +     *
    +     * @return void
    +     */
    +    public function handle()
    +    {
    +        $this->delete();
    +
    +        if (! $actionClass = RuleAction::find($this->action)) {
    +            return traceLog('Error: Could not restore action #' . $this->action);
    +        }
    +
    +        $params = $this->unserializeParams();
    +        $actionClass->triggerAction($params, false);
    +    }
    +
    +    protected function serializeParams($params)
    +    {
    +        $result = [];
    +
    +        foreach ($params as $param => $value) {
    +            $result[$param] = $this->getSerializedPropertyValue($value);
    +        }
    +
    +        return $result;
    +    }
    +
    +    protected function unserializeParams()
    +    {
    +        $result = [];
    +
    +        foreach ($this->params as $param => $value) {
    +            $result[$param] = $this->getRestoredPropertyValue($value);
    +        }
    +
    +        return $result;
    +    }
    +}
    diff --git a/plugins/rainlab/notify/classes/compoundcondition/fields.yaml b/plugins/rainlab/notify/classes/compoundcondition/fields.yaml
    new file mode 100644
    index 00000000..551ae812
    --- /dev/null
    +++ b/plugins/rainlab/notify/classes/compoundcondition/fields.yaml
    @@ -0,0 +1,14 @@
    +# ===================================
    +#  Field Definitions
    +# ===================================
    +
    +fields:
    +    condition_type:
    +        label: Condition Type
    +        span: auto
    +        type: dropdown
    +
    +    condition:
    +        label: Required Value
    +        span: auto
    +        type: dropdown
    diff --git a/plugins/rainlab/notify/classes/modelattributesconditionbase/_column_select_record.htm b/plugins/rainlab/notify/classes/modelattributesconditionbase/_column_select_record.htm
    new file mode 100644
    index 00000000..a2187452
    --- /dev/null
    +++ b/plugins/rainlab/notify/classes/modelattributesconditionbase/_column_select_record.htm
    @@ -0,0 +1,10 @@
    +
    +    
    +        
    +    
    +
    diff --git a/plugins/rainlab/notify/classes/modelattributesconditionbase/_field_value.htm b/plugins/rainlab/notify/classes/modelattributesconditionbase/_field_value.htm
    new file mode 100644
    index 00000000..1ac013dd
    --- /dev/null
    +++ b/plugins/rainlab/notify/classes/modelattributesconditionbase/_field_value.htm
    @@ -0,0 +1,107 @@
    +subcondition;
    +    $controlType = $formModel->getValueControlType();
    +?>
    +
    +
    +    
    +
    +
    +    getValueDropdownOptions();
    +    ?>
    +    
    +
    +
    +    listSelectedReferenceRecords();
    +        $hasData = $selectedRecords->count();
    +    ?>
    +    
    + +
    + render() ?> + render() ?> +
    + + +
    +
    + + + + + + + + + + + +
    + + + + + + + getReferencePrimaryColumn($record)) ?> +
    +
    +
    + + + + + + + +
    + + diff --git a/plugins/rainlab/notify/classes/modelattributesconditionbase/fields.yaml b/plugins/rainlab/notify/classes/modelattributesconditionbase/fields.yaml new file mode 100644 index 00000000..d0c49815 --- /dev/null +++ b/plugins/rainlab/notify/classes/modelattributesconditionbase/fields.yaml @@ -0,0 +1,22 @@ +# =================================== +# Field Definitions +# =================================== + +fields: + + subcondition: + label: Attribute + span: auto + type: dropdown + + operator: + label: Operator + span: auto + type: dropdown + dependsOn: subcondition + + value: + label: Value + dependsOn: [subcondition, operator] + type: partial + path: field_value diff --git a/plugins/rainlab/notify/composer.json b/plugins/rainlab/notify/composer.json new file mode 100644 index 00000000..f55725b5 --- /dev/null +++ b/plugins/rainlab/notify/composer.json @@ -0,0 +1,25 @@ +{ + "name": "rainlab/notify-plugin", + "type": "october-plugin", + "description": "Notify plugin for October CMS", + "homepage": "https://octobercms.com/plugin/rainlab-pages", + "keywords": ["october", "octobercms", "pages"], + "license": "MIT", + "authors": [ + { + "name": "Alexey Bobkov", + "email": "aleksey.bobkov@gmail.com", + "role": "Co-founder" + }, + { + "name": "Samuel Georges", + "email": "daftspunky@gmail.com", + "role": "Co-founder" + } + ], + "require": { + "php": ">=5.5.9", + "composer/installers": "~1.0" + }, + "minimum-stability": "dev" +} diff --git a/plugins/rainlab/notify/controllers/Notifications.php b/plugins/rainlab/notify/controllers/Notifications.php new file mode 100644 index 00000000..20e65f60 --- /dev/null +++ b/plugins/rainlab/notify/controllers/Notifications.php @@ -0,0 +1,130 @@ +asExtension('ListController')->index(); + } + + public function create($eventAlias = null) + { + try { + if (!$eventAlias) { + throw new ApplicationException('Missing a rule code'); + } + + $this->eventAlias = $eventAlias; + $this->bodyClass = 'compact-container breadcrumb-fancy'; + $this->asExtension('FormController')->create(); + } + catch (Exception $ex) { + $this->handleError($ex); + } + } + + public function update($recordId = null, $context = null) + { + $this->bodyClass = 'compact-container breadcrumb-fancy'; + $this->asExtension('FormController')->update($recordId, $context); + } + + public function formExtendModel($model) + { + if (!$model->exists) { + $model->applyEventClass($this->getEventClass()); + $model->name = $model->getEventDescription(); + } + + return $model; + } + + // public function formBeforeSave($model) + // { + // $model->is_custom = 1; + // } + + public function index_onLoadRuleGroupForm() + { + try { + $groups = EventBase::findEventGroups(); + $this->vars['eventGroups'] = $groups; + } + catch (Exception $ex) { + $this->handleError($ex); + } + + return $this->makePartial('add_rule_group_form'); + } + + /** + * This handler requires the group code passed from `onLoadRuleGroupForm` + */ + public function index_onLoadRuleEventForm() + { + try { + if (!$code = post('code')) { + throw new ApplicationException('Missing event group code'); + } + + $events = EventBase::findEventsByGroup($code); + $this->vars['events'] = $events; + } + catch (Exception $ex) { + $this->handleError($ex); + } + + return $this->makePartial('add_rule_event_form'); + } + + protected function getEventClass() + { + $alias = post('event_alias', $this->eventAlias); + + if ($this->eventClass !== null) { + return $this->eventClass; + } + + if (!$event = EventBase::findEventByIdentifier($alias)) { + throw new ApplicationException('Unable to find event with alias: '. $alias); + } + + return $this->eventClass = get_class($event); + } +} diff --git a/plugins/rainlab/notify/controllers/notifications/_add_rule_event_form.htm b/plugins/rainlab/notify/controllers/notifications/_add_rule_event_form.htm new file mode 100644 index 00000000..8e939e22 --- /dev/null +++ b/plugins/rainlab/notify/controllers/notifications/_add_rule_event_form.htm @@ -0,0 +1,39 @@ + 'addRuleEventForm']) ?> + + + + + diff --git a/plugins/rainlab/notify/controllers/notifications/_add_rule_group_form.htm b/plugins/rainlab/notify/controllers/notifications/_add_rule_group_form.htm new file mode 100644 index 00000000..23507f4b --- /dev/null +++ b/plugins/rainlab/notify/controllers/notifications/_add_rule_group_form.htm @@ -0,0 +1,47 @@ + 'addRuleGroupForm']) ?> + + + + + diff --git a/plugins/rainlab/notify/controllers/notifications/_form_toolbar.htm b/plugins/rainlab/notify/controllers/notifications/_form_toolbar.htm new file mode 100644 index 00000000..f2277a3f --- /dev/null +++ b/plugins/rainlab/notify/controllers/notifications/_form_toolbar.htm @@ -0,0 +1,39 @@ +formGetContext() == 'create'; + $pageUrl = isset($pageUrl) ? $pageUrl : null; +?> + diff --git a/plugins/rainlab/notify/controllers/notifications/_list_toolbar.htm b/plugins/rainlab/notify/controllers/notifications/_list_toolbar.htm new file mode 100644 index 00000000..52030b6f --- /dev/null +++ b/plugins/rainlab/notify/controllers/notifications/_list_toolbar.htm @@ -0,0 +1,9 @@ + diff --git a/plugins/rainlab/notify/controllers/notifications/config_form.yaml b/plugins/rainlab/notify/controllers/notifications/config_form.yaml new file mode 100644 index 00000000..2e2bda06 --- /dev/null +++ b/plugins/rainlab/notify/controllers/notifications/config_form.yaml @@ -0,0 +1,16 @@ +# =================================== +# Form Behavior Config +# =================================== + +name: Notification Rule +form: $/rainlab/notify/models/notificationrule/fields.yaml +modelClass: RainLab\Notify\Models\NotificationRule +defaultRedirect: rainlab/notify/notifications + +create: + redirect: rainlab/notify/notifications/update/:id + redirectClose: rainlab/notify/notifications + +update: + redirect: rainlab/notify/notifications + redirectClose: rainlab/notify/notifications diff --git a/plugins/rainlab/notify/controllers/notifications/config_list.yaml b/plugins/rainlab/notify/controllers/notifications/config_list.yaml new file mode 100644 index 00000000..1d923a2d --- /dev/null +++ b/plugins/rainlab/notify/controllers/notifications/config_list.yaml @@ -0,0 +1,20 @@ +# =================================== +# List Behavior Config +# =================================== + +title: Notification Rules +list: $/rainlab/notify/models/notificationrule/columns.yaml +modelClass: RainLab\Notify\Models\NotificationRule +recordUrl: rainlab/notify/notifications/update/:id +noRecordsMessage: backend::lang.list.no_records +recordsPerPage: 30 +showSetup: true +showCheckboxes: true +defaultSort: + column: count + direction: desc + +toolbar: + buttons: list_toolbar + search: + prompt: backend::lang.list.search_prompt diff --git a/plugins/rainlab/notify/controllers/notifications/create.htm b/plugins/rainlab/notify/controllers/notifications/create.htm new file mode 100644 index 00000000..c1d8cf21 --- /dev/null +++ b/plugins/rainlab/notify/controllers/notifications/create.htm @@ -0,0 +1,31 @@ + +
      +
    • +
    • pageTitle)) ?>
    • +
    + + +fatalError): ?> + +
    + 'layout', + 'data-change-monitor' => 'true', + 'data-window-close-confirm' => e(__('Are you sure?')), + 'id' => 'post-form' + ]) ?> + + + formRender() ?> + + +
    + + + +
    +

    fatalError)) ?>

    +

    +
    + + diff --git a/plugins/rainlab/notify/controllers/notifications/index.htm b/plugins/rainlab/notify/controllers/notifications/index.htm new file mode 100644 index 00000000..766877d9 --- /dev/null +++ b/plugins/rainlab/notify/controllers/notifications/index.htm @@ -0,0 +1,2 @@ + +listRender() ?> diff --git a/plugins/rainlab/notify/controllers/notifications/update.htm b/plugins/rainlab/notify/controllers/notifications/update.htm new file mode 100644 index 00000000..ab376896 --- /dev/null +++ b/plugins/rainlab/notify/controllers/notifications/update.htm @@ -0,0 +1,30 @@ + +
      +
    • +
    • pageTitle)) ?>
    • +
    + + +fatalError): ?> + +
    + 'layout', + 'data-change-monitor' => 'true', + 'data-window-close-confirm' => e(__('Are you sure?')), + 'id' => 'post-form' + ]) ?> + + formRender() ?> + + +
    + + + +
    +

    fatalError)) ?>

    +

    +
    + + diff --git a/plugins/rainlab/notify/formwidgets/ActionBuilder.php b/plugins/rainlab/notify/formwidgets/ActionBuilder.php new file mode 100644 index 00000000..02d39338 --- /dev/null +++ b/plugins/rainlab/notify/formwidgets/ActionBuilder.php @@ -0,0 +1,388 @@ +fillFromConfig([ + // ]); + + if ($widget = $this->makeActionFormWidget()) { + $widget->bindToController(); + } + + if ($widget = $this->makeActionScheduleFormWidget()) { + $widget->bindToController(); + } + } + + /** + * {@inheritDoc} + */ + protected function loadAssets() + { + $this->addJs('js/actions.js', 'RainLab.Notify'); + $this->addCss('css/actions.css', 'RainLab.Notify'); + } + + /** + * {@inheritDoc} + */ + public function render() + { + $this->prepareVars(); + + return $this->makePartial('actions_container'); + } + + /** + * Prepares the list data + */ + public function prepareVars() + { + $this->vars['name'] = $this->getFieldName(); + $this->vars['formModel'] = $this->model; + $this->vars['actions'] = $this->getActions(); + $this->vars['actionFormWidget'] = $this->actionFormWidget; + $connection = config('queue.default'); + $this->vars['queueDriver'] = config("queue.connections.{$connection}.driver"); + $this->vars['actionScheduleFormWidget'] = $this->actionScheduleFormWidget; + $this->vars['availableTags'] = $this->getAvailableTags(); + } + + /** + * @inheritDoc + */ + public function getSaveValue($value) + { + $this->model->bindEvent('model.afterSave', function() { + $this->processSave(); + }); + + return FormField::NO_SAVE_DATA; + } + + protected function processSave() + { + $cache = $this->getCacheActionDataPayload(); + + foreach ($cache as $id => $data) { + $action = $this->findActionObj($id); + + if ($attributes = $this->getCacheActionAttributes($action)) { + $action->fill($attributes); + } + + $action->save(null, $this->sessionKey); + } + } + + // + // AJAX + // + + public function onLoadCreateActionForm() + { + try { + $actions = ActionBase::findActions(); + $this->vars['actions'] = $actions; + } + catch (Exception $ex) { + $this->handleError($ex); + } + + return $this->makePartial('create_action_form'); + } + + public function onSaveAction() + { + $this->restoreCacheActionDataPayload(); + + $action = $this->findActionObj(); + + $data = post('Action', []); + $schedule = post('ActionSchedule', []); + $action->fill($data); + $action->fill($schedule); + $action->validate(); + $action->action_text = $action->getActionObject()->getText(); + + $action->applyCustomData(); + + $this->setCacheActionData($action); + + return $this->renderActions($action); + } + + public function onLoadActionSetup() + { + try { + $action = $this->findActionObj(); + + $data = $this->getCacheActionAttributes($action); + + if (!is_null($this->actionFormWidget)) { + $this->actionFormWidget->setFormValues($data); + } + $this->actionScheduleFormWidget->setFormValues($data); + + $this->prepareVars(); + $this->vars['action'] = $action; + } + catch (Exception $ex) { + $this->handleError($ex); + } + + return $this->makePartial('action_settings_form'); + } + + public function onCreateAction() + { + if (!$className = post('action_class')) { + throw new ApplicationException('Please specify an action'); + } + + $this->restoreCacheActionDataPayload(); + + $newAction = $this->getRelationModel(); + $newAction->class_name = $className; + $newAction->save(); + + $this->model->rule_actions()->add($newAction, post('_session_key')); + + $this->vars['newActionId'] = $newAction->id; + + return $this->renderActions(); + } + + public function onDeleteAction() + { + $action = $this->findActionObj(); + + $this->model->rule_actions()->remove($action, post('_session_key')); + + return $this->renderActions(); + } + + public function onCancelActionSettings() + { + $action = $this->findActionObj(post('new_action_id')); + + $action->delete(); + + return $this->renderActions(); + } + + // + // Postback deferring + // + + public function getCacheActionAttributes($action) + { + return array_get($this->getCacheActionData($action), 'attributes'); + } + + public function getCacheActionTitle($action) + { + return array_get($this->getCacheActionData($action), 'title'); + } + + public function getCacheActionText($action) + { + return array_get($this->getCacheActionData($action), 'text'); + } + + public function getCacheActionData($action, $default = null) + { + $cache = post('action_data', []); + + if (is_array($cache) && array_key_exists($action->id, $cache)) { + return json_decode($cache[$action->id], true); + } + + if ($default === false) { + return null; + } + + return $this->makeCacheActionData($action); + } + + public function makeCacheActionData($action) + { + $data = [ + 'attributes' => $action->config_data, + 'title' => $action->getTitle(), + 'text' => $action->getText(), + ]; + + return $data; + } + + public function setCacheActionData($action) + { + $cache = post('action_data', []); + + $cache[$action->id] = json_encode($this->makeCacheActionData($action)); + + Request::merge([ + 'action_data' => $cache + ]); + } + + public function restoreCacheActionDataPayload() + { + Request::merge([ + 'action_data' => json_decode(post('current_action_data'), true) + ]); + } + + public function getCacheActionDataPayload() + { + return post('action_data', []); + } + + // + // Helpers + // + + protected function getAvailableTags() + { + $tags = []; + + if ($this->model->methodExists('defineParams')) { + $params = $this->model->defineParams(); + + foreach ($params as $param => $definition) { + $tags[$param] = array_get($definition, 'label'); + } + } + + return $tags; + } + + /** + * Updates the primary rule actions container + * @return array + */ + protected function renderActions() + { + $this->prepareVars(); + + return [ + '#'.$this->getId() => $this->makePartial('actions') + ]; + } + + protected function makeActionFormWidget() + { + if ($this->actionFormWidget !== null) { + return $this->actionFormWidget; + } + + if (!$model = $this->findActionObj(null, false)) { + return null; + } + + if (!$model->hasFieldConfig()) { + return null; + } + + $config = $model->getFieldConfig(); + $config->model = $model; + $config->alias = $this->alias . 'Form'; + $config->arrayName = 'Action'; + + $widget = $this->makeWidget('Backend\Widgets\Form', $config); + + return $this->actionFormWidget = $widget; + } + + protected function makeActionScheduleFormWidget() + { + if ($this->actionScheduleFormWidget !== null) { + return $this->actionScheduleFormWidget; + } + + if (!$model = $this->findActionObj(null, false)) { + return null; + } + + $config = $this->makeConfig('$/rainlab/notify/models/ruleaction/fields_schedule.yaml'); + $config->model = $model; + $config->alias = $this->alias . 'ScheduleForm'; + $config->arrayName = 'ActionSchedule'; + + $widget = $this->makeWidget('Backend\Widgets\Form', $config); + + return $this->actionScheduleFormWidget = $widget; + } + + protected function getActions() + { + if ($this->actionsCache !== false) { + return $this->actionsCache; + } + + $relationObject = $this->getRelationObject(); + $actions = $relationObject->withDeferred($this->sessionKey)->get(); + + return $this->actionsCache = $actions ?: null; + } + + protected function findActionObj($actionId = null, $throw = true) + { + $actionId = $actionId ? $actionId : post('current_action_id'); + + $action = null; + + if (strlen($actionId)) { + $action = $this->getRelationModel()->find($actionId); + } + + if ($throw && !$action) { + throw new ApplicationException('Action not found'); + } + + return $action; + } +} diff --git a/plugins/rainlab/notify/formwidgets/ConditionBuilder.php b/plugins/rainlab/notify/formwidgets/ConditionBuilder.php new file mode 100644 index 00000000..083716de --- /dev/null +++ b/plugins/rainlab/notify/formwidgets/ConditionBuilder.php @@ -0,0 +1,455 @@ +fillFromConfig([ + 'conditionsRuleType', + ]); + + if ($widget = $this->makeConditionFormWidget()) { + $widget->bindToController(); + } + + $this->initRootCondition(); + } + + /** + * {@inheritDoc} + */ + protected function loadAssets() + { + $this->addJs('js/conditions.js', 'RainLab.Notify'); + $this->addJs('js/conditions.multivalue.js', 'RainLab.Notify'); + $this->addCss('css/conditions.css', 'RainLab.Notify'); + } + + /** + * {@inheritDoc} + */ + public function render() + { + $this->prepareVars(); + + return $this->makePartial('conditions_container'); + } + + /** + * Prepares the list data + */ + public function prepareVars() + { + $this->vars['name'] = $this->getFieldName(); + $this->vars['rootCondition'] = $this->getConditionsRoot(); + $this->vars['conditionFormWidget'] = $this->conditionFormWidget; + } + + public function initRootCondition() + { + if ($this->getConditionsRoot()) { + return; + } + + $relationObject = $this->getRelationObject(); + + $rootRule = $this->getRelationModel(); + $rootRule->rule_host_type = $this->conditionsRuleType; + $rootRule->class_name = $rootRule->getRootConditionClass(); + $rootRule->save(); + + $relationObject->add($rootRule, $this->sessionKey); + + $this->conditionsRoot = $rootRule; + } + + public function isRootCondition($condition) + { + if ($root = $this->getConditionsRoot()) { + return $condition->id === $root->id; + } + + return false; + } + + public function getConditionsRoot() + { + if ($this->conditionsRoot !== false) { + return $this->conditionsRoot; + } + + $relationObject = $this->getRelationObject(); + $rootCondition = $relationObject->withDeferred($this->sessionKey)->first(); + + return $this->conditionsRoot = $rootCondition ?: null; + } + + /** + * @inheritDoc + */ + public function getSaveValue($value) + { + $this->model->bindEvent('model.afterSave', function() { + $this->processSave(); + }); + + return FormField::NO_SAVE_DATA; + } + + protected function processSave() + { + $cache = $this->getCacheConditionDataPayload(); + + foreach ($cache as $id => $data) { + $condition = $this->findConditionObj($id); + $attributes = $this->getCacheConditionAttributes($condition); + $condition->fill($attributes); + $condition->save(null, $this->sessionKey.'_'.$condition->id); + } + } + + // + // AJAX + // + + public function onLoadConditionSetup() + { + try { + $condition = $this->findConditionObj(); + + $this->prepareVars(); + + $this->vars['condition'] = $condition; + } + catch (Exception $ex) { + $this->handleError($ex); + } + + return $this->makePartial('condition_settings_form'); + } + + public function onLoadCreateChildCondition() + { + try { + $condition = $this->findConditionObj(); + + /* + * Look up parents + */ + $parents = [$condition->id]; + $parentsArray = post('condition_parent_id', []); + $currentId = $condition->id; + + while (array_key_exists($currentId, $parentsArray) && $parentsArray[$currentId]) { + $parents[] = $currentId = $parentsArray[$currentId]; + } + + /* + * Custom rules provided by model + */ + $extraRules = []; + if ($this->model->methodExists('getExtraConditionRules')) { + $extraRules = $this->model->getExtraConditionRules(); + } + + /* + * Look up conditions + */ + $options = $condition->getChildOptions([ + 'ruleType' => $this->conditionsRuleType, + 'parentIds' => $parents, + 'extraRules' => $extraRules + ]); + + $this->prepareVars(); + $this->vars['condition'] = $condition; + $this->vars['options'] = $options; + } + catch (Exception $ex) { + $this->handleError($ex); + } + + return $this->makePartial('create_child_form'); + } + + public function onSaveCondition() + { + $this->restoreCacheConditionDataPayload(); + + $condition = $this->findConditionObj(); + + $data = post('Condition', []); + $condition->fill($data); + $condition->validate(); + $condition->condition_text = $condition->getConditionObject()->getText(); + + $condition->applyCustomData(); + + $this->setCacheConditionData($condition); + + return $this->renderConditions($condition); + } + + public function onCreateCondition() + { + if (!$className = post('condition_class')) { + throw new ValidationException(['condition_class' => 'Please specify a condition']); + } + + $this->restoreCacheConditionDataPayload(); + + $subcondition = null; + + $parts = explode(':', $className); + if (count($parts) > 1) { + $subcondition = $parts[1]; + $className = $parts[0]; + } + + $parentCondition = $this->findConditionObj(); + + $newCondition = $this->getRelationModel(); + $newCondition->class_name = $className; + $newCondition->rule_host_type = $parentCondition->rule_host_type; + + if ($subcondition) { + $newCondition->subcondition = $subcondition; + } + + $newCondition->save(); + + $parentCondition->children()->add($newCondition, post('_session_key').'_'.$parentCondition->id); + + $this->vars['newConditionId'] = $newCondition->id; + + return $this->renderConditions($parentCondition); + } + + public function onDeleteCondition() + { + $parentCondition = null; + + $condition = $this->findConditionObj(); + + if ($parentId = $this->getParentIdFromCondition($condition)) { + $parentCondition = $this->findConditionObj($parentId); + + if ($parentCondition) { + $parentCondition->children()->remove($condition, post('_session_key').'_'.$parentCondition->id); + } + } + + return $this->renderConditions($parentCondition); + } + + public function onCancelConditionSettings() + { + $condition = $this->findConditionObj(post('new_condition_id')); + + $condition->delete(); + + return $this->renderConditions(); + } + + // + // Postback deferring + // + + public function getCacheConditionAttributes($condition) + { + return array_get($this->getCacheConditionData($condition), 'attributes'); + } + + public function getCacheConditionText($condition) + { + return array_get($this->getCacheConditionData($condition), 'text'); + } + + public function getCacheConditionJoinText($condition) + { + return array_get($this->getCacheConditionData($condition), 'joinText'); + } + + public function getCacheConditionData($condition, $default = null) + { + $cache = post('condition_data', []); + + if (is_array($cache) && array_key_exists($condition->id, $cache)) { + return json_decode($cache[$condition->id], true); + } + + if ($default === false) { + return null; + } + + return $this->makeCacheConditionData($condition); + } + + public function makeCacheConditionData($condition) + { + $data = [ + 'attributes' => $condition->config_data, + 'text' => $condition->getText() + ]; + + if ($condition->isCompound()) { + $data['joinText'] = $condition->getJoinText(); + } + + return $data; + } + + public function setCacheConditionData($condition) + { + $cache = post('condition_data', []); + + $cache[$condition->id] = json_encode($this->makeCacheConditionData($condition)); + + Request::merge([ + 'condition_data' => $cache + ]); + } + + public function restoreCacheConditionDataPayload() + { + Request::merge([ + 'condition_data' => json_decode(post('current_condition_data', []), true) + ]); + } + + public function getCacheConditionDataPayload() + { + return post('condition_data'); + } + + // + // Helpers + // + + public function getParentIdFromCondition($condition) + { + if ($parentId = post('current_parent_id')) { + return $parentId; + } + + $parentIds = post('condition_parent_id', []); + + if (isset($parentIds[$condition->id])) { + return $parentIds[$condition->id]; + } + } + + /** + * Updates the primary rule conditions container + * @return array + */ + protected function renderConditions($currentCondition = null) + { + if ($currentCondition && $this->isRootCondition($currentCondition)) { + $condition = $currentCondition; + } + else { + $condition = $this->getConditionsRoot(); + } + + return [ + '#'.$this->getId() => $this->makePartial('conditions', ['condition' => $condition]) + ]; + } + + protected function makeConditionFormWidget() + { + if ($this->conditionFormWidget !== null) { + return $this->conditionFormWidget; + } + + if (!$model = $this->findConditionObj(null, false)) { + return null; + } + + $config = $model->getFieldConfig(); + $config->model = $model; + $config->alias = $this->alias . 'Form'; + $config->arrayName = 'Condition'; + + $widget = $this->makeWidget('Backend\Widgets\Form', $config); + + /* + * Set form values based on postback or cached attributes + */ + if (!$data = post('Condition')) { + $data = $this->getCacheConditionAttributes($model); + } + + $widget->setFormValues($data); + + /* + * Allow conditions to register their own widgets + */ + $model->onPreRender($this->controller, $this); + + return $this->conditionFormWidget = $widget; + } + + protected function findConditionObj($conditionId = null, $throw = true) + { + $conditionId = $conditionId ? $conditionId : post('current_condition_id'); + + $condition = null; + + if (strlen($conditionId)) { + $condition = $this->getRelationModel()->find($conditionId); + } + + if ($throw && !$condition) { + throw new ApplicationException('Condition not found'); + } + + return $condition; + } +} diff --git a/plugins/rainlab/notify/formwidgets/actionbuilder/assets/css/actions.css b/plugins/rainlab/notify/formwidgets/actionbuilder/assets/css/actions.css new file mode 100644 index 00000000..817a9407 --- /dev/null +++ b/plugins/rainlab/notify/formwidgets/actionbuilder/assets/css/actions.css @@ -0,0 +1,227 @@ +.action-set { + margin-top: 10px; +} +.action-set ol { + margin: 0; + padding: 0; + list-style: none; +} +.action-set ol > li { + -webkit-transition: width 1s; + transition: width 1s; +} +.action-set ol > li > div { + font-size: 14px; + font-weight: normal; + position: relative; +} +.action-set ol > li > div > a, +.action-set ol > li > div div.content { + color: #2b3e50; + padding: 10px 45px 10px 91px; + display: block; + line-height: 150%; + text-decoration: none; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} +.action-set ol > li > div span.icon { + display: block; + position: absolute; + width: 22px; + height: 22px; + left: 20px; + top: 15px; + text-align: center; + font-weight: 500; + line-height: 22px; + color: #999; +} +.action-set ol > li > div span.icon > i { + line-height: 22px; + font-weight: normal; + font-size: 22px; + color: #999; +} +.action-set ol > li > div:not(.no-hover):hover, +.action-set ol > li > div.popover-highlight { + background-color: #4ea5e0 !important; +} +.action-set ol > li > div:not(.no-hover):hover span.icon > i, +.action-set ol > li > div.popover-highlight span.icon > i { + color: #fff !important; +} +.action-set ol > li > div:not(.no-hover):hover > a, +.action-set ol > li > div.popover-highlight > a { + color: #ffffff !important; +} +.action-set ol > li > div:not(.no-hover):hover:before, +.action-set ol > li > div.popover-highlight:before { + background-position: 0px -80px; +} +.action-set ol > li > div:not(.no-hover):hover:after, +.action-set ol > li > div.popover-highlight:after { + top: 0 !important; + bottom: 0 !important; +} +.action-set ol > li > div:not(.no-hover):hover span, +.action-set ol > li > div.popover-highlight span { + color: #ffffff !important; +} +.action-set ol > li > div:not(.no-hover):hover span.drag-handle, +.action-set ol > li > div.popover-highlight span.drag-handle { + cursor: move; + opacity: 1; + filter: alpha(opacity=100); +} +.action-set ol > li > div:not(.no-hover):active { + background-color: #3498db !important; +} +.action-set ol > li > div:not(.no-hover):active > a { + color: #ffffff !important; +} +.action-set ol > li > div span.icon:first-child { + font-size: 11px; + color: #ccc; +} +.action-set ol > li > div span.icon:first-child > i { + color: #ccc; + font-size: 15px; +} +.action-set ol > li > div span.icon:last-child { + left: 52px; +} +.action-set ol > li > div span.comment { + display: block; + font-weight: 400; + color: #95a5a6; + font-size: 13px; + margin-top: 2px; + overflow: hidden; + text-overflow: ellipsis; +} +.action-set ol > li > div > .subpanel { + right: 0; + top: 0; + height: 100%; + position: absolute; + z-index: 200; + padding: 0 20px; +} +.action-set ol > li > div > ul.submenu { + position: absolute; + right: 0; + top: 0; + padding: 0; + list-style: none; + z-index: 200; + height: 100%; + display: none; + margin: 0; + font-size: 0; +} +.action-set ol > li > div > ul.submenu li { + font-size: 12px; + height: 100%; + display: inline-block; + background: #2581b8; + border-right: 1px solid #328ec8; +} +.action-set ol > li > div > ul.submenu li p { + display: table; + height: 100%; + padding: 0; + margin: 0; +} +.action-set ol > li > div > ul.submenu li p a { + display: table-cell; + vertical-align: middle; + height: 100%; + padding: 0 20px; + font-size: 13px; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + color: #ffffff; + text-decoration: none; +} +.action-set ol > li > div > ul.submenu li p a i.control-icon { + font-size: 22px; +} +.action-set ol > li > div:hover > ul.submenu { + display: block; +} +.action-set ol > li > div .checkbox { + position: absolute; + top: -2px; + right: 0; +} +.action-set ol > li > div .checkbox label { + margin-right: 0; +} +.action-set ol > li > div .checkbox label:before { + border-color: #cccccc; +} +.action-set ol > li > div.popover-highlight { + background-color: #4ea5e0 !important; +} +.action-set ol > li > div.popover-highlight:before { + background-position: 0px -80px; +} +.action-set ol > li > div.popover-highlight > a { + color: #ffffff !important; + cursor: default; +} +.action-set ol > li > div.popover-highlight span { + color: #ffffff !important; +} +.action-set ol > li > div.popover-highlight > ul.submenu, +.action-set ol > li > div.popover-highlight > span.drag-handle { + display: none !important; +} +.action-set ol > li.active > div { + background: #dddddd; +} +.action-set ol > li.active > div:after { + position: absolute; + width: 4px; + left: 0; + top: -1px; + bottom: -1px; + background: #e67e22; + display: block; + content: ' '; +} +.action-set ol > li.active > div > span.comment, +.action-set ol > li.active > div > span.expand { + color: #8f8f8f; +} +.action-set a.menu-control { + display: block; + margin: 20px; + padding: 13px 15px; + border: dotted 2px #ebebeb; + color: #bdc3c7; + font-size: 12px; + font-weight: 600; + text-transform: uppercase; + border-radius: 5px; + vertical-align: middle; +} +.action-set a.menu-control:hover, +.action-set a.menu-control:focus { + text-decoration: none; + background-color: #4ea5e0; + color: #ffffff; + border: none; + padding: 15px 17px; +} +.action-set a.menu-control:active { + background: #3498db; + color: #ffffff; +} +.action-set a.menu-control i { + margin-right: 10px; + font-size: 14px; +} diff --git a/plugins/rainlab/notify/formwidgets/actionbuilder/assets/js/actions.js b/plugins/rainlab/notify/formwidgets/actionbuilder/assets/js/actions.js new file mode 100644 index 00000000..ea7a12d6 --- /dev/null +++ b/plugins/rainlab/notify/formwidgets/actionbuilder/assets/js/actions.js @@ -0,0 +1,156 @@ +/* + * Global helpers + */ +function showActionSettings(id) { + var $control = $('[data-action-id='+id+']').closest('[data-control="ruleactions"]') + + $control.ruleActions('onShowNewActionSettings', id) +} + +/* + * Plugin definition + */ ++function ($) { "use strict"; + var Base = $.oc.foundation.base, + BaseProto = Base.prototype + + var RuleActions = function (element, options) { + this.$el = $(element) + this.options = options || {} + + $.oc.foundation.controlUtils.markDisposable(element) + Base.call(this) + this.init() + } + + RuleActions.prototype = Object.create(BaseProto) + RuleActions.prototype.constructor = RuleActions + + RuleActions.prototype.init = function() { + this.$el.on('click', '[data-actions-settings]', this.proxy(this.onShowSettings)) + this.$el.on('click', '[data-actions-delete]', this.proxy(this.onDeleteAction)) + this.$el.one('dispose-control', this.proxy(this.dispose)) + } + + RuleActions.prototype.dispose = function() { + this.$el.off('click', '[data-actions-settings]', this.proxy(this.onShowSettings)) + this.$el.off('click', '[data-actions-delete]', this.proxy(this.onDeleteAction)) + this.$el.off('dispose-control', this.proxy(this.dispose)) + this.$el.removeData('oc.ruleActions') + + this.$el = null + + // In some cases options could contain callbacks, + // so it's better to clean them up too. + this.options = null + + BaseProto.dispose.call(this) + } + + RuleActions.prototype.onDeleteAction = function(event) { + var $el = $(event.target), + actionId = getActionIdFromElement($el) + + $el.request(this.options.deleteHandler, { + data: { current_action_id: actionId }, + confirm: 'Do you really want to delete this action?' + }) + } + + RuleActions.prototype.onShowNewActionSettings = function(actionId) { + var $el = $('[data-action-id='+actionId+']') + + var popup_size = 'giant'; + // Action does not use settings + if ($el.hasClass('no-form')) { + // Popup will contain scheduling option only + popup_size = 'medium' + } + + $el.popup({ + handler: this.options.settingsHandler, + extraData: { current_action_id: actionId }, + size: popup_size + }) + + // This will not fire on successful save because the target element + // is replaced by the time the popup loader has finished to call it + $el.one('hide.oc.popup', this.proxy(this.onCancelAction)) + } + + RuleActions.prototype.onCancelAction = function(event) { + var $el = $(event.target), + actionId = getActionIdFromElement($el) + + $el.request(this.options.cancelHandler, { + data: { new_action_id: actionId } + }) + + return false + } + + RuleActions.prototype.onShowSettings = function(event) { + var $el = $(event.target), + actionId = getActionIdFromElement($el) + + var popup_size = 'giant'; + // Action does not use settings + if ($el.closest('li.action-item').hasClass('no-form')) { + popup_size = 'medium' + } + + $el.popup({ + handler: this.options.settingsHandler, + extraData: { current_action_id: actionId }, + size: popup_size + }) + + return false + } + + function getActionIdFromElement($el) { + var $item = $el.closest('li.action-item') + + return $item.data('action-id') + } + + RuleActions.DEFAULTS = { + settingsHandler: null, + deleteHandler: null, + cancelHandler: null, + createHandler: null + } + + // PLUGIN DEFINITION + // ============================ + + var old = $.fn.ruleActions + + $.fn.ruleActions = function (option) { + var args = Array.prototype.slice.call(arguments, 1), items, result + + items = this.each(function () { + var $this = $(this) + var data = $this.data('oc.ruleActions') + var options = $.extend({}, RuleActions.DEFAULTS, $this.data(), typeof option == 'object' && option) + if (!data) $this.data('oc.ruleActions', (data = new RuleActions(this, options))) + if (typeof option == 'string') result = data[option].apply(data, args) + if (typeof result != 'undefined') return false + }) + + return result ? result : items + } + + $.fn.ruleActions.Constructor = RuleActions + + $.fn.ruleActions.noConflict = function () { + $.fn.ruleActions = old + return this + } + + // Add this only if required + $(document).render(function (){ + $('[data-control="ruleactions"]').ruleActions() + }) + +}(window.jQuery); diff --git a/plugins/rainlab/notify/formwidgets/actionbuilder/assets/less/actions.less b/plugins/rainlab/notify/formwidgets/actionbuilder/assets/less/actions.less new file mode 100644 index 00000000..16440bb2 --- /dev/null +++ b/plugins/rainlab/notify/formwidgets/actionbuilder/assets/less/actions.less @@ -0,0 +1,273 @@ +@import "../../../../../../../modules/backend/assets/less/core/boot.less"; + +// +// Actions +// -------------------------------------------------- + +@color-actions-item-bg: #ffffff; +@color-actions-item-title: #2b3e50; +@color-actions-item-comment: #95a5a6; +@color-actions-control: #bdc3c7; +@color-actions-hover-bg: @highlight-hover-bg; +@color-actions-hover-text: @highlight-hover-text; +@color-actions-active-bg: @highlight-active-bg; +@color-actions-active-text: @highlight-active-text; +@color-actions-item-active-comment: #8f8f8f; +@color-actions-submenu-text: #ffffff; +@color-actions-light-submenu-bg: #2581b8; +@color-actions-light-submenu-border: #328ec8; + +.action-set { + margin-top: 10px; + + ol { + margin: 0; + padding: 0; + list-style: none; + + > li { + .transition(width 1s); + + > div { + font-size: @font-size-base; + font-weight: normal; + position: relative; + + > a, div.content { + color: @color-actions-item-title; + padding: 10px 45px 10px 91px; + display: block; + line-height: 150%; + text-decoration: none; + .box-sizing(border-box); + } + + span.icon { + display: block; + position: absolute; + width: 22px; + height: 22px; + left: 20px; + top: 15px; + text-align: center; + font-weight: 500; + line-height: 22px; + color: #999; + + > i { + line-height: 22px; + font-weight: normal; + font-size: 22px; + color: #999; + } + } + + &:not(.no-hover):hover, &.popover-highlight { + background-color: @color-actions-hover-bg !important; + + span.icon > i { + color: #fff !important; + } + + > a { + color: @color-actions-hover-text !important; + } + + &:before { + background-position: 0px -80px; + } + + &:after { + top: 0 !important; + bottom: 0 !important; + } + + span { + color: @color-actions-hover-text !important; + + &.drag-handle { + cursor: move; + .opacity(1); + } + } + } + + &:not(.no-hover):active { + background-color: @color-actions-active-bg !important; + + > a { + color: @color-actions-active-text !important; + } + } + + span.icon:first-child { + font-size: 11px; + color: #ccc; + + > i { + color: #ccc; + font-size: 15px; + } + } + span.icon:last-child { + left: 52px; + } + + span.comment { + display: block; + font-weight: 400; + color: @color-actions-item-comment; + font-size: @font-size-base - 1; + margin-top: 2px; + overflow: hidden; + text-overflow: ellipsis; + } + + > .subpanel { + right: 0; + top: 0; + height: 100%; + position: absolute; + z-index: 200; + padding: 0 20px; + } + + > ul.submenu { + position: absolute; + right: 0; + top: 0; + padding: 0; + list-style: none; + z-index: 200; + height: 100%; + display: none; + margin: 0; + font-size: 0; + + li { + font-size: @font-size-base - 2; + height: 100%; + display: inline-block; + background: @color-actions-light-submenu-bg; + border-right: 1px solid @color-actions-light-submenu-border; + + p { + display: table; + height: 100%; + padding: 0; + margin: 0; + + a { + display: table-cell; + vertical-align: middle; + height: 100%; + padding: 0 20px; + font-size: @font-size-base - 1; + .box-sizing(border-box); + color: @color-actions-submenu-text; + text-decoration: none; + + i.control-icon { + font-size: 22px; + } + } + } + } + } + + &:hover { + > ul.submenu { + display: block; + } + } + + .checkbox { + position: absolute; + top: -2px; + right: 0; + + label { + margin-right: 0; + + &:before { + border-color: @color-filelist-cb-border; + } + } + } + + &.popover-highlight { + background-color: @color-actions-hover-bg !important; + + &:before { + background-position: 0px -80px; + } + + > a { + color: @color-actions-hover-text !important; + cursor: default; + } + + span { + color: @color-actions-hover-text !important; + } + + > ul.submenu, > span.drag-handle { + display: none !important; + } + } + } + + &.active { + > div { + background: @color-list-active; + + &:after { + position: absolute; + width: 4px; + left: 0; + top: -1px; + bottom: -1px; + background: @color-list-active-border; + display: block; + content: ' '; + } + + > span.comment, > span.expand { + color: @color-actions-item-active-comment; + } + } + } + } + } + + a.menu-control { + display: block; + margin: 20px; + padding: 13px 15px; + border: dotted 2px #ebebeb; + color: #bdc3c7; + font-size: @font-size-base - 2; + font-weight: 600; + text-transform: uppercase; + border-radius: 5px; + vertical-align: middle; + + &:hover, &:focus { + text-decoration: none; + background-color: @color-actions-hover-bg; + color: @color-actions-hover-text; + border: none; + padding: 15px 17px; + } + + &:active { + background: @color-actions-active-bg; + color: @color-actions-active-text; + } + + i { + margin-right: 10px; + font-size: 14px; + } + } +} \ No newline at end of file diff --git a/plugins/rainlab/notify/formwidgets/actionbuilder/partials/_action.htm b/plugins/rainlab/notify/formwidgets/actionbuilder/partials/_action.htm new file mode 100644 index 00000000..ecda30b0 --- /dev/null +++ b/plugins/rainlab/notify/formwidgets/actionbuilder/partials/_action.htm @@ -0,0 +1,29 @@ +
  • + + + +
  • diff --git a/plugins/rainlab/notify/formwidgets/actionbuilder/partials/_action_settings_form.htm b/plugins/rainlab/notify/formwidgets/actionbuilder/partials/_action_settings_form.htm new file mode 100644 index 00000000..5ca84f8b --- /dev/null +++ b/plugins/rainlab/notify/formwidgets/actionbuilder/partials/_action_settings_form.htm @@ -0,0 +1,77 @@ + 'propertyForm']) ?> + + + + fatalError): ?> + + + + + + + + + + + + + + + diff --git a/plugins/rainlab/notify/formwidgets/actionbuilder/partials/_actions.htm b/plugins/rainlab/notify/formwidgets/actionbuilder/partials/_actions.htm new file mode 100644 index 00000000..afabf2d7 --- /dev/null +++ b/plugins/rainlab/notify/formwidgets/actionbuilder/partials/_actions.htm @@ -0,0 +1,31 @@ +
    +
      +
    1. +
      +
      + + +
      +
      + getEventName()) ?> + getEventDescription()) ?> +
      +
      +
    2. + + makePartial('action', ['action' => $action]) ?> + +
    + + + + +
    + + + + diff --git a/plugins/rainlab/notify/formwidgets/actionbuilder/partials/_actions_container.htm b/plugins/rainlab/notify/formwidgets/actionbuilder/partials/_actions_container.htm new file mode 100644 index 00000000..3e50fe9d --- /dev/null +++ b/plugins/rainlab/notify/formwidgets/actionbuilder/partials/_actions_container.htm @@ -0,0 +1,14 @@ +
    +
    + makePartial('actions', ['actions' => $actions]) ?> +
    + + +
    diff --git a/plugins/rainlab/notify/formwidgets/actionbuilder/partials/_available_tags.htm b/plugins/rainlab/notify/formwidgets/actionbuilder/partials/_available_tags.htm new file mode 100644 index 00000000..324de43f --- /dev/null +++ b/plugins/rainlab/notify/formwidgets/actionbuilder/partials/_available_tags.htm @@ -0,0 +1,23 @@ + +
    +

    +
    +
      + $description): ?> +
    • + {{ }} +
    • + +
    +
    +
    +
    + +
    +
    +
    + diff --git a/plugins/rainlab/notify/formwidgets/actionbuilder/partials/_create_action_form.htm b/plugins/rainlab/notify/formwidgets/actionbuilder/partials/_create_action_form.htm new file mode 100644 index 00000000..01b04d99 --- /dev/null +++ b/plugins/rainlab/notify/formwidgets/actionbuilder/partials/_create_action_form.htm @@ -0,0 +1,46 @@ + 'addRuleActionForm']) ?> + + + + + + + + diff --git a/plugins/rainlab/notify/formwidgets/actionbuilder/partials/_schedule.htm b/plugins/rainlab/notify/formwidgets/actionbuilder/partials/_schedule.htm new file mode 100644 index 00000000..d011fdef --- /dev/null +++ b/plugins/rainlab/notify/formwidgets/actionbuilder/partials/_schedule.htm @@ -0,0 +1,22 @@ +
    +

    + + +

    +

    + render() ?> +

    +
    + + + + + + + + + + + +
    +
    diff --git a/plugins/rainlab/notify/formwidgets/conditionbuilder/assets/css/conditions.css b/plugins/rainlab/notify/formwidgets/conditionbuilder/assets/css/conditions.css new file mode 100644 index 00000000..84cba645 --- /dev/null +++ b/plugins/rainlab/notify/formwidgets/conditionbuilder/assets/css/conditions.css @@ -0,0 +1,227 @@ +ul.condition-set { + padding: 5px 0 0; + margin: 0; + list-style: none; +} +ul.condition-set ul { + padding: 0; + margin: 0; + list-style: none; +} +ul.condition-set li { + margin: 0; + padding: 0; +} +ul.condition-set .condition-item { + border-radius: 4px; +} +ul.condition-set .condition-item.is-root { + border-radius: 0; +} +ul.condition-set .condition-item.collapsed .compound-content { + border-left-color: transparent !important; +} +ul.condition-set .condition-item.collapsed .condition-set { + display: none; +} +ul.condition-set .condition-item.collapsed.is-inner .compound-add-item { + display: none; +} +ul.condition-set .condition-item .condition-delete { + position: absolute; + display: block; + width: 10px; + height: 10px; + right: 7px; + top: 7px; +} +ul.condition-set a.condition-collapse { + text-decoration: none; + display: block; + position: absolute; + right: 8px; + top: 2px; +} +ul.condition-set a.condition-collapse > i { + -webkit-transition: transform 0.3s; + transition: transform 0.3s; + width: 15px; + height: 15px; + color: #bdc3c7; + font-size: 12px; + display: block; +} +ul.condition-set a.condition-collapse:hover > i { + color: #999; +} +ul.condition-set li.collapsed a.condition-collapse > i { + -webkit-transform: scale(1, -1); + -ms-transform: scale(1, -1); + transform: scale(1, -1); +} +ul.condition-set .compound-join { + text-align: center; + padding: 3px 0 2px 0; +} +ul.condition-set .compound-join a { + color: #333333; + text-decoration: none; +} +ul.condition-set .compound-join a:hover { + text-decoration: underline; +} +ul.condition-set .condition-item.is-single { + padding: 8px 20px 9px 24px; + background: #fff; + border: 1px solid #eee; + position: relative; +} +ul.condition-set .condition-item.is-single:before { + font-family: FontAwesome; + font-weight: normal; + font-style: normal; + text-decoration: inherit; + -webkit-font-smoothing: antialiased; + *margin-right: .3em; + content: "\f126"; + line-height: 100%; + font-size: 16px; + color: #bdc3c7; + position: absolute; + width: 15px; + height: 15px; + left: 12px; + top: 11px; +} +ul.condition-set .condition-item.is-single .condition-text { + padding-left: 10px; +} +ul.condition-set .condition-item.is-single .condition-text a { + color: #333333; + text-decoration: none; +} +ul.condition-set .condition-item.is-single .condition-text a:hover { + text-decoration: underline; +} +ul.condition-set .condition-item.is-single .condition-text a span.operator { + font-weight: 500; +} +ul.condition-set .condition-item.is-single a.condition-delete { + right: 12px; + top: 9px; +} +ul.condition-set .condition-item.is-compound .compound-content { + position: relative; + border-left: 1px solid #dbdee0; + margin-left: 5px; +} +ul.condition-set .condition-item.is-compound .compound-content:before { + color: #bdc3c7; + font-family: FontAwesome; + font-weight: normal; + font-style: normal; + text-decoration: inherit; + -webkit-font-smoothing: antialiased; + *margin-right: .3em; + content: "\f111"; + font-size: 8px; + position: absolute; + left: -4px; + top: -2px; +} +ul.condition-set .condition-item.is-compound .compound-content h4 { + padding: 0 30px 5px 5px; + margin: 0; + position: relative; + top: -5px; + font-size: 13px; + line-height: 130%; + font-weight: 400; + color: #666; +} +ul.condition-set .condition-item.is-compound .compound-content h4 a.condition-text { + color: #333333; + padding-left: 10px; +} +ul.condition-set .condition-item.is-compound .compound-content h4 a.condition-text:hover { + text-decoration: underline; +} +ul.condition-set .condition-item.is-compound .compound-content .condition-set { + padding-left: 15px; +} +ul.condition-set .condition-item.is-compound .compound-add-item { + position: relative; + margin-top: 10px; + margin-left: 20px; + border: 2px dotted #e0e0e0; + border-radius: 5px; +} +ul.condition-set .condition-item.is-compound .compound-add-item:before { + color: #bdc3c7; + font-family: FontAwesome; + font-weight: normal; + font-style: normal; + text-decoration: inherit; + -webkit-font-smoothing: antialiased; + *margin-right: .3em; + content: "\f067"; + font-size: 16px; + position: absolute; + left: -23px; + top: -11px; +} +ul.condition-set .condition-item.is-compound .compound-add-item > a { + color: #bdc3c7; + text-align: center; + display: block; + text-decoration: none; + padding: 13px 15px; + text-transform: uppercase; + font-weight: 600; + font-size: 12px; +} +ul.condition-set .condition-item.is-compound .compound-add-item:hover, +ul.condition-set .condition-item.is-compound .compound-add-item:focus { + background-color: #4ea5e0; + border-color: #4ea5e0; +} +ul.condition-set .condition-item.is-compound .compound-add-item:hover:before, +ul.condition-set .condition-item.is-compound .compound-add-item:focus:before { + color: #999; +} +ul.condition-set .condition-item.is-compound .compound-add-item:hover > a, +ul.condition-set .condition-item.is-compound .compound-add-item:focus > a { + color: #ffffff; +} +ul.condition-set .condition-item.is-compound .compound-add-item:active { + background: #3498db; + border-color: #3498db; +} +ul.condition-set .condition-item.is-compound .compound-add-item:active > a { + color: #ffffff; +} +ul.condition-set .condition-item.is-compound.is-inner { + padding-right: 20px; +} +ul.condition-set .condition-item.is-compound.is-inner a.condition-delete-compound { + right: -7px; + top: 0; +} +.condition-multi-value .condition-filter-search { + background-position: right -81px !important; + border-top: none !important; + border-left: none !important; + border-right: none !important; + border-radius: 0; + padding-left: 20px; +} +.condition-multi-value .filter-list { + margin: 0 -20px; +} +.condition-multi-value .filter-list .list-footer { + padding-top: 0; + padding-bottom: 0; +} +.condition-multi-value .added-filter-list { + margin: 0 -20px; +} diff --git a/plugins/rainlab/notify/formwidgets/conditionbuilder/assets/js/conditions.js b/plugins/rainlab/notify/formwidgets/conditionbuilder/assets/js/conditions.js new file mode 100644 index 00000000..e4a7379c --- /dev/null +++ b/plugins/rainlab/notify/formwidgets/conditionbuilder/assets/js/conditions.js @@ -0,0 +1,184 @@ +/* + * Global helpers + */ +function showConditionSettings(id) { + var $control = $('[data-condition-id='+id+']').closest('[data-control="ruleconditions"]') + + $control.ruleConditions('onShowNewConditionSettings', id) +} + +/* + * Plugin definition + */ ++function ($) { "use strict"; + var Base = $.oc.foundation.base, + BaseProto = Base.prototype + + var RuleConditions = function (element, options) { + this.$el = $(element) + this.options = options || {} + + $.oc.foundation.controlUtils.markDisposable(element) + Base.call(this) + this.init() + } + + RuleConditions.prototype = Object.create(BaseProto) + RuleConditions.prototype.constructor = RuleConditions + + RuleConditions.prototype.init = function() { + this.$el.on('click', '[data-conditions-collapse]', this.proxy(this.onConditionsToggle)) + this.$el.on('click', '[data-conditions-settings]', this.proxy(this.onShowSettings)) + this.$el.on('click', '[data-conditions-create]', this.proxy(this.onCreateChildCondition)) + this.$el.on('click', '[data-conditions-delete]', this.proxy(this.onDeleteCondition)) + this.$el.one('dispose-control', this.proxy(this.dispose)) + } + + RuleConditions.prototype.dispose = function() { + this.$el.off('click', '[data-conditions-collapse]', this.proxy(this.onConditionsToggle)) + this.$el.off('click', '[data-conditions-settings]', this.proxy(this.onShowSettings)) + this.$el.off('click', '[data-conditions-create]', this.proxy(this.onCreateChildCondition)) + this.$el.off('click', '[data-conditions-delete]', this.proxy(this.onDeleteCondition)) + this.$el.off('dispose-control', this.proxy(this.dispose)) + this.$el.removeData('oc.ruleConditions') + + this.$el = null + + // In some cases options could contain callbacks, + // so it's better to clean them up too. + this.options = null + + BaseProto.dispose.call(this) + } + + RuleConditions.prototype.onDeleteCondition = function(event) { + var $el = $(event.target), + conditionId = getConditionIdFromElement($el) + + $el.request(this.options.deleteHandler, { + data: { current_condition_id: conditionId }, + confirm: 'Do you really want to delete this condition?' + }) + } + + RuleConditions.prototype.onCreateChildCondition = function(event) { + var $el = $(event.target), + conditionId = getConditionIdFromElement($el) + + $el.popup({ + handler: this.options.createHandler, + extraData: { current_condition_id: conditionId }, + size: 'large' + }) + + return false + } + + RuleConditions.prototype.onConditionsToggle = function(event) { + var $el = $(event.target), + $item = $el.closest('li'), + newStatusValue = $item.hasClass('collapsed') ? 0 : 1, + conditionId = getConditionIdFromElement($el) + + $el.request(this.options.collapseHandler, { + data: { + status: newStatusValue, + group: conditionId + } + }) + + if (newStatusValue) { + $el.parents('li:first').addClass('collapsed'); + } + else { + $el.parents('li:first').removeClass('collapsed'); + } + + return false + } + + RuleConditions.prototype.onShowNewConditionSettings = function(conditionId) { + var $el = $('[data-condition-id='+conditionId+']') + + $el.popup({ + handler: this.options.settingsHandler, + extraData: { current_condition_id: conditionId }, + size: 'large' + }) + + // This will not fire on successful save because the target element + // is replaced by the time the popup loader has finished to call it + $el.one('hide.oc.popup', this.proxy(this.onCancelCondition)) + } + + RuleConditions.prototype.onCancelCondition = function(event) { + var $el = $(event.target), + conditionId = getConditionIdFromElement($el) + + $el.request(this.options.cancelHandler, { + data: { new_condition_id: conditionId } + }) + + return false + } + + RuleConditions.prototype.onShowSettings = function(event) { + var $el = $(event.target), + conditionId = getConditionIdFromElement($el) + + $el.popup({ + handler: this.options.settingsHandler, + extraData: { current_condition_id: conditionId }, + size: 'large' + }) + + return false + } + + function getConditionIdFromElement($el) { + var $item = $el.closest('li.condition-item') + + return $item.data('condition-id') + } + + RuleConditions.DEFAULTS = { + collapseHandler: null, + settingsHandler: null, + deleteHandler: null, + cancelHandler: null, + createHandler: null + } + + // PLUGIN DEFINITION + // ============================ + + var old = $.fn.ruleConditions + + $.fn.ruleConditions = function (option) { + var args = Array.prototype.slice.call(arguments, 1), items, result + + items = this.each(function () { + var $this = $(this) + var data = $this.data('oc.ruleConditions') + var options = $.extend({}, RuleConditions.DEFAULTS, $this.data(), typeof option == 'object' && option) + if (!data) $this.data('oc.ruleConditions', (data = new RuleConditions(this, options))) + if (typeof option == 'string') result = data[option].apply(data, args) + if (typeof result != 'undefined') return false + }) + + return result ? result : items + } + + $.fn.ruleConditions.Constructor = RuleConditions + + $.fn.ruleConditions.noConflict = function () { + $.fn.ruleConditions = old + return this + } + + // Add this only if required + $(document).render(function (){ + $('[data-control="ruleconditions"]').ruleConditions() + }) + +}(window.jQuery); diff --git a/plugins/rainlab/notify/formwidgets/conditionbuilder/assets/js/conditions.multivalue.js b/plugins/rainlab/notify/formwidgets/conditionbuilder/assets/js/conditions.multivalue.js new file mode 100644 index 00000000..f87e2f8c --- /dev/null +++ b/plugins/rainlab/notify/formwidgets/conditionbuilder/assets/js/conditions.multivalue.js @@ -0,0 +1,139 @@ +/* + * Plugin definition + */ ++function ($) { "use strict"; + var Base = $.oc.foundation.base, + BaseProto = Base.prototype + + var ConditionMultiValue = function (element, options) { + this.$el = $(element) + this.options = options || {} + this.$template = $('[data-record-template]', this.$el) + this.$emptyTemplate = $('[data-empty-record-template]', this.$el) + this.$addedRecords = $('[data-added-records]', this.$el) + + $.oc.foundation.controlUtils.markDisposable(element) + Base.call(this) + + this.init() + this.recompileData() + } + + ConditionMultiValue.prototype = Object.create(BaseProto) + ConditionMultiValue.prototype.constructor = ConditionMultiValue + + ConditionMultiValue.prototype.init = function() { + this.$el.on('click', '[data-multivalue-add-record]', this.proxy(this.onAddRecord)) + this.$el.on('click', '[data-multivalue-remove-record]', this.proxy(this.onRemoveRecord)) + this.$el.one('dispose-control', this.proxy(this.dispose)) + } + + ConditionMultiValue.prototype.dispose = function() { + this.$el.off('click', '[data-multivalue-add-record]', this.proxy(this.onAddRecord)) + this.$el.off('click', '[data-multivalue-remove-record]', this.proxy(this.onRemoveRecord)) + this.$el.off('dispose-control', this.proxy(this.dispose)) + this.$el.removeData('oc.conditionMultiValue') + + this.$el = null + + // In some cases options could contain callbacks, + // so it's better to clean them up too. + this.options = null + + BaseProto.dispose.call(this) + } + + ConditionMultiValue.prototype.onAddRecord = function(event) { + var $el = $(event.target), + recordId = $el.closest('[data-record-key]').data('record-key'), + recordValue = $el.closest('a').data('record-value') + + if (!!$('[data-record-key='+recordId+']', this.$addedRecords).length) { + return + } + + this.$addedRecords.append(this.renderTemplate({ + key: recordId, + value: recordValue + })) + + this.recompileData() + } + + ConditionMultiValue.prototype.onRemoveRecord = function(event) { + var $el = $(event.target), + recordId = $el.closest('[data-record-key]').data('record-key') + + $('[data-record-key='+recordId+']', this.$addedRecords).remove() + + this.recompileData() + } + + ConditionMultiValue.prototype.recompileData = function(params) { + var $recordElements = $('[data-record-key]', this.$addedRecords), + hasData = !!$recordElements.length + + if (hasData) { + $('[data-no-record-data]', this.$addedRecords).remove() + } + else { + this.$addedRecords.append(this.renderEmptyTemplate()) + } + + if (this.options.dataLocker) { + var $locker = $(this.options.dataLocker), + selectedIds = [] + + $recordElements.each(function(key, record) { + selectedIds.push($(record).data('record-key')) + }) + + $locker.val(selectedIds.join(',')) + } + } + + ConditionMultiValue.prototype.renderTemplate = function(params) { + return Mustache.render(this.$template.html(), params) + } + + ConditionMultiValue.prototype.renderEmptyTemplate = function() { + return this.$emptyTemplate.html() + } + + ConditionMultiValue.DEFAULTS = { + dataLocker: null + } + + // PLUGIN DEFINITION + // ============================ + + var old = $.fn.conditionMultiValue + + $.fn.conditionMultiValue = function (option) { + var args = Array.prototype.slice.call(arguments, 1), items, result + + items = this.each(function () { + var $this = $(this) + var data = $this.data('oc.conditionMultiValue') + var options = $.extend({}, ConditionMultiValue.DEFAULTS, $this.data(), typeof option == 'object' && option) + if (!data) $this.data('oc.conditionMultiValue', (data = new ConditionMultiValue(this, options))) + if (typeof option == 'string') result = data[option].apply(data, args) + if (typeof result != 'undefined') return false + }) + + return result ? result : items + } + + $.fn.conditionMultiValue.Constructor = ConditionMultiValue + + $.fn.conditionMultiValue.noConflict = function () { + $.fn.conditionMultiValue = old + return this + } + + // Add this only if required + $(document).render(function (){ + $('[data-control="condition-multivalue"]').conditionMultiValue() + }) + +}(window.jQuery); diff --git a/plugins/rainlab/notify/formwidgets/conditionbuilder/assets/less/conditions.less b/plugins/rainlab/notify/formwidgets/conditionbuilder/assets/less/conditions.less new file mode 100644 index 00000000..e406b208 --- /dev/null +++ b/plugins/rainlab/notify/formwidgets/conditionbuilder/assets/less/conditions.less @@ -0,0 +1,286 @@ +@import "../../../../../../../modules/backend/assets/less/core/boot.less"; + +// +// Conditions +// -------------------------------------------------- + +ul.condition-set { + padding: 5px 0 0; + margin: 0; + list-style: none; + + ul { + padding: 0; + margin: 0; + list-style: none; + } + + li { + margin: 0; + padding: 0; + } + + .condition-item { + border-radius: 4px; + + &.is-root { + border-radius: 0; + } + + &.is-inner {} + + &.collapsed { + .compound-content { + border-left-color: transparent !important; + } + + .condition-set { + display: none; + } + + &.is-inner .compound-add-item { + display: none; + } + } + + .condition-delete { + position: absolute; + display: block; + width: 10px; + height: 10px; + right: 7px; + top: 7px; + } + } + + // + // Collapse + // + + a.condition-collapse { + text-decoration: none; + display: block; + position: absolute; + right: 8px; + top: 2px; + + > i { + .transition(~'transform 0.3s'); + width: 15px; + height: 15px; + color: #bdc3c7; + font-size: 12px; + display: block; + } + + &:hover { + > i { + color: #999; + } + } + } + + li.collapsed a.condition-collapse { + > i { + .transform(scale(1,-1)); + } + } + + // + // Joins + // + + .compound-join { + text-align: center; + padding: 3px 0 2px 0; + a { + color: #333333; + text-decoration: none; + &:hover { + text-decoration: underline; + } + } + } + + // + // Single + // + + .condition-item.is-single { + padding: 8px 20px 9px 24px; + background: #fff; + border: 1px solid #eee; + position: relative; + + &:before { + .icon(@code-fork); + line-height: 100%; + font-size: 16px; + color: #bdc3c7; + + position: absolute; + width: 15px; + height: 15px; + left: 12px; + top: 11px; + } + + .condition-text { + padding-left: 10px; + + a { + color: #333333; + text-decoration: none; + &:hover { + text-decoration: underline; + } + + span.operator { + font-weight: 500; + } + } + } + + a.condition-delete { + right: 12px; + top: 9px; + } + } + + // + // Compound + // + + .condition-item.is-compound { + .compound-content { + position: relative; + border-left: 1px solid #dbdee0; + margin-left: 5px; + + &:before { + color: #bdc3c7; + .icon(@circle); + font-size: 8px; + position: absolute; + left: -4px; + top: -2px; + } + + h4 { + padding: 0 30px 5px 5px; + margin: 0; + position: relative; + top: -5px; + font-size: 13px; + line-height: 130%; + font-weight: 400; + color: #666; + a.condition-text { + color: #333333; + padding-left: 10px; + &:hover { + text-decoration: underline; + } + } + } + + .condition-set { + padding-left: 15px; + } + } + + .condition-item.is-compound { + + } + + .compound-add-item { + position: relative; + margin-top: 10px; + margin-left: 20px; + border: 2px dotted #e0e0e0; + border-radius: 5px; + + &:before { + color: #bdc3c7; + .icon(@plus); + font-size: 16px; + position: absolute; + left: -23px; + top: -11px; + } + + > a { + color: #bdc3c7; + text-align: center; + display: block; + text-decoration: none; + padding: 13px 15px; + text-transform: uppercase; + font-weight: 600; + font-size: @font-size-base - 2; + } + + &:hover, &:focus { + background-color: @highlight-hover-bg; + border-color: @highlight-hover-bg; + + &:before { + color: #999; + } + + > a { + color: @highlight-hover-text; + } + } + + &:active { + background: @highlight-active-bg; + border-color: @highlight-active-bg; + > a { + color: @highlight-active-text; + } + } + } + } + + // + // Compound (Inner) + // + + .condition-item.is-compound.is-inner { + padding-right: 20px; + + a.condition-delete-compound { + right: -7px; + top: 0; + } + } +} + +// +// Multi value +// + +.condition-multi-value { + .condition-filter-search { + background-position: right -81px !important; + border-top: none !important; + border-left: none !important; + border-right: none !important; + border-radius: 0; + padding-left: 20px; + } + + .filter-list { + margin: 0 -20px; + + .list-footer { + padding-top: 0; + padding-bottom: 0; + } + } + + .added-filter-list { + margin: 0 -20px; + } +} diff --git a/plugins/rainlab/notify/formwidgets/conditionbuilder/partials/_condition.htm b/plugins/rainlab/notify/formwidgets/conditionbuilder/partials/_condition.htm new file mode 100644 index 00000000..95bb61d2 --- /dev/null +++ b/plugins/rainlab/notify/formwidgets/conditionbuilder/partials/_condition.htm @@ -0,0 +1,99 @@ +isCompound(); + $isRoot = $treeLevel == 0; + $collapsed = $this->getCollapseStatus($condition->id, false); +?> +
  • + + + +
    +

    + + + + + + getCacheConditionText($condition)) ?> + + + + + + +

    +
      + children()->withDeferred($this->sessionKey.'_'.$condition->id)->get(); + $lastIndex = $children->count() - 1; + ?> + $childCondition): ?> + makePartial('condition', [ + 'condition' => $childCondition, + 'treeLevel' => $treeLevel + 1, + 'parentCondition' => $condition + ]) ?> + +
    • + + getCacheConditionJoinText($condition)) ?> + +
    • + + +
    +
    + +
    + + + +
    + + + + + + + + + + + + + + + + +
  • diff --git a/plugins/rainlab/notify/formwidgets/conditionbuilder/partials/_condition_settings_form.htm b/plugins/rainlab/notify/formwidgets/conditionbuilder/partials/_condition_settings_form.htm new file mode 100644 index 00000000..77e0d843 --- /dev/null +++ b/plugins/rainlab/notify/formwidgets/conditionbuilder/partials/_condition_settings_form.htm @@ -0,0 +1,68 @@ + 'propertyForm']) ?> + + + + fatalError): ?> + + + + + + + + + + + + + + + + diff --git a/plugins/rainlab/notify/formwidgets/conditionbuilder/partials/_conditions.htm b/plugins/rainlab/notify/formwidgets/conditionbuilder/partials/_conditions.htm new file mode 100644 index 00000000..8cc8c4bb --- /dev/null +++ b/plugins/rainlab/notify/formwidgets/conditionbuilder/partials/_conditions.htm @@ -0,0 +1,10 @@ +
      + makePartial('condition', [ + 'condition' => $condition, + 'treeLevel' => 0 + ]) ?> +
    + + + + diff --git a/plugins/rainlab/notify/formwidgets/conditionbuilder/partials/_conditions_container.htm b/plugins/rainlab/notify/formwidgets/conditionbuilder/partials/_conditions_container.htm new file mode 100644 index 00000000..737b072a --- /dev/null +++ b/plugins/rainlab/notify/formwidgets/conditionbuilder/partials/_conditions_container.htm @@ -0,0 +1,15 @@ +
    +
    + makePartial('conditions', ['condition' => $rootCondition]) ?> +
    + + +
    diff --git a/plugins/rainlab/notify/formwidgets/conditionbuilder/partials/_create_child_form.htm b/plugins/rainlab/notify/formwidgets/conditionbuilder/partials/_create_child_form.htm new file mode 100644 index 00000000..3e9e8635 --- /dev/null +++ b/plugins/rainlab/notify/formwidgets/conditionbuilder/partials/_create_child_form.htm @@ -0,0 +1,67 @@ + 'propertyForm']) ?> + + + + fatalError): ?> + + + + + + + + + + + + + + + diff --git a/plugins/rainlab/notify/interfaces/Action.php b/plugins/rainlab/notify/interfaces/Action.php new file mode 100644 index 00000000..011cc13e --- /dev/null +++ b/plugins/rainlab/notify/interfaces/Action.php @@ -0,0 +1,31 @@ + [], + ]; + + /** + * Mark the notification as read. + * + * @return void + */ + public function markAsRead() + { + if (is_null($this->read_at)) { + $this->forceFill(['read_at' => $this->freshTimestamp()])->save(); + } + } + + /** + * Determine if a notification has been read. + * + * @return bool + */ + public function read() + { + return $this->read_at !== null; + } + + /** + * Determine if a notification has not been read. + * + * @return bool + */ + public function unread() + { + return $this->read_at === null; + } + + /** + * Get the entity's unread notifications. + */ + public function scopeApplyUnread($query) + { + return $query->whereNull('read_at'); + } + + /** + * Get the entity's read notifications. + */ + public function scopeApplyRead($query) + { + return $query->whereNotNull('read_at'); + } + + /** + * Get the parsed body of the announcement. + * + * @return string + */ + public function getParsedBodyAttribute() + { + return Markdown::parse($this->body); + } +} diff --git a/plugins/rainlab/notify/models/NotificationRule.php b/plugins/rainlab/notify/models/NotificationRule.php new file mode 100644 index 00000000..8db57a4d --- /dev/null +++ b/plugins/rainlab/notify/models/NotificationRule.php @@ -0,0 +1,289 @@ + 'required' + ]; + + /** + * @var array Relations + */ + public $hasMany = [ + 'rule_conditions' => [ + RuleCondition::class, + 'key' => 'rule_host_id', + 'conditions' => 'rule_parent_id is null', + 'delete' => true + ], + 'rule_actions' => [ + RuleAction::class, + 'key' => 'rule_host_id', + 'delete' => true + ], + ]; + + /** + * Kicks off this notification rule, fires the event to obtain its parameters, + * checks the rule conditions evaluate as true, then spins over each action. + */ + public function triggerRule() + { + $params = $this->getEventObject()->getParams(); + $rootCondition = $this->rule_conditions->first(); + + if ($rootCondition && !$rootCondition->getConditionObject()->isTrue($params)) { + return false; + } + + foreach ($this->rule_actions as $action) { + $action->setRelation('notification_rule', $this); + $action->triggerAction($params); + } + } + + /** + * Returns extra conditions provided by the event. + * @return array + */ + public function getExtraConditionRules() + { + $rules = []; + + $classes = $this->getEventObject()->defineConditions(); + + foreach ($classes as $class) { + $rules[$class] = new $class; + } + + return $rules; + } + + /** + * Extends this class with the event class + * @param string $class Class name + * @return boolean + */ + public function applyEventClass($class = null) + { + if (!$class) { + $class = $this->class_name; + } + + if (!$class) { + return false; + } + + if (!$this->isClassExtendedWith($class)) { + $this->extendClassWith($class); + } + + $this->class_name = $class; + $this->event_name = Lang::get(array_get($this->eventDetails(), 'name', 'Unknown')); + return true; + } + + /** + * Returns the event class extension object. + * @return \RainLab\Notify\Classes\NotificationEvent + */ + public function getEventObject() + { + $this->applyEventClass(); + + return $this->asExtension($this->getEventClass()); + } + + public function getEventClass() + { + return $this->class_name; + } + + // + // Events + // + + public function afterFetch() + { + $this->applyEventClass(); + } + + public function beforeValidate() + { + if (!$this->applyEventClass()) { + return; + } + } + + // + // Scopes + // + + public function scopeApplyEnabled($query) + { + return $query->where('is_enabled', true); + } + + public function scopeApplyClass($query, $class) + { + if (!is_string($class)) { + $class = get_class($class); + } + + return $query->where('class_name', $class); + } + + // + // Presets + // + + /** + * Returns an array of rule codes and descriptions. + * @return array + */ + public static function listRulesForEvent($eventClass) + { + $results = []; + + $dbRules = self::applyClass($eventClass)->get(); + $presets = (array) EventBase::findEventPresetsByClass($eventClass); + + foreach ($dbRules as $dbRule) { + if ($dbRule->code) { + unset($presets[$dbRule->code]); + } + + if ($dbRule->is_enabled) { + $results[] = $dbRule; + } + } + + foreach ($presets as $code => $preset) { + if ($newPreset = self::createFromPreset($code, $preset)) { + $results[] = $newPreset; + } + } + + return $results; + } + + /** + * Syncronise all file-based presets to the database. + * @return void + */ + public static function syncAll() + { + $presets = (array) EventBase::findEventPresets(); + $dbRules = self::where('code', '!=', '')->whereNotNull('code')->lists('is_custom', 'code'); + $newRules = array_diff_key($presets, $dbRules); + + /* + * Clean up non-customized templates + */ + foreach ($dbRules as $code => $isCustom) { + if ($isCustom) { + continue; + } + + if (!array_key_exists($code, $presets) && ($record = self::whereCode($code)->first())) { + $record->delete(); + } + } + + /* + * Create new rules + */ + foreach ($newRules as $code => $preset) { + self::createFromPreset($code, $preset); + } + } + + public static function createFromPreset($code, $preset) + { + $actions = array_get($preset, 'items'); + if (!$actions || !is_array($actions)) { + return; + } + + $newRule = new self; + $newRule->code = $code; + $newRule->is_enabled = 1; + $newRule->is_custom = 0; + $newRule->name = array_get($preset, 'name'); + $newRule->class_name = array_get($preset, 'event'); + $newRule->forceSave(); + + // Add the actions + foreach ($actions as $action) { + $params = array_except($action, 'action'); + + $newAction = new RuleAction; + $newAction->class_name = array_get($action, 'action'); + $newAction->notification_rule = $newRule; + $newAction->fill($params); + $newAction->forceSave(); + } + + // Add the conditions + $conditions = array_get($preset, 'conditions'); + if (!$conditions || !is_array($conditions)) { + return $newRule; + } + + // Create the root condition + $rootCondition = new RuleCondition(); + $rootCondition->rule_host_type = ConditionBase::TYPE_ANY; + $rootCondition->class_name = $rootCondition->getRootConditionClass(); + $rootCondition->notification_rule = $newRule; + $rootCondition->save(); + + // Add the sub conditions + foreach ($conditions as $condition) { + $params = array_except($condition, 'condition'); + $newCondition = new RuleCondition(); + $newCondition->class_name = array_get($condition, 'condition'); + $newCondition->parent = $rootCondition; + $newCondition->fill($params); + $newCondition->forceSave(); + } + + return $newRule; + } +} diff --git a/plugins/rainlab/notify/models/RuleAction.php b/plugins/rainlab/notify/models/RuleAction.php new file mode 100644 index 00000000..7d4b42c3 --- /dev/null +++ b/plugins/rainlab/notify/models/RuleAction.php @@ -0,0 +1,201 @@ + [NotificationRule::class, 'key' => 'rule_host_id'], + ]; + + public function triggerAction($params, $scheduled = true) + { + try { + $actionObject = $this->getActionObject(); + + // Check if action is muted in which case we don't proceed sending a notification + if (method_exists($actionObject, 'isMuted') && $actionObject->isMuted()) { + return; + } + + // Apply action schedule + if ($scheduled && $schedule = $this->getSchedule()) { + // We delay the execution using Queues. When dequeued + // ScheduledAction will call this triggerAction + // function with $scheduled=false + Queue::later($schedule, new ScheduledAction($this, $params)); + } + else { + // We trigger the action + $actionObject->triggerAction($params); + } + } + catch (Exception $ex) { + // We could log the error here, for now we should suppress + // any exceptions to let other actions proceed as normal + traceLog('Error with ' . $this->getActionClass()); + traceLog($ex); + } + } + + /** + * Extends this model with the action class + * @param string $class Class name + * @return boolean + */ + public function applyActionClass($class = null) + { + if (!$class) { + $class = $this->class_name; + } + + if (!$class) { + return false; + } + + if (!$this->isClassExtendedWith($class)) { + $this->extendClassWith($class); + } + + $this->class_name = $class; + return true; + } + + public function beforeSave() + { + $this->setCustomData(); + } + + public function afterSave() + { + // Make sure that this record is removed from the DB after being removed from a rule + $removedFromRule = $this->rule_host_id === null && $this->getOriginal('rule_host_id'); + if ($removedFromRule && !$this->notification_rule()->withDeferred(post('_session_key'))->exists()) { + $this->delete(); + } + } + + public function applyCustomData() + { + $this->setCustomData(); + $this->loadCustomData(); + } + + protected function loadCustomData() + { + $this->setRawAttributes((array) $this->getAttributes() + (array) $this->config_data, true); + } + + protected function setCustomData() + { + if (!$actionObj = $this->getActionObject()) { + throw new SystemException(sprintf('Unable to find action object [%s]', $this->getActionClass())); + } + + /* + * Spin over each field and add it to config_data + */ + + $metaAttributes = [ + 'action_text', + 'schedule_type', + 'schedule_delay', + 'schedule_delay_factor', + ]; + + $formAttributes = []; + $config = $actionObj->getFieldConfig(); + if (isset($config->fields)) { + $formAttributes = array_keys($config->fields); + } + + $fieldAttributes = array_merge($formAttributes, $metaAttributes); + + $dynamicAttributes = array_only($this->getAttributes(), $fieldAttributes); + $this->config_data = $dynamicAttributes; + + $this->setRawAttributes(array_except($this->getAttributes(), $fieldAttributes)); + } + + public function afterFetch() + { + $this->applyActionClass(); + $this->loadCustomData(); + } + + public function getText() + { + if (strlen($this->action_text)) { + return $this->action_text; + } + + if ($actionObj = $this->getActionObject()) { + return $actionObj->getText(); + } + } + + public function getSchedule() + { + if ($this->schedule_type !== 'delayed') { + return false; + } + + if (!is_numeric($this->schedule_delay) || !is_numeric($this->schedule_delay_factor)) { + return false; + } + + $delay = (int) $this->schedule_delay; + $delay_factor = (int) $this->schedule_delay_factor; + + return abs($delay * $delay_factor); + } + + public function getActionObject() + { + $this->applyActionClass(); + + return $this->asExtension($this->getActionClass()); + } + + public function getActionClass() + { + return $this->class_name; + } +} diff --git a/plugins/rainlab/notify/models/RuleCondition.php b/plugins/rainlab/notify/models/RuleCondition.php new file mode 100644 index 00000000..02827484 --- /dev/null +++ b/plugins/rainlab/notify/models/RuleCondition.php @@ -0,0 +1,172 @@ + [self::class, 'key' => 'rule_parent_id', 'delete' => true], + ]; + + public $belongsTo = [ + 'parent' => [self::class, 'key' => 'rule_parent_id'], + 'notification_rule' => [NotificationRule::class, 'key'=>'rule_host_id'] + ]; + + public function filterFields($fields, $context) + { + /* + * Let the condition contribute + */ + $this->getConditionObject()->setFormFields($fields); + } + + /** + * Extends this model with the condition class + * @param string $class Class name + * @return boolean + */ + public function applyConditionClass($class = null) + { + if (!$class) { + $class = $this->class_name; + } + + if (!$class) { + return false; + } + + if (!$this->isClassExtendedWith($class)) { + $this->extendClassWith($class); + } + + $this->class_name = $class; + return true; + } + + public function beforeSave() + { + $this->setCustomData(); + } + + public function applyCustomData() + { + $this->setCustomData(); + $this->loadCustomData(); + } + + protected function loadCustomData() + { + $this->setRawAttributes((array) $this->getAttributes() + (array) $this->config_data, true); + } + + protected function setCustomData() + { + /* + * Let the condition contribute + */ + $this->getConditionObject()->setCustomData($this); + + /* + * Spin over each field and add it to config_data + */ + $config = $this->getFieldConfig(); + + if (!isset($config->fields)) { + throw new SystemException('Condition class has no fields.'); + } + + $staticAttributes = ['condition_text']; + + $fieldAttributes = array_merge($staticAttributes, array_keys($config->fields)); + + $dynamicAttributes = array_only($this->getAttributes(), $fieldAttributes); + + $this->config_data = $dynamicAttributes; + + $this->setRawAttributes(array_except($this->getAttributes(), $fieldAttributes)); + } + + public function afterFetch() + { + $this->applyConditionClass(); + $this->loadCustomData(); + } + + public function afterSave() + { + // Make sure that this record is removed from the DB after being removed from a rule + $removedFromRule = $this->rule_parent_id === null && $this->getOriginal('rule_parent_id'); + if ($removedFromRule && !$this->notification_rule()->withDeferred(post('_session_key'))->exists()) { + $this->delete(); + } + } + + public function getText() + { + if (strlen($this->condition_text)) { + return $this->condition_text; + } + + if ($conditionObj = $this->getConditionObject()) { + return $conditionObj->getText(); + } + } + + public function isCompound() + { + return $this->getConditionObject() instanceof CompoundConditionInterface; + } + + public function getConditionObject() + { + $this->applyConditionClass(); + + return $this->asExtension($this->getConditionClass()); + } + + public function getConditionClass() + { + return $this->class_name; + } + + public function getRootConditionClass() + { + return CompoundCondition::class; + } +} diff --git a/plugins/rainlab/notify/models/notificationrule/columns.yaml b/plugins/rainlab/notify/models/notificationrule/columns.yaml new file mode 100644 index 00000000..2e298a31 --- /dev/null +++ b/plugins/rainlab/notify/models/notificationrule/columns.yaml @@ -0,0 +1,13 @@ +# =================================== +# Column Definitions +# =================================== + +columns: + + name: + label: Name + searchable: true + + code: + label: Code + searchable: true diff --git a/plugins/rainlab/notify/models/notificationrule/fields.yaml b/plugins/rainlab/notify/models/notificationrule/fields.yaml new file mode 100644 index 00000000..b0eec2ae --- /dev/null +++ b/plugins/rainlab/notify/models/notificationrule/fields.yaml @@ -0,0 +1,47 @@ +# =================================== +# Field Definitions +# =================================== + +fields: + name: + label: Name + placeholder: New notification rule name + attributes: + autofocus: 1 + + toolbar: + type: partial + path: form_toolbar + cssClass: collapse-visible + +tabs: + stretch: true + cssClass: master-area + paneCssClass: + 'Actions': 'pane-compact' + + fields: + rule_actions: + type: RainLab\Notify\FormWidgets\ActionBuilder + tab: Actions + + rule_conditions: + type: RainLab\Notify\FormWidgets\ConditionBuilder + tab: Conditions + + is_enabled: + label: Active + type: checkbox + tab: Settings + default: true + + description: + label: Description + type: textarea + size: tiny + tab: Settings + + code: + label: API Code + span: auto + tab: Settings diff --git a/plugins/rainlab/notify/models/ruleaction/columns.yaml b/plugins/rainlab/notify/models/ruleaction/columns.yaml new file mode 100644 index 00000000..b11160b4 --- /dev/null +++ b/plugins/rainlab/notify/models/ruleaction/columns.yaml @@ -0,0 +1,8 @@ +# =================================== +# List Column Definitions +# =================================== + +columns: + id: + label: ID + searchable: true diff --git a/plugins/rainlab/notify/models/ruleaction/fields.yaml b/plugins/rainlab/notify/models/ruleaction/fields.yaml new file mode 100644 index 00000000..c611f31c --- /dev/null +++ b/plugins/rainlab/notify/models/ruleaction/fields.yaml @@ -0,0 +1,8 @@ +# =================================== +# Form Field Definitions +# =================================== + +fields: + id: + label: ID + disabled: true diff --git a/plugins/rainlab/notify/models/ruleaction/fields_schedule.yaml b/plugins/rainlab/notify/models/ruleaction/fields_schedule.yaml new file mode 100644 index 00000000..bd3d80cb --- /dev/null +++ b/plugins/rainlab/notify/models/ruleaction/fields_schedule.yaml @@ -0,0 +1,30 @@ +# =================================== +# Form Field Definitions +# =================================== + +fields: + schedule_type: + type: dropdown + options: + disabled: None + delayed: Delay execution + schedule_delay: + type: number + span: left + trigger: + action: show + field: schedule_type + condition: value[delayed] + schedule_delay_factor: + type: dropdown + span: right + options: + 1: Seconds + 60: Minutes + 3600: Hours + 86400: Days + 2592000: Months + trigger: + action: show + field: schedule_type + condition: value[delayed] diff --git a/plugins/rainlab/notify/notifyrules/ExecutionContextCondition.php b/plugins/rainlab/notify/notifyrules/ExecutionContextCondition.php new file mode 100644 index 00000000..32369cab --- /dev/null +++ b/plugins/rainlab/notify/notifyrules/ExecutionContextCondition.php @@ -0,0 +1,136 @@ + 'is', + 'is_not' => 'is not', + ]; + + public function getName() + { + return 'Event is triggered from environment'; + } + + public function getTitle() + { + return 'Execution context'; + } + + public function getText() + { + $host = $this->host; + $value = $host->value; + $attribute = $host->subcondition; + $subconditions = $this->getSubconditionOptions(); + + $result = array_get($subconditions, $attribute, 'Execution context'); + $result .= ' '.array_get($this->operators, $host->operator, $host->operator).' '; + + if ($attribute == 'locale' || $attribute == 'environment') { + $result .= strtolower($value) ?: '?'; + } + elseif ($value) { + $options = $this->getValueOptions(); + $result .= strtolower(array_get($options, $value)); + } + else { + $result .= '?'; + } + + return $result; + } + + /** + * Returns a title to use for grouping subconditions + * in the Create Condition drop-down menu + */ + public function getGroupingTitle() + { + return 'Execution context'; + } + + public function listSubconditions() + { + return array_flip($this->getSubconditionOptions()); + } + + public function initConfigData($host) + { + $host->operator = 'is'; + } + + public function setFormFields($fields) + { + $attribute = $fields->subcondition->value; + + if ($attribute == 'locale' || $attribute == 'environment') { + $fields->value->type = 'text'; + } + else { + $fields->value->type = 'dropdown'; + } + } + + public function getValueOptions() + { + $attribute = $this->host->subcondition; + $result = []; + + if ($attribute == 'context') { + $result = [ + 'backend' => 'Back-end area', + 'front' => 'Front-end website', + 'console' => 'Command line interface', + ]; + } + + if ($attribute == 'theme') { + foreach (Theme::all() as $theme) { + $result[$theme->getDirName()] = $theme->getDirName(); + } + } + + return $result; + } + + public function getSubconditionOptions() + { + return [ + 'environment' => 'Application environment', + 'context' => 'Request context', + 'theme' => 'Active theme', + 'locale' => 'Visitor locale', + ]; + } + + public function getOperatorOptions() + { + return $this->operators; + } + + /** + * Checks whether the condition is TRUE for specified parameters + * @param array $params + * @return bool + */ + public function isTrue(&$params) + { + $hostObj = $this->host; + $attribute = $hostObj->subcondition; + + $conditionValue = $hostObj->value; + $conditionValue = trim(mb_strtolower($conditionValue)); + + if ($attribute == 'locale') { + return array_get($params, 'appLocale') == $conditionValue; + } else if ($attribute === 'environment') { + return $conditionValue === \App::environment(); + } + + return false; + } +} diff --git a/plugins/rainlab/notify/notifyrules/SaveDatabaseAction.php b/plugins/rainlab/notify/notifyrules/SaveDatabaseAction.php new file mode 100644 index 00000000..61921265 --- /dev/null +++ b/plugins/rainlab/notify/notifyrules/SaveDatabaseAction.php @@ -0,0 +1,119 @@ + 'Store in database', + 'description' => 'Log event data in the notifications activity log', + 'icon' => 'icon-database' + ]; + } + + public function defineFormFields() + { + return 'fields.yaml'; + } + + public function getText() + { + if ($this->host->related_object) { + $label = array_get($this->getRelatedObjectOptions(), $this->host->related_object); + + return 'Log event in the '.$label.' log'; + } + + return parent::getText(); + } + + /** + * Triggers this action. + * @param array $params + * @return void + */ + public function triggerAction($params) + { + if ( + (!$definition = array_get($this->tableDefinitions, $this->host->related_object)) || + (!$param = array_get($definition, 'param')) || + (!$value = array_get($params, $param)) + ) { + throw new ApplicationException('Error evaluating the save database action: the related object is not found in the action parameters.'); + } + + if (!$value instanceof EloquentModel) { + // @todo Perhaps value is an ID or a model array, + // look up model $definition[class] from ID ... + } + + $rule = $this->host->notification_rule; + $relation = array_get($definition, 'relation'); + + $value->$relation()->create([ + 'id' => Uuid::uuid4()->toString(), + 'event_type' => $rule->getEventClass(), + 'icon' => $this->host->icon, + 'type' => $this->host->type, + 'body' => $this->host->body, + 'data' => $this->getData($params), + 'read_at' => null, + ]); + } + + /** + * Get the data for the notification. + * + * @param array $notifiable + * @return array + */ + protected function getData($params) + { + // This should check for params that cannot be jsonable. + return $params; + } + + public function getRelatedObjectOptions() + { + $result = []; + + foreach ($this->tableDefinitions as $key => $definition) { + $result[$key] = array_get($definition, 'label'); + } + + return $result; + } + + public function getTableDefinitions() + { + return $this->tableDefinitions; + } + + public function addTableDefinition($options) + { + if (!$className = array_get($options, 'class')) { + throw new ApplicationException('Missing class name from table definition.'); + } + + $options = array_merge([ + 'label' => 'Undefined table', + 'class' => null, + 'param' => null, + 'relation' => 'notifications', + ], $options); + + $keyName = $className . '@' . array_get($options, 'relation'); + + $this->tableDefinitions[$keyName] = $options; + } +} diff --git a/plugins/rainlab/notify/notifyrules/SendMailTemplateAction.php b/plugins/rainlab/notify/notifyrules/SendMailTemplateAction.php new file mode 100644 index 00000000..e7abf4bc --- /dev/null +++ b/plugins/rainlab/notify/notifyrules/SendMailTemplateAction.php @@ -0,0 +1,225 @@ + 'System default', + 'user' => 'User email address (if applicable)', + 'sender' => 'Sender user email address (if applicable)', + 'admin' => 'Back-end administrators', + 'custom' => 'Specific email address', + ]; + + /** + * Returns information about this event, including name and description. + */ + public function actionDetails() + { + return [ + 'name' => 'Compose a mail message', + 'description' => 'Send a message to a recipient', + 'icon' => 'icon-envelope' + ]; + } + + /** + * Triggers this action. + * @param array $params + * @return void + */ + public function triggerAction($params) + { + $template = $this->host->mail_template; + + $recipient = $this->getRecipientAddress($params); + + $replyTo = $this->getReplyToAddress($params); + + if (!$recipient || !$template) { + throw new ApplicationException('Missing valid recipient or mail template'); + } + + Mail::sendTo($recipient, $template, $params, function($message) use ($replyTo) { + if ($replyTo) { + $message->replyTo($replyTo); + } + }); + } + + /** + * Field configuration for the action. + */ + public function defineFormFields() + { + return 'fields.yaml'; + } + + /** + * Defines validation rules for the custom fields. + * @return array + */ + public function defineValidationRules() + { + return [ + 'mail_template' => 'required', + 'send_to_mode' => 'required', + ]; + } + + public function getTitle() + { + if ($this->isAdminMode()) { + return 'Compose mail to administrators'; + } + + return parent::getTitle(); + } + + public function getActionIcon() + { + if ($this->isAdminMode()) { + return 'icon-envelope-square'; + } + + return parent::getActionIcon(); + } + + public function getText() + { + $hostObj = $this->host; + + $recipient = array_get($this->recipientModes, $hostObj->send_to_mode); + + if ($this->isAdminMode()) { + if ($groupId = $this->host->send_to_admin) { + if ($group = AdminGroupModel::find($groupId)) { + $adminText = $group->name; + } + else { + $adminText = '?'; + } + + $adminText .= ' admin group'; + } + else { + $adminText = 'all admins'; + } + return sprintf( + 'Send a message to %s using template %s', + $adminText, + $hostObj->mail_template + ); + } + + if ($hostObj->mail_template) { + return sprintf( + 'Send a message to %s using template %s', + mb_strtolower($recipient), + $hostObj->mail_template + ); + } + + return parent::getText(); + } + + public function getSendToAdminOptions() + { + $options = ['' => '- All administrators -']; + + $groups = AdminGroupModel::lists('name', 'id'); + + return $options + $groups; + } + + public function getSendToModeOptions() + { + $modes = $this->recipientModes; + + unset($modes['system']); + + return $modes; + } + + public function getReplyToModeOptions() + { + $modes = $this->recipientModes; + + unset($modes['admin']); + + return $modes; + } + + public function getMailTemplateOptions() + { + $codes = array_keys(MailTemplate::listAllTemplates()); + + $result = array_combine($codes, $codes); + + // Wrap to prevent collisions with language keys + array_walk($result, function(&$value, $key) { + $value = "[$value]"; + }); + + return $result; + } + + protected function getReplyToAddress($params) + { + $mode = $this->host->reply_to_mode; + + if ($mode == 'custom') { + return $this->host->reply_to_custom; + } + + if ($mode == 'user' || $mode == 'sender') { + $obj = array_get($params, $mode); + return $obj->email; + } + } + + protected function getRecipientAddress($params) + { + $mode = $this->host->send_to_mode; + + if ($mode == 'custom') { + return $this->host->send_to_custom; + } + + if ($mode == 'system') { + $name = Config::get('mail.from.name', 'Your Site'); + $address = Config::get('mail.from.address', 'admin@domain.tld'); + return [$address => $name]; + } + + if ($mode == 'admin') { + if ($groupId = $this->host->send_to_admin) { + if (!$group = AdminGroupModel::find($groupId)) { + throw new ApplicationException('Unable to find admin group with ID: '.$groupId); + } + + return $group->users->lists('full_name', 'email'); + } + else { + return AdminUserModel::all()->lists('full_name', 'email'); + } + } + + if ($mode == 'user' || $mode == 'sender') { + return array_get($params, $mode); + } + } + + protected function isAdminMode() + { + return $this->host->send_to_mode == 'admin'; + } +} diff --git a/plugins/rainlab/notify/notifyrules/executioncontextcondition/fields.yaml b/plugins/rainlab/notify/notifyrules/executioncontextcondition/fields.yaml new file mode 100644 index 00000000..9981f871 --- /dev/null +++ b/plugins/rainlab/notify/notifyrules/executioncontextcondition/fields.yaml @@ -0,0 +1,21 @@ +# =================================== +# Field Definitions +# =================================== + +fields: + + subcondition: + label: Attribute + span: auto + type: dropdown + + operator: + label: Operator + span: auto + type: dropdown + dependsOn: subcondition + + value: + label: Value + dependsOn: [subcondition, operator] + type: dropdown diff --git a/plugins/rainlab/notify/notifyrules/savedatabaseaction/fields.yaml b/plugins/rainlab/notify/notifyrules/savedatabaseaction/fields.yaml new file mode 100644 index 00000000..77c144f2 --- /dev/null +++ b/plugins/rainlab/notify/notifyrules/savedatabaseaction/fields.yaml @@ -0,0 +1,8 @@ +# =================================== +# Field Definitions +# =================================== + +fields: + related_object: + label: Related object + type: dropdown diff --git a/plugins/rainlab/notify/notifyrules/sendmailtemplateaction/fields.yaml b/plugins/rainlab/notify/notifyrules/sendmailtemplateaction/fields.yaml new file mode 100644 index 00000000..13aa5480 --- /dev/null +++ b/plugins/rainlab/notify/notifyrules/sendmailtemplateaction/fields.yaml @@ -0,0 +1,42 @@ +# =================================== +# Field Definitions +# =================================== + +fields: + mail_template: + label: Mail template + type: dropdown + placeholder: Select template + + send_to_mode: + label: Send to + type: radio + span: left + + send_to_custom: + cssClass: radio-align + trigger: + action: show + field: send_to_mode + condition: value[custom] + + send_to_admin: + label: Send to admin group + span: right + type: dropdown + trigger: + action: show + field: send_to_mode + condition: value[admin] + + reply_to_mode: + label: Reply-to address + type: radio + span: left + + reply_to_custom: + cssClass: radio-align + trigger: + action: show + field: reply_to_mode + condition: value[custom] diff --git a/plugins/rainlab/notify/updates/create_notification_rules_table.php b/plugins/rainlab/notify/updates/create_notification_rules_table.php new file mode 100644 index 00000000..2bcc6b07 --- /dev/null +++ b/plugins/rainlab/notify/updates/create_notification_rules_table.php @@ -0,0 +1,29 @@ +engine = 'InnoDB'; + $table->increments('id'); + $table->string('name')->nullable(); + $table->string('code')->index()->nullable(); + $table->string('class_name')->nullable(); + $table->text('description')->nullable(); + $table->mediumText('config_data')->nullable(); + $table->mediumText('condition_data')->nullable(); + $table->boolean('is_enabled')->default(0); + $table->boolean('is_custom')->default(1); + $table->timestamps(); + }); + } + + public function down() + { + Schema::dropIfExists('rainlab_notify_notification_rules'); + } +} diff --git a/plugins/rainlab/notify/updates/create_notifications_table.php b/plugins/rainlab/notify/updates/create_notifications_table.php new file mode 100644 index 00000000..4c73bb4c --- /dev/null +++ b/plugins/rainlab/notify/updates/create_notifications_table.php @@ -0,0 +1,28 @@ +uuid('id')->primary(); + $table->string('event_type'); + $table->morphs('notifiable'); + $table->string('icon')->nullable(); + $table->string('type')->nullable(); + $table->text('body')->nullable(); + $table->mediumText('data'); + $table->timestamp('read_at')->nullable(); + $table->timestamps(); + }); + } + + public function down() + { + Schema::dropIfExists('rainlab_notify_notifications'); + } +} diff --git a/plugins/rainlab/notify/updates/create_rule_actions_table.php b/plugins/rainlab/notify/updates/create_rule_actions_table.php new file mode 100644 index 00000000..af89e412 --- /dev/null +++ b/plugins/rainlab/notify/updates/create_rule_actions_table.php @@ -0,0 +1,25 @@ +engine = 'InnoDB'; + $table->increments('id'); + $table->string('class_name')->nullable(); + $table->mediumText('config_data')->nullable(); + $table->integer('rule_host_id')->unsigned()->nullable()->index(); + $table->timestamps(); + }); + } + + public function down() + { + Schema::dropIfExists('rainlab_notify_rule_actions'); + } +} diff --git a/plugins/rainlab/notify/updates/create_rule_conditions_table.php b/plugins/rainlab/notify/updates/create_rule_conditions_table.php new file mode 100644 index 00000000..4a0211e6 --- /dev/null +++ b/plugins/rainlab/notify/updates/create_rule_conditions_table.php @@ -0,0 +1,29 @@ +engine = 'InnoDB'; + $table->increments('id'); + $table->string('class_name')->nullable(); + $table->mediumText('config_data')->nullable(); + $table->string('condition_control_type', 100)->nullable(); + $table->string('rule_host_type', 100)->nullable(); + $table->integer('rule_host_id')->unsigned()->nullable()->index(); + $table->integer('rule_parent_id')->unsigned()->nullable()->index(); + $table->index(['rule_host_id', 'rule_host_type'], 'host_rule_id_type'); + $table->timestamps(); + }); + } + + public function down() + { + Schema::dropIfExists('rainlab_notify_rule_conditions'); + } +} diff --git a/plugins/rainlab/notify/updates/version.yaml b/plugins/rainlab/notify/updates/version.yaml new file mode 100644 index 00000000..af1dae03 --- /dev/null +++ b/plugins/rainlab/notify/updates/version.yaml @@ -0,0 +1,13 @@ +v1.0.1: + - First version of Notify + - create_notifications_table.php + - create_notification_rules_table.php + - create_rule_conditions_table.php + - create_rule_actions_table.php +v1.0.2: Fixes crashing bug. +v1.0.3: Added Turkish & Russian translations, various bug fixes. +v1.1.0: Fixes support for October CMS 2.0 +v1.1.1: Fixes missing template bug when saving notification rule +v1.1.2: Fixes collisions with language keys in compose mail action +v1.2.0: Adds delayed execution to actions +v1.2.1: Improve support with October v3 diff --git a/plugins/rainlab/translate/.gitignore b/plugins/rainlab/translate/.gitignore new file mode 100644 index 00000000..4b53aba1 --- /dev/null +++ b/plugins/rainlab/translate/.gitignore @@ -0,0 +1,4 @@ +/vendor +composer.lock +.DS_Store +.phpunit.result.cache diff --git a/plugins/rainlab/translate/LICENCE.md b/plugins/rainlab/translate/LICENCE.md new file mode 100644 index 00000000..e49b459a --- /dev/null +++ b/plugins/rainlab/translate/LICENCE.md @@ -0,0 +1,21 @@ +# MIT license + +Copyright (c) 2014-2022 Responsiv Pty Ltd, October CMS + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/plugins/rainlab/translate/Plugin.php b/plugins/rainlab/translate/Plugin.php new file mode 100644 index 00000000..ec578833 --- /dev/null +++ b/plugins/rainlab/translate/Plugin.php @@ -0,0 +1,381 @@ + 'rainlab.translate::lang.plugin.name', + 'description' => 'rainlab.translate::lang.plugin.description', + 'author' => 'Alexey Bobkov, Samuel Georges', + 'icon' => 'icon-language', + 'homepage' => 'https://github.com/rainlab/translate-plugin' + ]; + } + + /** + * register the plugin + */ + public function register() + { + // Load localized version of mail templates (akin to localized CMS content files) + Event::listen('mailer.beforeAddContent', function ($mailer, $message, $view, $data, $raw, $plain) { + return EventRegistry::instance()->findLocalizedMailViewContent($mailer, $message, $view, $data, $raw, $plain); + }, 1); + + // Defer event with low priority to let others contribute before this registers. + Event::listen('backend.form.extendFieldsBefore', function($widget) { + EventRegistry::instance()->registerFormFieldReplacements($widget); + }, -1); + + // Handle translated page URLs + Page::extend(function($model) { + if (!$model->propertyExists('translatable')) { + $model->addDynamicProperty('translatable', []); + } + $model->translatable = array_merge($model->translatable, ['title', 'description', 'meta_title', 'meta_description']); + if (!$model->isClassExtendedWith(\RainLab\Translate\Behaviors\TranslatablePageUrl::class)) { + $model->extendClassWith(\RainLab\Translate\Behaviors\TranslatablePageUrl::class); + } + if (!$model->isClassExtendedWith(\RainLab\Translate\Behaviors\TranslatablePage::class)) { + $model->extendClassWith(\RainLab\Translate\Behaviors\TranslatablePage::class); + } + }); + + // Extension logic for October CMS v1.0 + if (!class_exists('System')) { + $this->extendLegacyPlatform(); + } + // Extension logic for October CMS v2.0 + else { + Event::listen('cms.theme.createThemeDataModel', function($attributes) { + return new \RainLab\Translate\Models\MLThemeData($attributes); + }); + + Event::listen('cms.template.getTemplateToolbarSettingsButtons', function($extension, $dataHolder) { + if ($dataHolder->templateType === 'page') { + EventRegistry::instance()->extendEditorPageToolbar($dataHolder); + } + }); + } + + // Register console commands + $this->registerConsoleCommand('translate.scan', \Rainlab\Translate\Console\ScanCommand::class); + + // Register asset bundles + $this->registerAssetBundles(); + } + + /** + * extendLegacyPlatform will add the legacy features expected in v1.0 + */ + protected function extendLegacyPlatform() + { + // Adds translation support to file models + File::extend(function ($model) { + if (!$model->propertyExists('translatable')) { + $model->addDynamicProperty('translatable', []); + } + $model->translatable = array_merge($model->translatable, ['title', 'description']); + if (!$model->isClassExtendedWith(\October\Rain\Database\Behaviors\Purgeable::class)) { + $model->extendClassWith(\October\Rain\Database\Behaviors\Purgeable::class); + } + if (!$model->isClassExtendedWith(\RainLab\Translate\Behaviors\TranslatableModel::class)) { + $model->extendClassWith(\RainLab\Translate\Behaviors\TranslatableModel::class); + } + }); + + // Adds translation support to theme settings + ThemeData::extend(static function ($model) { + if (!$model->propertyExists('translatable')) { + $model->addDynamicProperty('translatable', []); + } + + if (!$model->isClassExtendedWith(\October\Rain\Database\Behaviors\Purgeable::class)) { + $model->extendClassWith(\October\Rain\Database\Behaviors\Purgeable::class); + } + if (!$model->isClassExtendedWith(\RainLab\Translate\Behaviors\TranslatableModel::class)) { + $model->extendClassWith(\RainLab\Translate\Behaviors\TranslatableModel::class); + } + + $model->bindEvent('model.afterFetch', static function() use ($model) { + foreach ($model->getFormFields() as $id => $field) { + if (!empty($field['translatable'])) { + $model->translatable[] = $id; + } + } + }); + }); + } + + /** + * boot the plugin + */ + public function boot() + { + // Set the page context for translation caching with high priority. + Event::listen('cms.page.init', function($controller, $page) { + EventRegistry::instance()->setMessageContext($page); + }, 100); + + // Populate MenuItem properties with localized values if available + Event::listen('pages.menu.referencesGenerated', function (&$items) { + $locale = App::getLocale(); + $iterator = function ($menuItems) use (&$iterator, $locale) { + $result = []; + foreach ($menuItems as $item) { + $localeFields = array_get($item->viewBag, "locale.$locale", []); + foreach ($localeFields as $fieldName => $fieldValue) { + if ($fieldValue) { + $item->$fieldName = $fieldValue; + } + } + if ($item->items) { + $item->items = $iterator($item->items); + } + $result[] = $item; + } + return $result; + }; + $items = $iterator($items); + }); + + // Import messages defined by the theme + Event::listen('cms.theme.setActiveTheme', function($code) { + EventRegistry::instance()->importMessagesFromTheme($code); + }); + + // Adds language suffixes to content files. + Event::listen('cms.page.beforeRenderContent', function($controller, $fileName) { + return EventRegistry::instance() + ->findTranslatedContentFile($controller, $fileName) + ; + }); + + // Prune localized content files from template list + Event::listen('pages.content.templateList', function($widget, $templates) { + return EventRegistry::instance() + ->pruneTranslatedContentTemplates($templates) + ; + }); + + // Look at session for locale using middleware + \Cms\Classes\CmsController::extend(function($controller) { + $controller->middleware(\RainLab\Translate\Classes\LocaleMiddleware::class); + }); + + // Append current locale to static page's cache keys + $modifyKey = function (&$key) { + $key = $key . '-' . Lang::getLocale(); + }; + Event::listen('pages.router.getCacheKey', $modifyKey); + Event::listen('pages.page.getMenuCacheKey', $modifyKey); + Event::listen('pages.snippet.getMapCacheKey', $modifyKey); + Event::listen('pages.snippet.getPartialMapCacheKey', $modifyKey); + + if (class_exists('\RainLab\Pages\Classes\SnippetManager')) { + $handler = function ($controller, $template, $type) { + if (!$template->methodExists('getDirtyLocales')) { + return; + } + + // Get the locales that have changed + $dirtyLocales = $template->getDirtyLocales(); + + if (!empty($dirtyLocales)) { + $currentLocale = Lang::getLocale(); + + foreach ($dirtyLocales as $locale) { + if (!$template->isTranslateDirty(null, $locale)) { + continue; + } + + // Clear the RainLab.Pages caches for each dirty locale + App::setLocale($locale); + \RainLab\Pages\Plugin::clearCache(); + } + + // Restore the original locale for this request + App::setLocale($currentLocale); + } + }; + + Event::listen('cms.template.save', $handler); + Event::listen('pages.object.save', $handler); + } + } + + /** + * registerComponents + */ + public function registerComponents() + { + return [ + \RainLab\Translate\Components\LocalePicker::class => 'localePicker', + \RainLab\Translate\Components\AlternateHrefLangElements::class => 'alternateHrefLangElements' + ]; + } + + /** + * registerPermissions + */ + public function registerPermissions() + { + return [ + 'rainlab.translate.manage_locales' => [ + 'tab' => 'rainlab.translate::lang.plugin.tab', + 'label' => 'rainlab.translate::lang.plugin.manage_locales' + ], + 'rainlab.translate.manage_messages' => [ + 'tab' => 'rainlab.translate::lang.plugin.tab', + 'label' => 'rainlab.translate::lang.plugin.manage_messages' + ] + ]; + } + + /** + * registerSettings + */ + public function registerSettings() + { + return [ + 'locales' => [ + 'label' => 'rainlab.translate::lang.locale.title', + 'description' => 'rainlab.translate::lang.plugin.description', + 'icon' => 'icon-language', + 'url' => Backend::url('rainlab/translate/locales'), + 'order' => 550, + 'category' => 'rainlab.translate::lang.plugin.name', + 'permissions' => ['rainlab.translate.manage_locales'], + 'keywords' => 'translate', + ], + 'messages' => [ + 'label' => 'rainlab.translate::lang.messages.title', + 'description' => 'rainlab.translate::lang.messages.description', + 'icon' => 'icon-list-alt', + 'url' => Backend::url('rainlab/translate/messages'), + 'order' => 551, + 'category' => 'rainlab.translate::lang.plugin.name', + 'permissions' => ['rainlab.translate.manage_messages'], + 'keywords' => 'translate', + ] + ]; + } + + /** + * registerMarkupTags for Twig + * @return array + */ + public function registerMarkupTags() + { + return [ + 'filters' => [ + '_' => [$this, 'translateString'], + '__' => [$this, 'translatePlural'], + 'transRaw' => [$this, 'translateRawString'], + 'transRawPlural' => [$this, 'translateRawPlural'], + 'localeUrl' => [$this, 'localeUrl'], + ] + ]; + } + + /** + * registerFormWidgets for multi-lingual + */ + public function registerFormWidgets() + { + $mediaFinderClass = class_exists('System') + ? \RainLab\Translate\FormWidgets\MLMediaFinderv2::class + : \RainLab\Translate\FormWidgets\MLMediaFinder::class; + + return [ + \RainLab\Translate\FormWidgets\MLText::class => 'mltext', + \RainLab\Translate\FormWidgets\MLTextarea::class => 'mltextarea', + \RainLab\Translate\FormWidgets\MLRichEditor::class => 'mlricheditor', + \RainLab\Translate\FormWidgets\MLMarkdownEditor::class => 'mlmarkdowneditor', + \RainLab\Translate\FormWidgets\MLRepeater::class => 'mlrepeater', + \RainLab\Translate\FormWidgets\MLNestedForm::class => 'mlnestedform', + $mediaFinderClass => 'mlmediafinder', + ]; + } + + /** + * registerAssetBundles for compilation + */ + protected function registerAssetBundles() + { + CombineAssets::registerCallback(function ($combiner) { + $combiner->registerBundle('$/rainlab/translate/assets/less/messages.less'); + $combiner->registerBundle('$/rainlab/translate/assets/less/multilingual.less'); + }); + } + + /** + * localeUrl builds a localized URL + */ + public function localeUrl($url, $locale) + { + $translator = Translator::instance(); + + $parts = parse_url($url); + + $path = array_get($parts, 'path'); + + return http_build_url($parts, [ + 'path' => '/' . $translator->getPathInLocale($path, $locale) + ]); + } + + /** + * translateString + */ + public function translateString($string, $params = [], $locale = null) + { + return Message::trans($string, $params, $locale); + } + + /** + * translatePlural + */ + public function translatePlural($string, $count = 0, $params = [], $locale = null) + { + return Lang::choice(Message::trans($string, $params, $locale), $count, $params); + } + + /** + * translateRawString + */ + public function translateRawString($string, $params = [], $locale = null) + { + return Message::transRaw($string, $params, $locale); + } + + /** + * translateRawPlural + */ + public function translateRawPlural($string, $count = 0, $params = [], $locale = null) + { + return Lang::choice(Message::transRaw($string, $params, $locale), $count, $params); + } +} diff --git a/plugins/rainlab/translate/README.md b/plugins/rainlab/translate/README.md new file mode 100644 index 00000000..65227172 --- /dev/null +++ b/plugins/rainlab/translate/README.md @@ -0,0 +1,394 @@ +# Translation plugin + +Enables multi-lingual sites. + +## Selecting a language + +Different languages can be set up in the back-end area, with a single default language selected. This activates the use of the language on the front-end and in the back-end UI. + +A visitor can select a language by prefixing the language code to the URL, this is then stored in the user's session as their chosen language. For example: + +* `http://website/ru/` will display the site in Russian +* `http://website/fr/` will display the site in French +* `http://website/` will display the site in the default language or the user's chosen language. + +## Language Picker Component + +A visitor can select their chosen language using the `LocalePicker` component. This component will display a simple dropdown that changes the page language depending on the selection. + + title = "Home" + url = "/" + + [localePicker] + == + +

    {{ 'Please select your language:'|_ }}

    + {% component 'localePicker' %} + +If translated, the text above will appear as whatever language is selected by the user. The dropdown is very basic and is intended to be restyled. A simpler example might be: + + [...] + == + +

    + Switch language to: + English, + Russian +

    + +## Message translation + +Message or string translation is the conversion of adhoc strings used throughout the site. A message can be translated with parameters. + + {{ 'site.name'|_ }} + + {{ 'Welcome to our website!'|_ }} + + {{ 'Hello :name!'|_({ name: 'Friend' }) }} + +A message can also be translated for a choice usage. + + {{ 'There are no apples|There are :number applies!'|__(2, { number: 'two' }) }} + +Or you set a locale manually by passing a second argument. + + {{ 'this is always english'|_({}, 'en') }} + +Themes can provide default values for these messages by defining a `translate` key in the `theme.yaml` file, located in the theme directory. + + name: My Theme + # [...] + + translate: + en: + site.name: 'My Website' + nav.home: 'Home' + nav.video: 'Video' + title.home: 'Welcome Home' + title.video: 'Screencast Video' + +You may also define the translations in a separate file, where the path is relative to the theme. The following definition will source the default messages from the file **config/lang.yaml** inside the theme. + + name: My Theme + # [...] + + translate: config/lang.yaml + +This is an example of **config/lang.yaml** file with two languages: + + en: + site.name: 'My Website' + nav.home: 'Home' + nav.video: 'Video' + title.home: 'Welcome Home' + hr: + site.name: 'Moje web stranice' + nav.home: 'Početna' + nav.video: 'Video' + title.home: 'Dobrodošli' + +You may also define the translations in a separate file per locale, where the path is relative to the theme. The following definition will source the default messages from the file **config/lang-en.yaml** inside the theme for the english locale and from the file **config/lang-fr.yaml** for the french locale. + + name: My Theme + # [...] + + translate: + en: config/lang-en.yaml + fr: config/lang-fr.yaml + +This is an example for the **config/lang-en.yaml** file: + + site.name: 'My Website' + nav.home: 'Home' + nav.video: 'Video' + title.home: 'Welcome Home' + +In order to make these default values reflected to your frontend site, go to **Settings -> Translate messages** in the backend and hit **Scan for messages**. They will also be loaded automatically when the theme is activated. + +The same operation can be performed with the `translate:scan` artisan command. It may be worth including it in a deployment script to automatically fetch updated messages: + + php artisan translate:scan + +Add the `--purge` option to clear old messages first: + + php artisan translate:scan --purge + +## Content & mail template translation + +This plugin activates a feature in the CMS that allows content & mail template files to use language suffixes, for example: + +* **welcome.htm** will contain the content or mail template in the default language. +* **welcome-ru.htm** will contain the content or mail template in Russian. +* **welcome-fr.htm** will contain the content or mail template in French. + +## Model translation + +Models can have their attributes translated by using the `RainLab.Translate.Behaviors.TranslatableModel` behavior and specifying which attributes to translate in the class. + + class User + { + public $implement = ['RainLab.Translate.Behaviors.TranslatableModel']; + + public $translatable = ['name']; + } + +The attribute will then contain the default language value and other language code values can be created by using the `translateContext()` method. + + $user = User::first(); + + // Outputs the name in the default language + echo $user->name; + + $user->translateContext('fr'); + + // Outputs the name in French + echo $user->name; + +You may use the same process for setting values. + + $user = User::first(); + + // Sets the name in the default language + $user->name = 'English'; + + $user->translateContext('fr'); + + // Sets the name in French + $user->name = 'Anglais'; + +The `lang()` method is a shorthand version of `translateContext()` and is also chainable. + + // Outputs the name in French + echo $user->lang('fr')->name; + +This can be useful inside a Twig template. + + {{ user.lang('fr').name }} + +There are ways to get and set attributes without changing the context. + + // Gets a single translated attribute for a language + $user->getAttributeTranslated('name', 'fr'); + + // Sets a single translated attribute for a language + $user->setAttributeTranslated('name', 'Jean-Claude', 'fr'); + +## Theme data translation + +It is also possible to translate theme customisation options. Just mark your form fields with `translatable` property and the plugin will take care about everything else: + + tabs: + fields: + website_name: + tab: Info + label: Website Name + type: text + default: Your website name + translatable: true + +## Fallback attribute values + +By default, untranslated attributes will fall back to the default locale. This behavior can be disabled by calling the `noFallbackLocale` method. + + $user = User::first(); + + $user->noFallbackLocale()->lang('fr'); + + // Returns NULL if there is no French translation + $user->name; + +## Indexed attributes + +Translatable model attributes can also be declared as an index by passing the `$transatable` attribute value as an array. The first value is the attribute name, the other values represent options, in this case setting the option `index` to `true`. + + public $translatable = [ + 'name', + ['slug', 'index' => true] + ]; + +Once an attribute is indexed, you may use the `transWhere` method to apply a basic query to the model. + + Post::transWhere('slug', 'hello-world')->first(); + +The `transWhere` method accepts a third argument to explicitly pass a locale value, otherwise it will be detected from the environment. + + Post::transWhere('slug', 'hello-world', 'en')->first(); + +## URL translation + +Pages in the CMS support translating the URL property. Assuming you have 3 languages set up: + +- en: English +- fr: French +- ru: Russian + +There is a page with the following content: + + url = "/contact" + + [viewBag] + localeUrl[ru] = "/контакт" + == +

    Page content

    + +The word "Contact" in French is the same so a translated URL is not given, or needed. If the page has no URL override specified, then the default URL will be used. Pages will not be duplicated for a given language. + +- /fr/contact - Page in French +- /en/contact - Page in English +- /ru/контакт - Page in Russian +- /ru/contact - 404 + +## URL parameter translation + +It's possible to translate URL parameters by listening to the `translate.localePicker.translateParams` event, which is fired when switching languages. + + Event::listen('translate.localePicker.translateParams', function($page, $params, $oldLocale, $newLocale) { + if ($page->baseFileName == 'your-page-filename') { + return YourModel::translateParams($params, $oldLocale, $newLocale); + } + }); + +In YourModel, one possible implementation might look like this: + + public static function translateParams($params, $oldLocale, $newLocale) { + $newParams = $params; + foreach ($params as $paramName => $paramValue) { + $records = self::transWhere($paramName, $paramValue, $oldLocale)->first(); + if ($records) { + $records->translateContext($newLocale); + $newParams[$paramName] = $records->$paramName; + } + } + return $newParams; + } + +## Query string translation + +It's possible to translate query string parameters by listening to the `translate.localePicker.translateQuery` event, which is fired when switching languages. + + Event::listen('translate.localePicker.translateQuery', function($page, $params, $oldLocale, $newLocale) { + if ($page->baseFileName == 'your-page-filename') { + return YourModel::translateParams($params, $oldLocale, $newLocale); + } + }); + +For a possible implementation of the `YourModel::translateParams` method look at the example under `URL parameter translation` from above. + +## Extend theme scan + + Event::listen('rainlab.translate.themeScanner.afterScan', function (ThemeScanner $scanner) { + ... + }); + +## Settings model translation + +It's possible to translate your settings model like any other model. To retrieve translated values use: + + Settings::instance()->getAttributeTranslated('your_attribute_name'); + +## Conditionally extending plugins + +#### Models + +It is possible to conditionally extend a plugin's models to support translation by placing an `@` symbol before the behavior definition. This is a soft implement will only use `TranslatableModel` if the Translate plugin is installed, otherwise it will not cause any errors. + + /** + * Blog Post Model + */ + class Post extends Model + { + + [...] + + /** + * Softly implement the TranslatableModel behavior. + */ + public $implement = ['@RainLab.Translate.Behaviors.TranslatableModel']; + + /** + * @var array Attributes that support translation, if available. + */ + public $translatable = ['title']; + + [...] + + } + +The back-end forms will automatically detect the presence of translatable fields and replace their controls for multilingual equivalents. + +#### Messages + +Since the Twig filter will not be available all the time, we can pipe them to the native Laravel translation methods instead. This ensures translated messages will always work on the front end. + + /** + * Register new Twig variables + * @return array + */ + public function registerMarkupTags() + { + // Check the translate plugin is installed + if (!class_exists('RainLab\Translate\Behaviors\TranslatableModel')) + return; + + return [ + 'filters' => [ + '_' => ['Lang', 'get'], + '__' => ['Lang', 'choice'], + ] + ]; + } + +# User Interface + +#### Switching locales + +Users can switch between locales by clicking on the locale indicator on the right hand side of the Multi-language input. By holding the CMD / CTRL key all Multi-language Input fields will switch to the selected locale. + +## Integration without jQuery and October Framework files + +It is possible to use the front-end language switcher without using jQuery or the OctoberCMS AJAX Framework by making the AJAX API request yourself manually. The following is an example of how to do that. + + document.querySelector('#languageSelect').addEventListener('change', function () { + const details = { + _session_key: document.querySelector('input[name="_session_key"]').value, + _token: document.querySelector('input[name="_token"]').value, + locale: this.value + } + + let formBody = [] + + for (var property in details) { + let encodedKey = encodeURIComponent(property) + let encodedValue = encodeURIComponent(details[property]) + formBody.push(encodedKey + '=' + encodedValue) + } + + formBody = formBody.join('&') + + fetch(location.href + '/', { + method: 'POST', + body: formBody, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', + 'X-OCTOBER-REQUEST-HANDLER': 'onSwitchLocale', + 'X-OCTOBER-REQUEST-PARTIALS': '', + 'X-Requested-With': 'XMLHttpRequest' + } + }) + .then(res => res.json()) + .then(res => window.location.replace(res.X_OCTOBER_REDIRECT)) + .catch(err => console.log(err)) + }) + +The HTML: + + {{ form_open() }} + + {{ form_close() }} diff --git a/plugins/rainlab/translate/assets/css/messages.css b/plugins/rainlab/translate/assets/css/messages.css new file mode 100644 index 00000000..13b13fab --- /dev/null +++ b/plugins/rainlab/translate/assets/css/messages.css @@ -0,0 +1,17 @@ +#messagesContainer{padding-top:20px} +.translate-messages{position:relative;margin-top:-20px} +.translate-messages .dropdown-button-placeholder{display:block;width:1px;height:20px} +.translate-messages .dropdown-to, +.translate-messages .dropdown-from{position:absolute;top:57px} +.translate-messages .dropdown-to{left:50%} +.translate-messages .dropdown-from{left:0} +.translate-messages .no-other-languages{text-align:center;padding:5px} +.translate-messages .header-language, +.translate-messages .header-swap-languages, +.translate-messages .header-hide-translated{line-height:27px} +.translate-messages .header-language{float:left;cursor:pointer} +.translate-messages .header-language span.is-default{text-transform:none} +.translate-messages .header-swap-languages{margin-right:10px;float:right;font-size:16px;cursor:pointer} +.translate-messages .header-hide-translated{float:right;text-align:right} +.translate-messages .header-hide-translated label{font-weight:normal;margin-bottom:0;margin-right:10px;font-size:12px;white-space:normal} +.translate-messages .header-hide-translated label:before{top:5px;left:0} \ No newline at end of file diff --git a/plugins/rainlab/translate/assets/css/multilingual-v1.css b/plugins/rainlab/translate/assets/css/multilingual-v1.css new file mode 100644 index 00000000..aa2ed8be --- /dev/null +++ b/plugins/rainlab/translate/assets/css/multilingual-v1.css @@ -0,0 +1,13 @@ +/* + * Legacy styles for October v1.0 + */ + +/* Fancy layout */ + +.fancy-layout .form-tabless-fields .field-multilingual .ml-btn { + color: rgba(255,255,255,0.8); +} + +.fancy-layout .form-tabless-fields .field-multilingual .ml-btn:hover { + color: #fff; +} diff --git a/plugins/rainlab/translate/assets/css/multilingual.css b/plugins/rainlab/translate/assets/css/multilingual.css new file mode 100644 index 00000000..6ecaba1c --- /dev/null +++ b/plugins/rainlab/translate/assets/css/multilingual.css @@ -0,0 +1,23 @@ +.field-multilingual{position:relative} +.field-multilingual .ml-btn{background:transparent;position:absolute;color:#7b7b7b;text-transform:uppercase;font-size:11px;letter-spacing:1px;width:44px;padding-right:0;-webkit-box-shadow:none;box-shadow:none;text-shadow:none;z-index:2} +.field-multilingual .ml-btn:hover{color:#555} +.field-multilingual.field-multilingual-text .form-control{padding-right:44px} +.field-multilingual.field-multilingual-text .ml-btn{right:4px;top:50%;margin-top:-44px;height:88px} +.field-multilingual.field-multilingual-textarea textarea{padding-right:30px} +.field-multilingual.field-multilingual-textarea .ml-btn{right:25px;top:5px;width:24px;text-align:right;padding-left:4px} +.field-multilingual.field-multilingual-textarea .ml-dropdown-menu{top:39px;right:1px} +.field-multilingual.field-multilingual-richeditor .ml-btn{top:43px;right:25px;text-align:right} +.field-multilingual.field-multilingual-richeditor .ml-dropdown-menu{top:74px;right:12px} +.field-multilingual.field-multilingual-markdowneditor .ml-btn{top:43px;right:25px;text-align:right} +.field-multilingual.field-multilingual-markdowneditor .ml-dropdown-menu{top:74px !important;right:12px} +.field-multilingual.field-multilingual-repeater .ml-btn{top:-27px;right:11px;text-align:right} +.field-multilingual.field-multilingual-repeater .ml-dropdown-menu{top:0;right:0} +.field-multilingual.field-multilingual-repeater.is-empty{padding-top:5px} +.field-multilingual.field-multilingual-repeater.is-empty .ml-btn{top:-10px;right:5px;text-align:right} +.field-multilingual.field-multilingual-repeater.is-empty .ml-dropdown-menu{top:15px;right:-7px} +.field-multilingual.field-multilingual-nestedform .ml-btn{top:-3px;right:11px;text-align:right} +.field-multilingual.field-multilingual-nestedform .ml-dropdown-menu{top:24px;right:-2px} +.field-multilingual.field-multilingual-nestedform.is-paneled .ml-btn{top:7px;right:15px} +.field-multilingual.field-multilingual-nestedform.is-paneled .ml-dropdown-menu{top:34px;right:2px} +.fancy-layout .field-multilingual-text input.form-control{padding-right:44px} +.help-block.before-field + .field-multilingual.field-multilingual-textarea .ml-btn{top:-41px} \ No newline at end of file diff --git a/plugins/rainlab/translate/assets/js/locales.js b/plugins/rainlab/translate/assets/js/locales.js new file mode 100644 index 00000000..10e652a2 --- /dev/null +++ b/plugins/rainlab/translate/assets/js/locales.js @@ -0,0 +1,28 @@ +/* + * Scripts for the Locales controller. + */ ++function ($) { "use strict"; + + var TranslateLocales = function() { + + this.clickRecord = function(recordId) { + var newPopup = $('') + + newPopup.popup({ + handler: 'onUpdateForm', + extraData: { + 'record_id': recordId, + } + }) + } + + this.createRecord = function() { + var newPopup = $('') + newPopup.popup({ handler: 'onCreateForm' }) + } + + } + + $.translateLocales = new TranslateLocales; + +}(window.jQuery); \ No newline at end of file diff --git a/plugins/rainlab/translate/assets/js/messages.js b/plugins/rainlab/translate/assets/js/messages.js new file mode 100644 index 00000000..74648fd8 --- /dev/null +++ b/plugins/rainlab/translate/assets/js/messages.js @@ -0,0 +1,165 @@ +/* + * Scripts for the Messages controller. + */ ++function ($) { "use strict"; + + var TranslateMessages = function() { + var self = this + + this.$form = null + + /* + * Table toolbar + */ + this.tableToolbar = null + + /* + * Input with the "from" locale value + */ + this.fromInput = null + + /* + * Template for the "from" header (title) + */ + this.fromHeader = null + + /* + * Input with the "to" locale value + */ + this.toInput = null + + /* + * Template for the "to" header (title) + */ + this.toHeader = null + + /* + * Template for the "found" header (title) + */ + this.foundHeader = null + + /* + * The table widget element + */ + this.tableElement = null + + /* + * Hide translated strings (show only from the empty data set) + */ + this.hideTranslated = false + + /* + * Data sets, complete and untranslated (empty) + */ + this.emptyDataSet = null + this.dataSet = null + + $(document).on('change', '#hideTranslated', function(){ + self.toggleTranslated($(this).is(':checked')) + self.refreshTable() + }); + + $(document).on('keyup', '.control-table input.string-input', function(ev) { + self.onApplyValue(ev) + }); + + this.toggleTranslated = function(isHide) { + this.hideTranslated = isHide + this.setTitleContents() + } + + this.setToolbarContents = function(tableToolbar) { + if (tableToolbar) this.tableToolbar = $(tableToolbar) + if (!this.tableElement) return + + var $toolbar = $('.toolbar', this.tableElement) + if ($toolbar.hasClass('message-buttons-added')) { + return + } + + $toolbar.addClass('message-buttons-added') + $toolbar.prepend(Mustache.render(this.tableToolbar.html())) + } + + this.setTitleContents = function(fromEl, toEl, foundEl) { + if (fromEl) this.fromHeader = $(fromEl) + if (toEl) this.toHeader = $(toEl) + if (foundEl) this.foundHeader = $(foundEl) + if (!this.tableElement) return + + var $headers = $('table.headers th', this.tableElement) + $headers.eq(0).html(this.fromHeader.html()) + $headers.eq(1).html(Mustache.render(this.toHeader.html(), { hideTranslated: this.hideTranslated } )) + $headers.eq(2).html(this.foundHeader.html()) + } + + this.setTableElement = function(el) { + this.tableElement = $(el) + this.$form = $('#messagesForm') + this.fromInput = this.$form.find('input[name=locale_from]') + this.toInput = this.$form.find('input[name=locale_to]') + + this.tableElement.one('oc.tableUpdateData', $.proxy(this.updateTableData, this)) + } + + this.onApplyValue = function(ev) { + if (ev.keyCode == 13) { + var $table = $(ev.currentTarget).closest('[data-control=table]') + + if (!$table.length) { + return + } + + var tableObj = $table.data('oc.table') + if (tableObj) { + tableObj.setCellValue($(ev.currentTarget).closest('td').get(0), ev.currentTarget.value) + tableObj.commitEditedRow() + } + } + } + + this.updateTableData = function(event, records) { + if (this.hideTranslated && !records.length) { + self.toggleTranslated($(this).is(':checked')) + self.refreshTable() + } + } + + this.toggleDropdown = function(el) { + setTimeout(function(){ $(el).dropdown('toggle') }, 1) + return false + } + + this.setLanguage = function(type, code) { + if (type == 'to') + this.toInput.val(code) + else if (type == 'from') + this.fromInput.val(code) + + this.refreshGrid() + return false + } + + this.swapLanguages = function() { + var from = this.fromInput.val(), + to = this.toInput.val() + + this.toggleTranslated(false) + this.fromInput.val(to) + this.toInput.val(from) + this.refreshGrid() + } + + this.refreshGrid = function() { + this.$form.request('onRefresh') + } + + this.refreshTable = function() { + this.tableElement.table('updateDataTable') + } + + } + + $.translateMessages = new TranslateMessages; + +}(window.jQuery); diff --git a/plugins/rainlab/translate/assets/js/multilingual.js b/plugins/rainlab/translate/assets/js/multilingual.js new file mode 100644 index 00000000..2fd6bc33 --- /dev/null +++ b/plugins/rainlab/translate/assets/js/multilingual.js @@ -0,0 +1,145 @@ +/* + * Multi lingual control plugin + * + * Data attributes: + * - data-control="multilingual" - enables the plugin on an element + * - data-default-locale="en" - default locale code + * - data-placeholder-field="#placeholderField" - an element that contains the placeholder value + * + * JavaScript API: + * $('a#someElement').multiLingual({ option: 'value' }) + * + * Dependences: + * - Nil + */ + ++function ($) { "use strict"; + + // MULTILINGUAL CLASS DEFINITION + // ============================ + + var MultiLingual = function(element, options) { + var self = this + this.options = options + this.$el = $(element) + + this.$activeField = null + this.$activeButton = $('[data-active-locale]', this.$el) + this.$dropdown = $('ul.ml-dropdown-menu', this.$el) + this.$placeholder = $(this.options.placeholderField) + + /* + * Init locale + */ + this.activeLocale = this.options.defaultLocale + this.$activeField = this.getLocaleElement(this.activeLocale) + this.$activeButton.text(this.activeLocale) + + this.$dropdown.on('click', '[data-switch-locale]', this.$activeButton, function(event){ + var currentLocale = event.data.text(); + var selectedLocale = $(this).data('switch-locale') + + // only call setLocale() if locale has changed + if (selectedLocale != currentLocale) { + self.setLocale(selectedLocale) + } + + /* + * If Ctrl/Cmd key is pressed, find other instances and switch + */ + if (event.ctrlKey || event.metaKey) { + event.preventDefault(); + $('[data-switch-locale="'+selectedLocale+'"]').click() + } + }) + + this.$placeholder.on('input', function(){ + self.$activeField.val(this.value) + }) + + /* + * Handle oc.inputPreset.beforeUpdate event + */ + $('[data-input-preset]', this.$el).on('oc.inputPreset.beforeUpdate', function(event, src) { + var sourceLocale = src.siblings('.ml-btn[data-active-locale]').text() + var targetLocale = $(this).data('locale-value') + var targetActiveLocale = $(this).siblings('.ml-btn[data-active-locale]').text() + + if (sourceLocale && targetLocale && targetActiveLocale) { + if (targetActiveLocale !== sourceLocale) + self.setLocale(sourceLocale) + $(this).data('update', sourceLocale === targetLocale) + } + }) + } + + MultiLingual.DEFAULTS = { + defaultLocale: 'en', + defaultField: null, + placeholderField: null + } + + MultiLingual.prototype.getLocaleElement = function(locale) { + var el = this.$el.find('[data-locale-value="'+locale+'"]') + return el.length ? el : null + } + + MultiLingual.prototype.getLocaleValue = function(locale) { + var value = this.getLocaleElement(locale) + return value ? value.val() : null + } + + MultiLingual.prototype.setLocaleValue = function(value, locale) { + if (locale) { + this.getLocaleElement(locale).val(value) + } + else { + this.$activeField.val(value) + } + } + + MultiLingual.prototype.setLocale = function(locale) { + this.activeLocale = locale + this.$activeField = this.getLocaleElement(locale) + this.$activeButton.text(locale) + + this.$placeholder.val(this.getLocaleValue(locale)) + this.$el.trigger('setLocale.oc.multilingual', [locale, this.getLocaleValue(locale)]) + } + + // MULTILINGUAL PLUGIN DEFINITION + // ============================ + + var old = $.fn.multiLingual + + $.fn.multiLingual = function (option) { + var args = Array.prototype.slice.call(arguments, 1), result + this.each(function () { + var $this = $(this) + var data = $this.data('oc.multilingual') + var options = $.extend({}, MultiLingual.DEFAULTS, $this.data(), typeof option == 'object' && option) + if (!data) $this.data('oc.multilingual', (data = new MultiLingual(this, options))) + if (typeof option == 'string') result = data[option].apply(data, args) + if (typeof result != 'undefined') return false + }) + + return result ? result : this + } + + $.fn.multiLingual.Constructor = MultiLingual + + // MULTILINGUAL NO CONFLICT + // ================= + + $.fn.multiLingual.noConflict = function () { + $.fn.multiLingual = old + return this + } + + // MULTILINGUAL DATA-API + // =============== + $(document).render(function () { + $('[data-control="multilingual"]').multiLingual() + }) + +}(window.jQuery); diff --git a/plugins/rainlab/translate/assets/less/messages.less b/plugins/rainlab/translate/assets/less/messages.less new file mode 100644 index 00000000..7a76a046 --- /dev/null +++ b/plugins/rainlab/translate/assets/less/messages.less @@ -0,0 +1,72 @@ +#messagesContainer { + padding-top: 20px; +} + +.translate-messages { + position: relative; + margin-top: -20px; + + .dropdown-button-placeholder { + display:block; + width: 1px; + height: 20px + } + + .dropdown-to, .dropdown-from { + position: absolute; + top: 57px; + } + + .dropdown-to { + left: 50%; + } + + .dropdown-from { + left: 0; + } + + .no-other-languages { + text-align: center; + padding: 5px; + } + + .header-language, + .header-swap-languages, + .header-hide-translated { + line-height: 27px; + } + + .header-language { + float: left; + cursor: pointer; + + span.is-default { + text-transform: none; + } + } + + .header-swap-languages { + margin-right: 10px; + float: right; + font-size: 16px; + cursor: pointer; + } + + .header-hide-translated { + float: right; + text-align: right; + + label { + font-weight: normal; + margin-bottom: 0; + margin-right: 10px; + font-size: 12px; + white-space: normal; + + &:before { + top: 5px; + left: 0; + } + } + } +} diff --git a/plugins/rainlab/translate/assets/less/multilingual.less b/plugins/rainlab/translate/assets/less/multilingual.less new file mode 100644 index 00000000..c4b9e798 --- /dev/null +++ b/plugins/rainlab/translate/assets/less/multilingual.less @@ -0,0 +1,148 @@ +@import "../../../../../modules/backend/assets/less/core/boot.less"; + +@multilingual-btn-color: #555555; + +.field-multilingual { + position: relative; + + .ml-btn { + background: transparent; + position: absolute; + color: lighten(@multilingual-btn-color, 15%); + text-transform: uppercase; + font-size: 11px; + letter-spacing: 1px; + width: 44px; + padding-right: 0; + .box-shadow(none); + text-shadow: none; + z-index: 2; + + &:hover { + color: @multilingual-btn-color; + } + } + + &.field-multilingual-text { + .form-control { + padding-right: 44px; + } + .ml-btn { + right: 4px; + top: 50%; + margin-top: -44px; + height: 88px; + } + } + + &.field-multilingual-textarea { + textarea { + // increase padding on the right so that the textarea content does not overlap with the language button + padding-right: 30px; + } + + .ml-btn { + right: 25px; + top: 5px; + width: 24px; + text-align: right; + padding-left: 4px; + } + + .ml-dropdown-menu { + top: 39px; + right: 1px; + } + } + + &.field-multilingual-richeditor { + .ml-btn { + top: 43px; + right: 25px; + text-align: right; + } + + .ml-dropdown-menu { + top: 74px; + right: 12px; + } + } + + &.field-multilingual-markdowneditor { + .ml-btn { + top: 43px; + right: 25px; + text-align: right; + } + + .ml-dropdown-menu { + top: 74px !important; + right: 12px; + } + } + + &.field-multilingual-repeater { + .ml-btn { + top: -27px; + right: 11px; + text-align: right; + } + + .ml-dropdown-menu { + top: 0; + right: 0; + } + + &.is-empty { + padding-top: 5px; + + .ml-btn { + top: -10px; + right: 5px; + text-align: right; + } + + .ml-dropdown-menu { + top: 15px; + right: -7px; + } + } + } + + &.field-multilingual-nestedform { + .ml-btn { + top: -3px; + right: 11px; + text-align: right; + } + + .ml-dropdown-menu { + top: 24px; + right: -2px; + } + + &.is-paneled { + .ml-btn { + top: 7px; + right: 15px; + } + + .ml-dropdown-menu { + top: 34px; + right: 2px; + } + } + } +} + +.fancy-layout { + .field-multilingual-text input.form-control { + padding-right: 44px; + } +} + +.help-block.before-field + .field-multilingual.field-multilingual-textarea { + .ml-btn { + top: -41px; + } +} diff --git a/plugins/rainlab/translate/behaviors/TranslatableCmsObject.php b/plugins/rainlab/translate/behaviors/TranslatableCmsObject.php new file mode 100644 index 00000000..f327f2a0 --- /dev/null +++ b/plugins/rainlab/translate/behaviors/TranslatableCmsObject.php @@ -0,0 +1,230 @@ +model->bindEvent('cmsObject.fillViewBagArray', function() { + $this->mergeViewBagAttributes(); + }); + + $this->model->bindEvent('cmsObject.getTwigCacheKey', function($key) { + return $this->overrideTwigCacheKey($key); + }); + + // delete all translation files associated with the default language static page + $this->model->bindEvent('model.afterDelete', function() use ($model) { + foreach (Locale::listEnabled() as $locale => $label) { + if ($locale == $this->translatableDefault) { + continue; + } + if ($obj = $this->getCmsObjectForLocale($locale)) { + $obj->delete(); + } + } + }); + } + + /** + * Merge the viewBag array for the base and translated objects. + * @return void + */ + protected function mergeViewBagAttributes() + { + $locale = $this->translatableContext; + + if (!array_key_exists($locale, $this->translatableAttributes)) { + $this->loadTranslatableData($locale); + } + + if (isset($this->translatableViewBag[$locale])) { + $this->model->viewBag = array_merge( + $this->model->viewBag, + $this->translatableViewBag[$locale] + ); + } + } + + /** + * Translated CMS objects need their own unique cache key in twig. + * @return string|null + */ + protected function overrideTwigCacheKey($key) + { + if (!$locale = $this->translatableContext) { + return null; + } + + return $key . '-' . $locale; + } + + /** + * {@inheritDoc} + */ + public function syncTranslatableAttributes() + { + parent::syncTranslatableAttributes(); + + if ($this->model->isDirty('fileName')) { + $this->syncTranslatableFileNames(); + } + } + + /** + * If the parent model file name is changed, this should + * be reflected in the translated models also. + */ + protected function syncTranslatableFileNames() + { + $knownLocales = array_keys($this->translatableAttributes); + foreach ($knownLocales as $locale) { + if ($locale == $this->translatableDefault) { + continue; + } + + if ($obj = $this->getCmsObjectForLocale($locale)) { + $obj->fileName = $this->model->fileName; + $obj->forceSave(); + } + } + } + + /** + * Saves the translation data in the join table. + * @param string $locale + * @return void + */ + protected function storeTranslatableData($locale = null) + { + if (!$locale) { + $locale = $this->translatableContext; + } + + /* + * Model doesn't exist yet, defer this logic in memory + */ + if (!$this->model->exists) { + $this->model->bindEventOnce('model.afterCreate', function() use ($locale) { + $this->storeTranslatableData($locale); + }); + + return; + } + + $data = $this->translatableAttributes[$locale]; + + if (!$obj = $this->getCmsObjectForLocale($locale)) { + $model = $this->createModel(); + $obj = $model::forLocale($locale, $this->model); + $obj->fileName = $this->model->fileName; + } + + if (!$this->isEmptyDataSet($data)) { + $obj->fill($data); + $obj->forceSave(); + } + } + + /** + * Returns true if all attributes are empty (false when converted to booleans). + * @param array $data + * @return bool + */ + protected function isEmptyDataSet($data) + { + return !array_get($data, 'markup') && + !count(array_filter(array_get($data, 'viewBag', []))) && + !count(array_filter(array_get($data, 'placeholders', []))); + } + + /** + * Loads the translation data from the join table. + * @param string $locale + * @return array + */ + protected function loadTranslatableData($locale = null) + { + if (!$locale) { + $locale = $this->translatableContext; + } + + if (!$this->model->exists) { + return $this->translatableAttributes[$locale] = []; + } + + $obj = $this->getCmsObjectForLocale($locale); + + $result = $obj ? $obj->getAttributes() : []; + + $this->translatableViewBag[$locale] = $obj ? $obj->viewBag : []; + + return $this->translatableOriginals[$locale] = $this->translatableAttributes[$locale] = $result; + } + + protected function getCmsObjectForLocale($locale) + { + if ($locale == $this->translatableDefault) { + return $this->model; + } + + $model = $this->createModel(); + return $model::findLocale($locale, $this->model); + } + + /** + * Internal method, prepare the form model object + * @return Model + */ + protected function createModel() + { + $class = $this->getTranslatableModelClass(); + $model = new $class; + return $model; + } + + /** + * Returns a collection of fields that will be hashed. + * @return array + */ + public function getTranslatableModelClass() + { + if (property_exists($this->model, 'translatableModel')) { + return $this->model->translatableModel; + } + + return 'RainLab\Translate\Classes\MLCmsObject'; + } +} diff --git a/plugins/rainlab/translate/behaviors/TranslatableModel.php b/plugins/rainlab/translate/behaviors/TranslatableModel.php new file mode 100644 index 00000000..36091b2a --- /dev/null +++ b/plugins/rainlab/translate/behaviors/TranslatableModel.php @@ -0,0 +1,319 @@ +morphMany['translations'] = [ + 'RainLab\Translate\Models\Attribute', + 'name' => 'model' + ]; + + // October v2.0 + if (class_exists('System')) { + $this->extendFileModels('attachOne'); + $this->extendFileModels('attachMany'); + } + + // Clean up indexes when this model is deleted + $model->bindEvent('model.afterDelete', function() use ($model) { + Db::table('rainlab_translate_attributes') + ->where('model_id', $model->getKey()) + ->where('model_type', get_class($model)) + ->delete(); + + Db::table('rainlab_translate_indexes') + ->where('model_id', $model->getKey()) + ->where('model_type', get_class($model)) + ->delete(); + }); + } + + /** + * extendFileModels will swap the standard File model with MLFile instead + */ + protected function extendFileModels(string $relationGroup) + { + foreach ($this->model->$relationGroup as $relationName => $relationObj) { + $relationClass = is_array($relationObj) ? $relationObj[0] : $relationObj; + if ($relationClass === \System\Models\File::class) { + if (is_array($relationObj)) { + $this->model->$relationGroup[$relationName][0] = \RainLab\Translate\Models\MLFile::class; + } + else { + $this->model->$relationGroup[$relationName] = \RainLab\Translate\Models\MLFile::class; + } + } + } + } + + /** + * Applies a translatable index to a basic query. This scope will join the index + * table and can be executed neither more than once, nor with scopeTransOrder. + * @param Builder $query + * @param string $index + * @param string $value + * @param string $locale + * @return Builder + */ + public function scopeTransWhere($query, $index, $value, $locale = null, $operator = '=') + { + if (!$locale) { + $locale = $this->translatableContext; + } + + // Separate query into two separate queries for improved performance + // @see https://github.com/rainlab/translate-plugin/pull/623 + $translateIndexes = Db::table('rainlab_translate_indexes') + ->where('rainlab_translate_indexes.model_type', '=', $this->getClass()) + ->where('rainlab_translate_indexes.locale', '=', $locale) + ->where('rainlab_translate_indexes.item', $index) + ->where('rainlab_translate_indexes.value', $operator, $value) + ->pluck('model_id'); + + if ($translateIndexes->count()) { + $query->whereIn($this->model->getQualifiedKeyName(), $translateIndexes); + } else { + $query->where($index, $operator, $value); + } + + return $query; + } + + /** + * Applies a sort operation with a translatable index to a basic query. This scope will join the index table. + * @param Builder $query + * @param string $index + * @param string $direction + * @param string $locale + * @return Builder + */ + public function scopeTransOrderBy($query, $index, $direction = 'asc', $locale = null) + { + if (!$locale) { + $locale = $this->translatableContext; + } + $indexTableAlias = 'rainlab_translate_indexes_' . $index . '_' . $locale; + + $query->select( + $this->model->getTable().'.*', + Db::raw('COALESCE(' . $indexTableAlias . '.value, '. $this->model->getTable() .'.'.$index.') AS translate_sorting_key') + ); + + $query->orderBy('translate_sorting_key', $direction); + + $this->joinTranslateIndexesTable($query, $locale, $index, $indexTableAlias); + + return $query; + } + + /** + * Joins the translatable indexes table to a query. + * @param Builder $query + * @param string $locale + * @param string $indexTableAlias + * @return Builder + */ + protected function joinTranslateIndexesTable($query, $locale, $index, $indexTableAlias) + { + $joinTableWithAlias = 'rainlab_translate_indexes as ' . $indexTableAlias; + // check if table with same name and alias is already joined + if (collect($query->getQuery()->joins)->contains('table', $joinTableWithAlias)) { + return $query; + } + + $query->leftJoin($joinTableWithAlias, function($join) use ($locale, $index, $indexTableAlias) { + $join + ->on(Db::raw(DbDongle::cast($this->model->getQualifiedKeyName(), 'TEXT')), '=', $indexTableAlias . '.model_id') + ->where($indexTableAlias . '.model_type', '=', $this->getClass()) + ->where($indexTableAlias . '.item', '=', $index) + ->where($indexTableAlias . '.locale', '=', $locale); + }); + + return $query; + } + + /** + * Saves the translation data in the join table. + * @param string $locale + * @return void + */ + protected function storeTranslatableData($locale = null) + { + if (!$locale) { + $locale = $this->translatableContext; + } + + /* + * Model doesn't exist yet, defer this logic in memory + */ + if (!$this->model->exists) { + $this->model->bindEventOnce('model.afterCreate', function() use ($locale) { + $this->storeTranslatableData($locale); + }); + + return; + } + + /** + * @event model.translate.resolveComputedFields + * Resolve computed fields before saving + * + * Example usage: + * + * Override Model's __construct method + * + * public function __construct(array $attributes = []) + * { + * parent::__construct($attributes); + * + * $this->bindEvent('model.translate.resolveComputedFields', function ($locale) { + * return [ + * 'content_html' => + * self::formatHtml($this->asExtension('TranslatableModel') + * ->getAttributeTranslated('content', $locale)) + * ]; + * }); + * } + * + */ + $computedFields = $this->model->fireEvent('model.translate.resolveComputedFields', [$locale], true); + if (is_array($computedFields)) { + $this->translatableAttributes[$locale] = array_merge($this->translatableAttributes[$locale], $computedFields); + } + + $this->storeTranslatableBasicData($locale); + $this->storeTranslatableIndexData($locale); + } + + /** + * Saves the basic translation data in the join table. + * @param string $locale + * @return void + */ + protected function storeTranslatableBasicData($locale = null) + { + $data = json_encode($this->translatableAttributes[$locale], JSON_UNESCAPED_UNICODE); + + $obj = Db::table('rainlab_translate_attributes') + ->where('locale', $locale) + ->where('model_id', $this->model->getKey()) + ->where('model_type', $this->getClass()); + + if ($obj->count() > 0) { + $obj->update(['attribute_data' => $data]); + } + else { + Db::table('rainlab_translate_attributes')->insert([ + 'locale' => $locale, + 'model_id' => $this->model->getKey(), + 'model_type' => $this->getClass(), + 'attribute_data' => $data + ]); + } + } + + /** + * Saves the indexed translation data in the join table. + * @param string $locale + * @return void + */ + protected function storeTranslatableIndexData($locale = null) + { + $optionedAttributes = $this->getTranslatableAttributesWithOptions(); + if (!count($optionedAttributes)) { + return; + } + + $data = $this->translatableAttributes[$locale]; + + foreach ($optionedAttributes as $attribute => $options) { + if (!array_get($options, 'index', false)) { + continue; + } + + $value = array_get($data, $attribute); + + $obj = Db::table('rainlab_translate_indexes') + ->where('locale', $locale) + ->where('model_id', $this->model->getKey()) + ->where('model_type', $this->getClass()) + ->where('item', $attribute); + + $recordExists = $obj->count() > 0; + + if (!strlen($value)) { + if ($recordExists) { + $obj->delete(); + } + continue; + } + + if ($recordExists) { + $obj->update(['value' => $value]); + } + else { + Db::table('rainlab_translate_indexes')->insert([ + 'locale' => $locale, + 'model_id' => $this->model->getKey(), + 'model_type' => $this->getClass(), + 'item' => $attribute, + 'value' => $value + ]); + } + } + } + + /** + * Loads the translation data from the join table. + * @param string $locale + * @return array + */ + protected function loadTranslatableData($locale = null) + { + if (!$locale) { + $locale = $this->translatableContext; + } + + if (!$this->model->exists) { + return $this->translatableAttributes[$locale] = []; + } + + $obj = $this->model->translations->first(function ($value, $key) use ($locale) { + return $value->attributes['locale'] === $locale; + }); + + $result = $obj ? json_decode($obj->attribute_data, true) : []; + + return $this->translatableOriginals[$locale] = $this->translatableAttributes[$locale] = $result; + } + + /** + * Returns the class name of the model. Takes any + * custom morphMap aliases into account. + * + * @return string + */ + protected function getClass() + { + return $this->model->getMorphClass(); + } +} diff --git a/plugins/rainlab/translate/behaviors/TranslatablePage.php b/plugins/rainlab/translate/behaviors/TranslatablePage.php new file mode 100644 index 00000000..4797e29e --- /dev/null +++ b/plugins/rainlab/translate/behaviors/TranslatablePage.php @@ -0,0 +1,140 @@ +model->bindEvent('model.afterFetch', function() { + $this->translatableOriginals = $this->getModelAttributes(); + + if (!App::runningInBackend()) { + $this->rewriteTranslatablePageAttributes(); + } + }); + } + + public function isTranslatable($key) + { + if ($key === 'translatable' || $this->translatableDefault == $this->translatableContext) { + return false; + } + + return in_array($key, $this->model->translatable); + } + + public function getTranslatableAttributes() + { + $attributes = []; + + foreach ($this->model->translatable as $attr) { + $attributes[] = 'settings['.$attr.']'; + } + + return $attributes; + } + + public function getModelAttributes() + { + $attributes = []; + + foreach ($this->model->translatable as $attr) { + $attributes[$attr] = $this->model[$attr]; + } + + return $attributes; + } + + public function initTranslatableContext() + { + parent::initTranslatableContext(); + $this->translatableOriginals = $this->getModelAttributes(); + } + + public function rewriteTranslatablePageAttributes($locale = null) + { + $locale = $locale ?: $this->translatableContext; + + foreach ($this->model->translatable as $attr) { + $localeAttr = $this->translatableOriginals[$attr]; + + if ($locale != $this->translatableDefault) { + $translated = $this->getAttributeTranslated($attr, $locale); + $localeAttr = ($translated ?: $this->translatableUseFallback) ? $localeAttr : null; + } + + $this->model[$attr] = $localeAttr; + } + } + + public function getAttributeTranslated($key, $locale = null) + { + $locale = $locale ?: $this->translatableContext; + + if (strpbrk($key, '[]') !== false) { + // Retrieve attr name within brackets (i.e. settings[title] yields title) + $key = preg_split("/[\[\]]/", $key)[1]; + } + + $default = ($locale == $this->translatableDefault || $this->translatableUseFallback) + ? array_get($this->translatableOriginals, $key) + : ''; + + $localeAttr = sprintf('viewBag.locale%s.%s', ucfirst($key), $locale); + return array_get($this->model->attributes, $localeAttr, $default); + } + + public function setAttributeTranslated($key, $value, $locale = null) + { + $locale = $locale ?: $this->translatableContext; + + if ($locale == $this->translatableDefault) { + return; + } + + if (strpbrk($key, '[]') !== false) { + // Retrieve attr name within brackets (i.e. settings[title] yields title) + $key = preg_split("/[\[\]]/", $key)[1]; + } + + if ($value == array_get($this->translatableOriginals, $key)) { + return; + } + + $this->saveTranslation($key, $value, $locale); + $this->model->bindEventOnce('model.beforeSave', function() use ($key, $value, $locale) { + $this->saveTranslation($key, $value, $locale); + }); + } + + public function saveTranslation($key, $value, $locale) + { + $localeAttr = sprintf('viewBag.locale%s.%s', ucfirst($key), $locale); + if (!$value) { + array_forget($this->model->attributes, $localeAttr); + } + else { + array_set($this->model->attributes, $localeAttr, $value); + } + } + + // Not needed but parent abstract model requires those + protected function storeTranslatableData($locale = null) {} + protected function loadTranslatableData($locale = null) {} +} diff --git a/plugins/rainlab/translate/behaviors/TranslatablePageUrl.php b/plugins/rainlab/translate/behaviors/TranslatablePageUrl.php new file mode 100644 index 00000000..bbc7147e --- /dev/null +++ b/plugins/rainlab/translate/behaviors/TranslatablePageUrl.php @@ -0,0 +1,173 @@ +model = $model; + + $this->initTranslatableContext(); + + $this->model->bindEvent('model.afterFetch', function() { + $this->translatableDefaultUrl = $this->getModelUrl(); + + if (!App::runningInBackend()) { + $this->rewriteTranslatablePageUrl(); + } + }); + } + + protected function setModelUrl($value) + { + if ($this->model instanceof \RainLab\Pages\Classes\Page) { + array_set($this->model->attributes, 'viewBag.url', $value); + } + else { + $this->model->url = $value; + } + } + + protected function getModelUrl() + { + if ($this->model instanceof \RainLab\Pages\Classes\Page) { + return array_get($this->model->attributes, 'viewBag.url'); + } + else { + return $this->model->url; + } + } + + /** + * Initializes this class, sets the default language code to use. + * @return void + */ + public function initTranslatableContext() + { + $translate = Translator::instance(); + $this->translatableContext = $translate->getLocale(); + $this->translatableDefault = $translate->getDefaultLocale(); + } + + /** + * Checks if a translated URL exists and rewrites it, this method + * should only be called from the context of front-end. + * @return void + */ + public function rewriteTranslatablePageUrl($locale = null) + { + $locale = $locale ?: $this->translatableContext; + $localeUrl = $this->translatableDefaultUrl; + + if ($locale != $this->translatableDefault) { + $localeUrl = $this->getSettingsUrlAttributeTranslated($locale) ?: $localeUrl; + } + + $this->setModelUrl($localeUrl); + } + + /** + * Determines if a locale has a translated URL. + * @return bool + */ + public function hasTranslatablePageUrl($locale = null) + { + $locale = $locale ?: $this->translatableContext; + + return strlen($this->getSettingsUrlAttributeTranslated($locale)) > 0; + } + + /** + * Mutator detected by MLControl + * @return string + */ + public function getSettingsUrlAttributeTranslated($locale) + { + $defaults = ($locale == $this->translatableDefault) ? $this->translatableDefaultUrl : null; + + return array_get($this->model->attributes, 'viewBag.localeUrl.'.$locale, $defaults); + } + + /** + * Mutator detected by MLControl + * @return void + */ + public function setSettingsUrlAttributeTranslated($value, $locale) + { + if ($locale == $this->translatableDefault) { + return; + } + + if ($value == $this->translatableDefaultUrl) { + return; + } + + /* + * The CMS controller will purge attributes just before saving, this + * will ensure the attributes are injected after this logic. + */ + $this->model->bindEventOnce('model.beforeSave', function() use ($value, $locale) { + if (!$value) { + array_forget($this->model->attributes, 'viewBag.localeUrl.'.$locale); + } + else { + array_set($this->model->attributes, 'viewBag.localeUrl.'.$locale, $value); + } + }); + } + + /** + * Mutator detected by MLControl, proxy for Static Pages plugin. + * @return string + */ + public function getViewBagUrlAttributeTranslated($locale) + { + return $this->getSettingsUrlAttributeTranslated($locale); + } + + /** + * Mutator detected by MLControl, proxy for Static Pages plugin. + * @return void + */ + public function setViewBagUrlAttributeTranslated($value, $locale) + { + $this->setSettingsUrlAttributeTranslated($value, $locale); + } +} diff --git a/plugins/rainlab/translate/classes/EventRegistry.php b/plugins/rainlab/translate/classes/EventRegistry.php new file mode 100644 index 00000000..262bf917 --- /dev/null +++ b/plugins/rainlab/translate/classes/EventRegistry.php @@ -0,0 +1,408 @@ +code ?? null; + + $properties = []; + foreach ($locales as $locale => $label) { + if ($locale == $defaultLocale) { + continue; + } + + $properties[] = [ + 'property' => 'localeUrl.'.$locale, + 'title' => 'cms::lang.editor.url', + 'tab' => $label, + 'type' => 'string', + ]; + + $properties[] = [ + 'property' => 'localeTitle.'.$locale, + 'title' => 'cms::lang.editor.title', + 'tab' => $label, + 'type' => 'string', + ]; + + $properties[] = [ + 'property' => 'localeDescription.'.$locale, + 'title' => 'cms::lang.editor.description', + 'tab' => $label, + 'type' => 'text', + ]; + + $properties[] = [ + 'property' => 'localeMeta_title.'.$locale, + 'title' => 'cms::lang.editor.meta_title', + 'tab' => $label, + 'type' => 'string', + ]; + + $properties[] = [ + 'property' => 'localeMeta_description.'.$locale, + 'title' => 'cms::lang.editor.meta_description', + 'tab' => $label, + 'type' => 'text', + ]; + } + + $dataHolder->buttons[] = [ + 'button' => 'rainlab.translate::lang.plugin.name', + 'icon' => 'octo-icon-globe', + 'popupTitle' => 'Translate Page Properties', + 'properties' => $properties + ]; + } + + // + // Utility + // + + /** + * registerFormFieldReplacements + */ + public function registerFormFieldReplacements($widget) + { + // Replace with ML Controls for translatable attributes + $this->registerModelTranslation($widget); + + // Handle URL translations + $this->registerPageUrlTranslation($widget); + + // Handle RainLab.Pages MenuItem translations + if (PluginManager::instance()->exists('RainLab.Pages')) { + $this->registerMenuItemTranslation($widget); + } + } + + /** + * registerMenuItemTranslation for RainLab.Pages MenuItem data + * @param Backend\Widgets\Form $widget + */ + public function registerMenuItemTranslation($widget) + { + if ($widget->model instanceof \RainLab\Pages\Classes\MenuItem) { + $defaultLocale = LocaleModel::getDefault(); + $availableLocales = LocaleModel::listAvailable(); + $fieldsToTranslate = ['title', 'url']; + + // Replace specified fields with multilingual versions + foreach ($fieldsToTranslate as $fieldName) { + $widget->fields[$fieldName]['type'] = 'mltext'; + + foreach ($availableLocales as $code => $locale) { + if (!$defaultLocale || $defaultLocale->code === $code) { + continue; + } + + // Add data locker fields for the different locales under the `viewBag[locale]` property + $widget->fields["viewBag[locale][$code][$fieldName]"] = [ + 'cssClass' => 'hidden', + 'attributes' => [ + 'data-locale' => $code, + 'data-field-name' => $fieldName, + ], + ]; + } + } + } + } + + // + // Translate URLs + // + + /** + * registerPageUrlTranslation + */ + public function registerPageUrlTranslation($widget) + { + if (!$model = $widget->model) { + return; + } + + if ( + $model instanceof Page && + isset($widget->fields['settings[url]']) + ) { + $widget->fields['settings[url]']['type'] = 'mltext'; + } + elseif ( + $model instanceof \RainLab\Pages\Classes\Page && + isset($widget->fields['viewBag[url]']) + ) { + $widget->fields['viewBag[url]']['type'] = 'mltext'; + } + } + + // + // Translatable behavior + // + + /** + * registerModelTranslation automatically replaces form fields for multi-lingual equivalents + */ + public function registerModelTranslation($widget) + { + if ($widget->isNested) { + return; + } + + if (!$model = $widget->model) { + return; + } + + if (!method_exists($model, 'isClassExtendedWith')) { + return; + } + + if ( + !$model->isClassExtendedWith(\RainLab\Translate\Behaviors\TranslatableModel::class) && + !$model->isClassExtendedWith(\RainLab\Translate\Behaviors\TranslatablePage::class) && + !$model->isClassExtendedWith(\RainLab\Translate\Behaviors\TranslatableCmsObject::class) + ) { + return; + } + + if (!$model->hasTranslatableAttributes()) { + return; + } + + if (!empty($widget->fields)) { + $widget->fields = $this->processFormMLFields($widget->fields, $model); + } + + if (!empty($widget->tabs['fields'])) { + $widget->tabs['fields'] = $this->processFormMLFields($widget->tabs['fields'], $model); + } + + if (!empty($widget->secondaryTabs['fields'])) { + $widget->secondaryTabs['fields'] = $this->processFormMLFields($widget->secondaryTabs['fields'], $model); + } + } + + /** + * processFormMLFields function to replace standard fields with multi-lingual equivalents + * @param array $fields + * @param Model $model + * @return array + */ + protected function processFormMLFields($fields, $model) + { + $typesMap = [ + 'text' => 'mltext', + 'textarea' => 'mltextarea', + 'richeditor' => 'mlricheditor', + 'markdown' => 'mlmarkdowneditor', + 'repeater' => 'mlrepeater', + 'nestedform' => 'mlnestedform', + 'mediafinder' => 'mlmediafinder', + ]; + + $translatable = array_flip($model->getTranslatableAttributes()); + + /* + * Special: A custom field "markup_html" is used for Content templates. + */ + if ($model instanceof Content && array_key_exists('markup', $translatable)) { + $translatable['markup_html'] = true; + } + + foreach ($fields as $name => $config) { + if (!array_key_exists($name, $translatable)) { + continue; + } + + $type = array_get($config, 'type', 'text'); + + if (array_key_exists($type, $typesMap)) { + $fields[$name]['type'] = $typesMap[$type]; + } + } + + return $fields; + } + + // + // Theme + // + + /** + * importMessagesFromTheme + */ + public function importMessagesFromTheme($themeCode) + { + try { + (new ThemeScanner)->scanThemeConfigForMessages($themeCode); + } + catch (Exception $ex) {} + } + + // + // CMS objects + // + + /** + * setMessageContext for translation caching. + */ + public function setMessageContext($page) + { + if (!$page) { + return; + } + + $translator = Translator::instance(); + + Message::setContext($translator->getLocale(), $page->url); + } + + /** + * findTranslatedContentFile adds language suffixes to content files. + * @return string|null + */ + public function findTranslatedContentFile($controller, $fileName) + { + if (!strlen(File::extension($fileName))) { + $fileName .= '.htm'; + } + + /* + * Splice the active locale in to the filename + * - content.htm -> content.en.htm + */ + $locale = Translator::instance()->getLocale(); + $fileName = substr_replace($fileName, '.'.$locale, strrpos($fileName, '.'), 0); + if (($content = Content::loadCached($controller->getTheme(), $fileName)) !== null) { + return $content; + } + } + + // + // Static pages + // + + /** + * pruneTranslatedContentTemplates removes localized content files from templates collection + * @param \October\Rain\Database\Collection $templates + * @return \October\Rain\Database\Collection + */ + public function pruneTranslatedContentTemplates($templates) + { + $locales = LocaleModel::listAvailable(); + + $extensions = array_map(function($ext) { + return '.'.$ext; + }, array_keys($locales)); + + return $templates->filter(function($template) use ($extensions) { + return !Str::endsWith($template->getBaseFileName(), $extensions); + }); + } + + /** + * findLocalizedMailViewContent adds language suffixes to mail view files. + * @param \October\Rain\Mail\Mailer $mailer + * @param \Illuminate\Mail\Message $message + * @param string $view + * @param array $data + * @param string $raw + * @param string $plain + * @return bool|void Will return false if the translation process successfully replaced the original message with a translated version to prevent the original version from being processed. + */ + public function findLocalizedMailViewContent($mailer, $message, $view, $data, $raw, $plain) + { + // Raw content cannot be localized at this level + if (!empty($raw)) { + return; + } + + // Get the locale to use for this template + $locale = !empty($data['_current_locale']) ? $data['_current_locale'] : App::getLocale(); + + $factory = $mailer->getViewFactory(); + + if (!empty($view)) { + $view = $this->getLocalizedView($factory, $view, $locale); + } + + if (!empty($plain)) { + $plain = $this->getLocalizedView($factory, $plain, $locale); + } + + $code = $view ?: $plain; + if (empty($code)) { + return null; + } + + $plainOnly = empty($view); + + if (MailManager::instance()->addContentToMailer($message, $code, $data, $plainOnly)) { + // the caller who fired the event is expecting a FALSE response to halt the event + return false; + } + } + + /** + * getLocalizedView searches mail view files based on locale + * @param \October\Rain\Mail\Mailer $mailer + * @param \Illuminate\Mail\Message $message + * @param string $code + * @param string $locale + * @return string|null + */ + public function getLocalizedView($factory, $code, $locale) + { + $locale = strtolower($locale); + + $searchPaths[] = $locale; + + if (str_contains($locale, '-')) { + list($lang) = explode('-', $locale); + $searchPaths[] = $lang; + } + + foreach ($searchPaths as $path) { + $localizedView = sprintf('%s-%s', $code, $path); + + if ($factory->exists($localizedView)) { + return $localizedView; + } + } + return null; + } +} diff --git a/plugins/rainlab/translate/classes/LocaleMiddleware.php b/plugins/rainlab/translate/classes/LocaleMiddleware.php new file mode 100644 index 00000000..20d4f036 --- /dev/null +++ b/plugins/rainlab/translate/classes/LocaleMiddleware.php @@ -0,0 +1,31 @@ +isConfigured(); + + if (!$translator->loadLocaleFromRequest()) { + if (Config::get('rainlab.translate::prefixDefaultLocale')) { + $translator->loadLocaleFromSession(); + } else { + $translator->setLocale($translator->getDefaultLocale()); + } + } + + return $next($request); + } +} diff --git a/plugins/rainlab/translate/classes/MLCmsObject.php b/plugins/rainlab/translate/classes/MLCmsObject.php new file mode 100644 index 00000000..2dce2a04 --- /dev/null +++ b/plugins/rainlab/translate/classes/MLCmsObject.php @@ -0,0 +1,56 @@ +theme); + } + + public static function findLocale($locale, $page) + { + return static::forLocale($locale, $page)->find($page->fileName); + } + + /** + * Returns the directory name corresponding to the object type. + * For pages the directory name is "pages", for layouts - "layouts", etc. + * @return string + */ + public function getObjectTypeDirName() + { + $dirName = static::$parent->getObjectTypeDirName(); + + if (strlen(static::$locale)) { + $dirName .= '-' . static::$locale; + } + + return $dirName; + } +} diff --git a/plugins/rainlab/translate/classes/MLContent.php b/plugins/rainlab/translate/classes/MLContent.php new file mode 100644 index 00000000..0402d8d1 --- /dev/null +++ b/plugins/rainlab/translate/classes/MLContent.php @@ -0,0 +1,70 @@ +exists) { + return null; + } + + $fileName = $page->getOriginal('fileName') ?: $page->fileName; + + $fileName = static::addLocaleToFileName($fileName, $locale); + + return static::forLocale($locale, $page)->find($fileName); + } + + /** + * Returns the directory name corresponding to the object type. + * Content does not use localized sub directories, but as file suffix instead. + * @return string + */ + public function getObjectTypeDirName() + { + return static::$parent->getObjectTypeDirName(); + } + + /** + * Splice in the locale when setting the file name. + * @param mixed $value + */ + public function setFileNameAttribute($value) + { + $value = static::addLocaleToFileName($value, static::$locale); + + parent::setFileNameAttribute($value); + } + + /** + * Splice the active locale in to the filename + * - content.htm -> content.en.htm + */ + protected static function addLocaleToFileName($fileName, $locale) + { + /* + * Check locale not already present + */ + $parts = explode('.', $fileName); + array_shift($parts); + + foreach ($parts as $part) { + if (trim($part) === $locale) { + return $fileName; + } + } + + return substr_replace($fileName, '.'.$locale, strrpos($fileName, '.'), 0); + } +} diff --git a/plugins/rainlab/translate/classes/MLStaticPage.php b/plugins/rainlab/translate/classes/MLStaticPage.php new file mode 100644 index 00000000..0fba7700 --- /dev/null +++ b/plugins/rainlab/translate/classes/MLStaticPage.php @@ -0,0 +1,118 @@ +getPlaceholdersAttribute(); + } + + /** + * Parses the page placeholder {% put %} tags and extracts the placeholder values. + * @return array Returns an associative array of the placeholder names and values. + */ + public function getPlaceholdersAttribute() + { + if (!strlen($this->code)) { + return []; + } + + if ($placeholders = array_get($this->attributes, 'placeholders')) { + return $placeholders; + } + + $bodyNode = $this->getTwigNodeTree($this->code)->getNode('body')->getNode(0); + if ($bodyNode instanceof \Cms\Twig\PutNode) { + $bodyNode = [$bodyNode]; + } + + $result = []; + foreach ($bodyNode as $node) { + if (!$node instanceof \Cms\Twig\PutNode) { + continue; + } + + // October CMS v2.2 and above + if (class_exists('System') && version_compare(\System::VERSION, '2.1') === 1) { + $names = $node->getNode('names'); + $values = $node->getNode('values'); + $isCapture = $node->getAttribute('capture'); + if ($isCapture) { + $name = $names->getNode(0); + $result[$name->getAttribute('name')] = trim($values->getAttribute('data')); + } + } + // Legacy PutNode support + else { + $values = $node->getNode('body'); + $result[$node->getAttribute('name')] = trim($values->getAttribute('data')); + } + } + + $this->attributes['placeholders'] = $result; + + return $result; + } + + /** + * Takes an array of placeholder data (key: code, value: content) and renders + * it as a single string of Twig markup against the "code" attribute. + * @param array $value + * @return void + */ + public function setPlaceholdersAttribute($value) + { + if (!is_array($value)) { + return; + } + + $placeholders = $value; + $result = ''; + + foreach ($placeholders as $code => $content) { + if (!strlen($content)) { + continue; + } + + $result .= '{% put '.$code.' %}'.PHP_EOL; + $result .= $content.PHP_EOL; + $result .= '{% endput %}'.PHP_EOL; + $result .= PHP_EOL; + } + + $this->attributes['code'] = trim($result); + $this->attributes['placeholders'] = $placeholders; + } + + /** + * Disables safe mode check for static pages. + * + * This allows developers to use placeholders in layouts even if safe mode is enabled. + * + * @return void + */ + protected function checkSafeMode() + { + } +} diff --git a/plugins/rainlab/translate/classes/ThemeScanner.php b/plugins/rainlab/translate/classes/ThemeScanner.php new file mode 100644 index 00000000..5341e517 --- /dev/null +++ b/plugins/rainlab/translate/classes/ThemeScanner.php @@ -0,0 +1,227 @@ +scanForMessages(); + + /** + * @event rainlab.translate.themeScanner.afterScan + * Fires after theme scanning. + * + * Example usage: + * + * Event::listen('rainlab.translate.themeScanner.afterScan', function (ThemeScanner $scanner) { + * // added an extra scan. Add generation files... + * }); + * + */ + Event::fire('rainlab.translate.themeScanner.afterScan', [$obj]); + } + + /** + * Scans theme templates and config for messages. + * @return void + */ + public function scanForMessages() + { + // Set all messages initially as being not found. The scanner later + // sets the entries it finds as found. + Message::query()->update(['found' => false]); + + $this->scanThemeConfigForMessages(); + $this->scanThemeTemplatesForMessages(); + $this->scanMailTemplatesForMessages(); + } + + /** + * Scans the theme configuration for defined messages + * @return void + */ + public function scanThemeConfigForMessages($themeCode = null) + { + if (!$themeCode) { + $theme = Theme::getActiveTheme(); + + if (!$theme) { + return; + } + } + else { + if (!Theme::exists($themeCode)) { + return; + } + + $theme = Theme::load($themeCode); + } + + // October v2.0 + if (class_exists('System') && $theme->hasParentTheme()) { + $parentTheme = $theme->getParentTheme(); + + try { + $this->scanThemeConfigForMessagesInternal($theme); + } + catch (Exception $ex) { + $this->scanThemeConfigForMessagesInternal($parentTheme); + } + } + else { + $this->scanThemeConfigForMessagesInternal($theme); + } + } + + /** + * scanThemeConfigForMessagesInternal + */ + protected function scanThemeConfigForMessagesInternal(Theme $theme) + { + $config = $theme->getConfigArray('translate'); + + if (!count($config)) { + return; + } + + $translator = Translator::instance(); + $keys = []; + + foreach ($config as $locale => $messages) { + if (is_string($messages)) { + // $message is a yaml filename, load the yaml file + $messages = $theme->getConfigArray('translate.'.$locale); + } + $keys = array_merge($keys, array_keys($messages)); + } + + Message::importMessages($keys); + + foreach ($config as $locale => $messages) { + if (is_string($messages)) { + // $message is a yaml filename, load the yaml file + $messages = $theme->getConfigArray('translate.'.$locale); + } + Message::importMessageCodes($messages, $locale); + } + } + + /** + * Scans the theme templates for message references. + * @return void + */ + public function scanThemeTemplatesForMessages() + { + $messages = []; + + foreach (Layout::all() as $layout) { + $messages = array_merge($messages, $this->parseContent($layout->markup)); + } + + foreach (Page::all() as $page) { + $messages = array_merge($messages, $this->parseContent($page->markup)); + } + + foreach (Partial::all() as $partial) { + $messages = array_merge($messages, $this->parseContent($partial->markup)); + } + + Message::importMessages($messages); + } + + /** + * Scans the mail templates for message references. + * @return void + */ + public function scanMailTemplatesForMessages() + { + $messages = []; + + foreach (MailTemplate::allTemplates() as $mailTemplate) { + $messages = array_merge($messages, $this->parseContent($mailTemplate->subject)); + $messages = array_merge($messages, $this->parseContent($mailTemplate->content_html)); + } + + Message::importMessages($messages); + } + + /** + * Parse the known language tag types in to messages. + * @param string $content + * @return array + */ + protected function parseContent($content) + { + $messages = []; + $messages = array_merge($messages, $this->processStandardTags($content)); + + return $messages; + } + + /** + * Process standard language filter tag (_|) + * @param string $content + * @return array + */ + protected function processStandardTags($content) + { + $messages = []; + + /* + * Regex used: + * + * {{'AJAX framework'|_}} + * {{\s*'([^'])+'\s*[|]\s*_\s*}} + * + * {{'AJAX framework'|_(variables)}} + * {{\s*'([^'])+'\s*[|]\s*_\s*\([^\)]+\)\s*}} + */ + + $quoteChar = preg_quote("'"); + + preg_match_all('#{{\s*'.$quoteChar.'([^'.$quoteChar.']+)'.$quoteChar.'\s*[|]\s*_\s*(?:[|].+)?}}#', $content, $match); + if (isset($match[1])) { + $messages = array_merge($messages, $match[1]); + } + + preg_match_all('#{{\s*'.$quoteChar.'([^'.$quoteChar.']+)'.$quoteChar.'\s*[|]\s*_\s*\([^\)]+\)\s*}}#', $content, $match); + if (isset($match[1])) { + $messages = array_merge($messages, $match[1]); + } + + $quoteChar = preg_quote('"'); + + preg_match_all('#{{\s*'.$quoteChar.'([^'.$quoteChar.']+)'.$quoteChar.'\s*[|]\s*_\s*(?:[|].+)?}}#', $content, $match); + if (isset($match[1])) { + $messages = array_merge($messages, $match[1]); + } + + preg_match_all('#{{\s*'.$quoteChar.'([^'.$quoteChar.']+)'.$quoteChar.'\s*[|]\s*_\s*\([^\)]+\)\s*}}#', $content, $match); + if (isset($match[1])) { + $messages = array_merge($messages, $match[1]); + } + + return $messages; + } +} diff --git a/plugins/rainlab/translate/classes/TranslatableBehavior.php b/plugins/rainlab/translate/classes/TranslatableBehavior.php new file mode 100644 index 00000000..3ae9e65a --- /dev/null +++ b/plugins/rainlab/translate/classes/TranslatableBehavior.php @@ -0,0 +1,494 @@ +model = $model; + + $this->initTranslatableContext(); + + $this->model->bindEvent('model.beforeGetAttribute', function ($key) use ($model) { + if ($this->isTranslatable($key)) { + $value = $this->getAttributeTranslated($key); + if ($model->hasGetMutator($key)) { + $method = 'get' . Str::studly($key) . 'Attribute'; + $value = $model->{$method}($value); + } + return $value; + } + }); + + $this->model->bindEvent('model.beforeSetAttribute', function ($key, $value) use ($model) { + if ($this->isTranslatable($key)) { + $value = $this->setAttributeTranslated($key, $value); + if ($model->hasSetMutator($key)) { + $method = 'set' . Str::studly($key) . 'Attribute'; + $value = $model->{$method}($value); + } + return $value; + } + }); + + $this->model->bindEvent('model.saveInternal', function() { + $this->syncTranslatableAttributes(); + }); + } + + /** + * Initializes this class, sets the default language code to use. + * @return void + */ + public function initTranslatableContext() + { + $translate = Translator::instance(); + $this->translatableContext = $translate->getLocale(); + $this->translatableDefault = $translate->getDefaultLocale(); + } + + /** + * Checks if an attribute should be translated or not. + * @param string $key + * @return boolean + */ + public function isTranslatable($key) + { + if ($key === 'translatable' || $this->translatableDefault == $this->translatableContext) { + return false; + } + + return in_array($key, $this->model->getTranslatableAttributes()); + } + + /** + * Disables translation fallback locale. + * @return self + */ + public function noFallbackLocale() + { + $this->translatableUseFallback = false; + + return $this->model; + } + + /** + * Enables translation fallback locale. + * @return self + */ + public function withFallbackLocale() + { + $this->translatableUseFallback = true; + + return $this->model; + } + + /** + * Returns a translated attribute value. + * + * The base value must come from 'attributes' on the model otherwise the process + * can possibly loop back to this event, then method triggered by __get() magic. + * + * @param string $key + * @param string $locale + * @return string + */ + public function getAttributeTranslated($key, $locale = null) + { + if ($locale == null) { + $locale = $this->translatableContext; + } + + /* + * Result should not return NULL to successfully hook beforeGetAttribute event + */ + $result = ''; + + /* + * Default locale + */ + if ($locale == $this->translatableDefault) { + $result = $this->getAttributeFromData($this->model->attributes, $key); + } + /* + * Other locale + */ + else { + if (!array_key_exists($locale, $this->translatableAttributes)) { + $this->loadTranslatableData($locale); + } + + if ($this->hasTranslation($key, $locale)) { + $result = $this->getAttributeFromData($this->translatableAttributes[$locale], $key); + } + elseif ($this->translatableUseFallback) { + $result = $this->getAttributeFromData($this->model->attributes, $key); + } + } + + /* + * Handle jsonable attributes, default locale may return the value as a string + */ + if ( + is_string($result) && + method_exists($this->model, 'isJsonable') && + $this->model->isJsonable($key) + ) { + $result = json_decode($result, true); + } + + return $result; + } + + /** + * Returns all translated attribute values. + * @param string $locale + * @return array + */ + public function getTranslateAttributes($locale) + { + if (!array_key_exists($locale, $this->translatableAttributes)) { + $this->loadTranslatableData($locale); + } + + return array_get($this->translatableAttributes, $locale, []); + } + + /** + * Returns whether the attribute is translatable (has a translation) for the given locale. + * @param string $key + * @param string $locale + * @return bool + */ + public function hasTranslation($key, $locale) + { + /* + * If the default locale is passed, the attributes are retreived from the model, + * otherwise fetch the attributes from the $translatableAttributes property + */ + if ($locale == $this->translatableDefault) { + $translatableAttributes = $this->model->attributes; + } + else { + /* + * Ensure that the translatableData has been loaded + * @see https://github.com/rainlab/translate-plugin/issues/302 + */ + if (!isset($this->translatableAttributes[$locale])) { + $this->loadTranslatableData($locale); + } + + $translatableAttributes = $this->translatableAttributes[$locale]; + } + + return !!$this->getAttributeFromData($translatableAttributes, $key); + } + + /** + * Sets a translated attribute value. + * @param string $key Attribute + * @param string $value Value to translate + * @return string Translated value + */ + public function setAttributeTranslated($key, $value, $locale = null) + { + if ($locale == null) { + $locale = $this->translatableContext; + } + + if ($locale == $this->translatableDefault) { + return $this->setAttributeFromData($this->model->attributes, $key, $value); + } + + if (!array_key_exists($locale, $this->translatableAttributes)) { + $this->loadTranslatableData($locale); + } + + return $this->setAttributeFromData($this->translatableAttributes[$locale], $key, $value); + } + + /** + * Restores the default language values on the model and + * stores the translated values in the attributes table. + * @return void + */ + public function syncTranslatableAttributes() + { + /* + * Spin through the known locales, store the translations if necessary + */ + $knownLocales = array_keys($this->translatableAttributes); + foreach ($knownLocales as $locale) { + if (!$this->isTranslateDirty(null, $locale)) { + continue; + } + + $this->storeTranslatableData($locale); + } + + /* + * Saving the default locale, no need to restore anything + */ + if ($this->translatableContext == $this->translatableDefault) { + return; + } + + /* + * Restore translatable values to models originals + */ + $original = $this->model->getOriginal(); + $attributes = $this->model->getAttributes(); + $translatable = $this->model->getTranslatableAttributes(); + $originalValues = array_intersect_key($original, array_flip($translatable)); + $this->model->attributes = array_merge($attributes, $originalValues); + } + + /** + * Changes the active language for this model + * @param string $context + * @return void + */ + public function translateContext($context = null) + { + if ($context === null) { + return $this->translatableContext; + } + + $this->translatableContext = $context; + } + + /** + * Shorthand for translateContext method, and chainable. + * @param string $context + * @return self + */ + public function lang($context = null) + { + $this->translateContext($context); + + return $this->model; + } + + /** + * Checks if this model has transatable attributes. + * @return true + */ + public function hasTranslatableAttributes() + { + return is_array($this->model->translatable) && + count($this->model->translatable) > 0; + } + + /** + * Returns a collection of fields that will be hashed. + * @return array + */ + public function getTranslatableAttributes() + { + $translatable = []; + + if (!is_array($this->model->translatable)) { + return []; + } + + foreach ($this->model->translatable as $attribute) { + $translatable[] = is_array($attribute) ? array_shift($attribute) : $attribute; + } + + return $translatable; + } + + /** + * Returns the defined options for a translatable attribute. + * @return array + */ + public function getTranslatableAttributesWithOptions() + { + $attributes = []; + + foreach ($this->model->translatable as $options) { + if (!is_array($options)) { + continue; + } + + $attributeName = array_shift($options); + + $attributes[$attributeName] = $options; + } + + return $attributes; + } + + /** + * Determine if the model or a given translated attribute has been modified. + * @param string|null $attribute + * @return bool + */ + public function isTranslateDirty($attribute = null, $locale = null) + { + $dirty = $this->getTranslateDirty($locale); + + if (is_null($attribute)) { + return count($dirty) > 0; + } + else { + return array_key_exists($attribute, $dirty); + } + } + + /** + * Get the locales that have changed, if any + * + * @return array + */ + public function getDirtyLocales() + { + $dirtyLocales = []; + $knownLocales = array_keys($this->translatableAttributes); + foreach ($knownLocales as $locale) { + if ($this->isTranslateDirty(null, $locale)) { + $dirtyLocales[] = $locale; + } + } + + return $dirtyLocales; + } + + /** + * Get the original values of the translated attributes. + * @param string|null $locale If `null`, the method will get the original data for all locales. + * @return array|null Returns locale data as an array, or `null` if an invalid locale is specified. + */ + public function getTranslatableOriginals($locale = null) + { + if (!$locale) { + return $this->translatableOriginals; + } else { + return $this->translatableOriginals[$locale] ?? null; + } + } + + /** + * Get the translated attributes that have been changed since last sync. + * @return array + */ + public function getTranslateDirty($locale = null) + { + if (!$locale) { + $locale = $this->translatableContext; + } + + if (!array_key_exists($locale, $this->translatableAttributes)) { + return []; + } + + if (!array_key_exists($locale, $this->translatableOriginals)) { + return $this->translatableAttributes[$locale]; // All dirty + } + + $dirty = []; + + foreach ($this->translatableAttributes[$locale] as $key => $value) { + + if (!array_key_exists($key, $this->translatableOriginals[$locale])) { + $dirty[$key] = $value; + } + elseif ($value != $this->translatableOriginals[$locale][$key]) { + $dirty[$key] = $value; + } + } + + return $dirty; + } + + /** + * Extracts a attribute from a model/array with nesting support. + * @param mixed $data + * @param string $attribute + * @return mixed + */ + protected function getAttributeFromData($data, $attribute) + { + $keyArray = HtmlHelper::nameToArray($attribute); + + return array_get($data, implode('.', $keyArray)); + } + + /** + * Sets an attribute from a model/array with nesting support. + * @param mixed $data + * @param string $attribute + * @return mixed + */ + protected function setAttributeFromData(&$data, $attribute, $value) + { + $keyArray = HtmlHelper::nameToArray($attribute); + + array_set($data, implode('.', $keyArray), $value); + + return $value; + } + + /** + * Saves the translation data for the model. + * @param string $locale + * @return void + */ + abstract protected function storeTranslatableData($locale = null); + + /** + * Loads the translation data from the model. + * @param string $locale + * @return array + */ + abstract protected function loadTranslatableData($locale = null); +} diff --git a/plugins/rainlab/translate/classes/Translator.php b/plugins/rainlab/translate/classes/Translator.php new file mode 100644 index 00000000..de9dd705 --- /dev/null +++ b/plugins/rainlab/translate/classes/Translator.php @@ -0,0 +1,259 @@ +defaultLocale = $this->isConfigured() ? array_get(Locale::getDefault(), 'code', 'en') : 'en'; + $this->activeLocale = $this->defaultLocale; + } + + /** + * Changes the locale in the application and optionally stores it in the session. + * @param string $locale Locale to use + * @param boolean $remember Set to false to not store in the session. + * @return boolean Returns true if the locale exists and is set. + */ + public function setLocale($locale, $remember = true) + { + if (!Locale::isValid($locale)) { + return false; + } + + App::setLocale($locale); + + $this->activeLocale = $locale; + + if ($remember) { + $this->setSessionLocale($locale); + } + + return true; + } + + /** + * Returns the active locale set by this instance. + * @param boolean $fromSession Look in the session. + * @return string + */ + public function getLocale($fromSession = false) + { + if ($fromSession && ($locale = $this->getSessionLocale())) { + return $locale; + } + + return $this->activeLocale; + } + + /** + * Returns the default locale as set by the application. + * @return string + */ + public function getDefaultLocale() + { + return $this->defaultLocale; + } + + /** + * Check if this plugin is installed and the database is available, + * stores the result in the session for efficiency. + * @return boolean + */ + public function isConfigured() + { + if ($this->isConfigured !== null) { + return $this->isConfigured; + } + + if (Session::has(self::SESSION_CONFIGURED)) { + $result = true; + } + elseif (App::hasDatabase() && Schema::hasTable('rainlab_translate_locales')) { + Session::put(self::SESSION_CONFIGURED, true); + $result = true; + } + else { + $result = false; + } + + return $this->isConfigured = $result; + } + + // + // Request handling + // + + /** + * handleLocaleRoute will check if the route contains a translated locale prefix (/en/) + * and return that locale to be registered with the router. + * @return string + */ + public function handleLocaleRoute() + { + if (Config::get('rainlab.translate::disableLocalePrefixRoutes', false)) { + return ''; + } + + if (App::runningInBackend()) { + return ''; + } + + if (!$this->isConfigured()) { + return ''; + } + + if (!$this->loadLocaleFromRequest()) { + return ''; + } + + $locale = $this->getLocale(); + if (!$locale) { + return ''; + } + + return $locale; + } + + /** + * Sets the locale based on the first URI segment. + * @return bool + */ + public function loadLocaleFromRequest() + { + $locale = Request::segment(1); + + if (!Locale::isValid($locale)) { + return false; + } + + $this->setLocale($locale); + return true; + } + + /** + * Returns the current path prefixed with language code. + * + * @param string $locale optional language code, default to the system default language + * @return string + */ + public function getCurrentPathInLocale($locale = null) + { + return $this->getPathInLocale(Request::path(), $locale); + } + + /** + * Returns the path prefixed with language code. + * + * @param string $path Path to rewrite, already translate, with or without locale prefixed + * @param string $locale optional language code, default to the system default language + * @param boolean $prefixDefaultLocale should we prefix the path when the locale = default locale + * @return string + */ + public function getPathInLocale($path, $locale = null, $prefixDefaultLocale = null) + { + $prefixDefaultLocale = (is_null($prefixDefaultLocale)) + ? Config::get('rainlab.translate::prefixDefaultLocale') + : $prefixDefaultLocale; + + $segments = explode('/', $path); + + $segments = array_values(array_filter($segments, function ($v) { + return $v != ''; + })); + + if (is_null($locale) || !Locale::isValid($locale)) { + $locale = $this->defaultLocale; + } + + if (count($segments) == 0 || Locale::isValid($segments[0])) { + $segments[0] = $locale; + } else { + array_unshift($segments, $locale); + } + + // If we don't want te default locale to be prefixed + // and the first segment equals the default locale + if ( + !$prefixDefaultLocale && + isset($segments[0]) && + $segments[0] == $this->defaultLocale + ) { + // Remove the default locale + array_shift($segments); + }; + + return htmlspecialchars(implode('/', $segments), ENT_QUOTES, 'UTF-8'); + } + + // + // Session handling + // + + /** + * Looks at the session storage to find a locale. + * @return bool + */ + public function loadLocaleFromSession() + { + $locale = $this->getSessionLocale(); + + if (!$locale) { + return false; + } + + $this->setLocale($locale); + return true; + } + + protected function getSessionLocale() + { + if (!Session::has(self::SESSION_LOCALE)) { + return null; + } + + return Session::get(self::SESSION_LOCALE); + } + + protected function setSessionLocale($locale) + { + Session::put(self::SESSION_LOCALE, $locale); + } +} diff --git a/plugins/rainlab/translate/components/AlternateHrefLangElements.php b/plugins/rainlab/translate/components/AlternateHrefLangElements.php new file mode 100644 index 00000000..687fd49d --- /dev/null +++ b/plugins/rainlab/translate/components/AlternateHrefLangElements.php @@ -0,0 +1,82 @@ + 'rainlab.translate::lang.alternate_hreflang.component_name', + 'description' => 'rainlab.translate::lang.alternate_hreflang.component_description' + ]; + } + + /** + * locales + */ + public function locales() + { + // Available locales + $locales = collect(LocaleModel::listEnabled()); + + // Transform it to contain the new urls + $locales->transform(function ($item, $key) { + return $this->retrieveLocalizedUrl($key); + }); + + return $locales->toArray(); + } + + /** + * retrieveLocalizedUrl + */ + protected function retrieveLocalizedUrl($locale) + { + $translator = Translator::instance(); + $page = $this->getPage(); + + /* + * Static Page + */ + if (isset($page->apiBag['staticPage'])) { + $staticPage = $page->apiBag['staticPage']; + $staticPage->rewriteTranslatablePageUrl($locale); + $localeUrl = array_get($staticPage->attributes, 'viewBag.url'); + } + /* + * CMS Page + */ + else { + $page->rewriteTranslatablePageUrl($locale); + $params = $this->getRouter()->getParameters(); + + $translatedParams = Event::fire('translate.localePicker.translateParams', [ + $page, + $params, + $this->oldLocale, + $locale + ], true); + + if ($translatedParams) { + $params = $translatedParams; + } + + $router = new RainRouter; + $localeUrl = $router->urlFromPattern($page->url, $params); + } + + return $translator->getPathInLocale($localeUrl, $locale); + } + +} diff --git a/plugins/rainlab/translate/components/LocalePicker.php b/plugins/rainlab/translate/components/LocalePicker.php new file mode 100644 index 00000000..fa5110d9 --- /dev/null +++ b/plugins/rainlab/translate/components/LocalePicker.php @@ -0,0 +1,226 @@ + 'rainlab.translate::lang.locale_picker.component_name', + 'description' => 'rainlab.translate::lang.locale_picker.component_description', + ]; + } + + public function defineProperties() + { + return [ + 'forceUrl' => [ + 'title' => 'Force URL schema', + 'description' => 'Always prefix the URL with a language code.', + 'default' => 0, + 'type' => 'checkbox' + ], + ]; + } + + public function init() + { + $this->translator = Translator::instance(); + } + + public function onRun() + { + if ($redirect = $this->redirectForceUrl()) { + return $redirect; + } + + $this->page['locales'] = $this->locales = LocaleModel::listEnabled(); + $this->page['activeLocale'] = $this->activeLocale = $this->translator->getLocale(); + $this->page['activeLocaleName'] = $this->activeLocaleName = array_get($this->locales, $this->activeLocale); + } + + public function onSwitchLocale() + { + if (!$locale = post('locale')) { + return; + } + + // Remember the current locale before switching to the requested one + $this->oldLocale = $this->translator->getLocale(); + + $this->translator->setLocale($locale); + + $pageUrl = $this->withPreservedQueryString($this->makeLocaleUrlFromPage($locale), $locale); + if ($this->property('forceUrl')) { + return Redirect::to($this->translator->getPathInLocale($pageUrl, $locale)); + } + + return Redirect::to($pageUrl); + } + + protected function redirectForceUrl() + { + if ( + Request::ajax() || + !$this->property('forceUrl') || + $this->translator->loadLocaleFromRequest() + ) { + return; + } + + $prefixDefaultLocale = Config::get('rainlab.translate::prefixDefaultLocale'); + $locale = $this->translator->getLocale(false) + ?: $this->translator->getDefaultLocale(); + + if ($prefixDefaultLocale) { + return Redirect::to( + $this->withPreservedQueryString( + $this->translator->getCurrentPathInLocale($locale), + $locale + ) + ); + } elseif ( $locale == $this->translator->getDefaultLocale()) { + return; + } else { + $this->translator->setLocale($this->translator->getDefaultLocale()); + return; + } + + } + + /** + * Returns the URL from a page object, including current parameter values. + * @return string + */ + protected function makeLocaleUrlFromPage($locale) + { + $page = $this->getPage(); + + /* + * Static Page + */ + if (isset($page->apiBag['staticPage'])) { + $staticPage = $page->apiBag['staticPage']; + + $staticPage->rewriteTranslatablePageUrl($locale); + + $localeUrl = array_get($staticPage->attributes, 'viewBag.url'); + } + /* + * CMS Page + */ + else { + $page->rewriteTranslatablePageUrl($locale); + + $router = new RainRouter; + + $params = $this->getRouter()->getParameters(); + + /** + * @event translate.localePicker.translateParams + * Enables manipulating the URL parameters + * + * You will have access to the page object, the old and new locale and the URL parameters. + * + * Example usage: + * + * Event::listen('translate.localePicker.translateParams', function($page, $params, $oldLocale, $newLocale) { + * if ($page->baseFileName == 'your-page-filename') { + * return YourModel::translateParams($params, $oldLocale, $newLocale); + * } + * }); + * + */ + $translatedParams = Event::fire('translate.localePicker.translateParams', [ + $page, + $params, + $this->oldLocale, + $locale + ], true); + + if ($translatedParams) { + $params = $translatedParams; + } + + $localeUrl = $router->urlFromPattern($page->url, $params); + } + + return $localeUrl; + } + + /** + * Makes sure to add any existing query string to the redirect url. + * + * @param $pageUrl + * @param $locale + * + * @return string + */ + protected function withPreservedQueryString($pageUrl, $locale) + { + $page = $this->getPage(); + $query = request()->query(); + + /** + * @event translate.localePicker.translateQuery + * Enables manipulating the URL query parameters + * + * You will have access to the page object, the old and new locale and the URL query parameters. + * + * Example usage: + * + * Event::listen('translate.localePicker.translateQuery', function($page, $params, $oldLocale, $newLocale) { + * if ($page->baseFileName == 'your-page-filename') { + * return YourModel::translateParams($params, $oldLocale, $newLocale); + * } + * }); + * + */ + $translatedQuery = Event::fire('translate.localePicker.translateQuery', [ + $page, + $query, + $this->oldLocale, + $locale + ], true); + + $query = http_build_query($translatedQuery ?: $query); + + return $query ? $pageUrl . '?' . $query : $pageUrl; + } +} diff --git a/plugins/rainlab/translate/components/alternatehreflangelements/default.htm b/plugins/rainlab/translate/components/alternatehreflangelements/default.htm new file mode 100644 index 00000000..e7b5fd89 --- /dev/null +++ b/plugins/rainlab/translate/components/alternatehreflangelements/default.htm @@ -0,0 +1,3 @@ +{% for locale, alternateUrl in __SELF__.locales %} + +{% endfor %} diff --git a/plugins/rainlab/translate/components/localepicker/default.htm b/plugins/rainlab/translate/components/localepicker/default.htm new file mode 100644 index 00000000..90c3444f --- /dev/null +++ b/plugins/rainlab/translate/components/localepicker/default.htm @@ -0,0 +1,7 @@ +{{ form_open() }} + +{{ form_close() }} \ No newline at end of file diff --git a/plugins/rainlab/translate/composer.json b/plugins/rainlab/translate/composer.json new file mode 100644 index 00000000..af341785 --- /dev/null +++ b/plugins/rainlab/translate/composer.json @@ -0,0 +1,25 @@ +{ + "name": "rainlab/translate-plugin", + "type": "october-plugin", + "description": "Translate plugin for October CMS", + "homepage": "https://octobercms.com/plugin/rainlab-translate", + "keywords": ["october", "octobercms", "translate"], + "license": "MIT", + "authors": [ + { + "name": "Alexey Bobkov", + "email": "aleksey.bobkov@gmail.com", + "role": "Co-founder" + }, + { + "name": "Samuel Georges", + "email": "daftspunky@gmail.com", + "role": "Co-founder" + } + ], + "require": { + "php": ">=5.5.9", + "composer/installers": "~1.0" + }, + "minimum-stability": "dev" +} diff --git a/plugins/rainlab/translate/config/config.php b/plugins/rainlab/translate/config/config.php new file mode 100644 index 00000000..d9c60fc3 --- /dev/null +++ b/plugins/rainlab/translate/config/config.php @@ -0,0 +1,49 @@ + env('TRANSLATE_FORCE_LOCALE', null), + + /* + |-------------------------------------------------------------------------- + | Prefix the Default Locale + |-------------------------------------------------------------------------- + | + | Specifies if the default locale be prefixed by the plugin. + | + */ + 'prefixDefaultLocale' => env('TRANSLATE_PREFIX_LOCALE', true), + + /* + |-------------------------------------------------------------------------- + | Cache Timeout in Minutes + |-------------------------------------------------------------------------- + | + | By default all translations are cached for 24 hours (1440 min). + | This setting allows to change that period with given amount of minutes. + | + | For example, 43200 for 30 days or 525600 for one year. + | + */ + 'cacheTimeout' => env('TRANSLATE_CACHE_TIMEOUT', 1440), + + /* + |-------------------------------------------------------------------------- + | Disable Locale Prefix Routes + |-------------------------------------------------------------------------- + | + | Disables the automatically generated locale prefixed routes + | (i.e. /en/original-route) when enabled. + | + */ + 'disableLocalePrefixRoutes' => env('TRANSLATE_DISABLE_PREFIX_ROUTES', false), + +]; diff --git a/plugins/rainlab/translate/console/ScanCommand.php b/plugins/rainlab/translate/console/ScanCommand.php new file mode 100644 index 00000000..4d38604f --- /dev/null +++ b/plugins/rainlab/translate/console/ScanCommand.php @@ -0,0 +1,40 @@ +option('purge')) { + $this->output->writeln('Purging messages...'); + Message::truncate(); + } + + ThemeScanner::scan(); + $this->output->success('Messages scanned successfully.'); + $this->output->note('You may need to run cache:clear for updated messages to take effect.'); + } + + protected function getArguments() + { + return []; + } + + protected function getOptions() + { + return [ + ['purge', 'null', InputOption::VALUE_NONE, 'First purge existing messages.', null], + ]; + } +} diff --git a/plugins/rainlab/translate/controllers/Locales.php b/plugins/rainlab/translate/controllers/Locales.php new file mode 100644 index 00000000..7f3be5d6 --- /dev/null +++ b/plugins/rainlab/translate/controllers/Locales.php @@ -0,0 +1,92 @@ +addJs('/plugins/rainlab/translate/assets/js/locales.js'); + } + + /** + * {@inheritDoc} + */ + public function listInjectRowClass($record, $definition = null) + { + if (!$record->is_enabled) { + return 'safe disabled'; + } + } + + public function onCreateForm() + { + $this->asExtension('FormController')->create(); + + return $this->makePartial('create_form'); + } + + public function onCreate() + { + LocaleModel::clearCache(); + $this->asExtension('FormController')->create_onSave(); + + return $this->listRefresh(); + } + + public function onUpdateForm() + { + $this->asExtension('FormController')->update(post('record_id')); + $this->vars['recordId'] = post('record_id'); + + return $this->makePartial('update_form'); + } + + public function onUpdate() + { + LocaleModel::clearCache(); + $this->asExtension('FormController')->update_onSave(post('record_id')); + + return $this->listRefresh(); + } + + public function onDelete() + { + LocaleModel::clearCache(); + $this->asExtension('FormController')->update_onDelete(post('record_id')); + + return $this->listRefresh(); + } + + public function onReorder() + { + LocaleModel::clearCache(); + $this->asExtension('ReorderController')->onReorder(); + } +} diff --git a/plugins/rainlab/translate/controllers/Messages.php b/plugins/rainlab/translate/controllers/Messages.php new file mode 100644 index 00000000..4f842863 --- /dev/null +++ b/plugins/rainlab/translate/controllers/Messages.php @@ -0,0 +1,237 @@ +addJs('/plugins/rainlab/translate/assets/js/messages.js'); + $this->addCss('/plugins/rainlab/translate/assets/css/messages.css'); + + $this->importColumns = MessageExport::getColumns(); + $this->exportColumns = MessageExport::getColumns(); + } + + public function index() + { + $this->pageTitle = 'rainlab.translate::lang.messages.title'; + $this->prepareTable(); + } + + public function onRefresh() + { + $this->prepareTable(); + return ['#messagesContainer' => $this->makePartial('messages')]; + } + + public function onClearCache() + { + CacheHelper::clear(); + + Flash::success(Lang::get('rainlab.translate::lang.messages.clear_cache_success')); + } + + public function onLoadScanMessagesForm() + { + return $this->makePartial('scan_messages_form'); + } + + public function onScanMessages() + { + if (post('purge_messages', false)) { + Message::truncate(); + } + + ThemeScanner::scan(); + + if (post('purge_deleted_messages', false)) { + Message::where('found', 0)->delete(); + } + + Flash::success(Lang::get('rainlab.translate::lang.messages.scan_messages_success')); + + return $this->onRefresh(); + } + + public function prepareTable() + { + $fromCode = post('locale_from', null); + $toCode = post('locale_to', Locale::getDefault()->code); + $this->hideTranslated = post('hide_translated', false); + + /* + * Page vars + */ + $this->vars['hideTranslated'] = $this->hideTranslated; + $this->vars['defaultLocale'] = Locale::getDefault(); + $this->vars['locales'] = Locale::all(); + $this->vars['selectedFrom'] = $selectedFrom = Locale::findByCode($fromCode); + $this->vars['selectedTo'] = $selectedTo = Locale::findByCode($toCode); + + /* + * Make table config, make default column read only + */ + $config = $this->makeConfig('config_table.yaml'); + + if (!$selectedFrom) { + $config->columns['from']['readOnly'] = true; + } + if (!$selectedTo) { + $config->columns['to']['readOnly'] = true; + } + + /* + * Make table widget + */ + $widget = $this->makeWidget(\Backend\Widgets\Table::class, $config); + $widget->bindToController(); + + /* + * Populate data + */ + $dataSource = $widget->getDataSource(); + + $dataSource->bindEvent('data.getRecords', function($offset, $count) use ($selectedFrom, $selectedTo) { + $messages = $this->listMessagesForDatasource([ + 'offset' => $offset, + 'count' => $count + ]); + + return $this->processTableData($messages, $selectedFrom, $selectedTo); + }); + + $dataSource->bindEvent('data.searchRecords', function($search, $offset, $count) use ($selectedFrom, $selectedTo) { + $messages = $this->listMessagesForDatasource([ + 'search' => $search, + 'offset' => $offset, + 'count' => $count + ]); + + return $this->processTableData($messages, $selectedFrom, $selectedTo); + }); + + $dataSource->bindEvent('data.getCount', function() { + return Message::count(); + }); + + $dataSource->bindEvent('data.updateRecord', function($key, $data) { + $message = Message::find($key); + $this->updateTableData($message, $data); + CacheHelper::clear(); + }); + + $dataSource->bindEvent('data.deleteRecord', function($key) { + if ($message = Message::find($key)) { + $message->delete(); + } + }); + + $this->vars['table'] = $widget; + } + + protected function isHideTranslated() + { + return post('hide_translated', false); + } + + protected function listMessagesForDatasource($options = []) + { + extract(array_merge([ + 'search' => null, + 'offset' => null, + 'count' => null, + ], $options)); + + $query = Message::orderBy('message_data','asc'); + + if ($search) { + $query = $query->searchWhere($search, ['message_data']); + } + + if ($count) { + $query = $query->limit($count)->offset($offset); + } + + return $query->get(); + } + + protected function processTableData($messages, $from, $to) + { + $fromCode = $from ? $from->code : null; + $toCode = $to ? $to->code : null; + + $data = []; + foreach ($messages as $message) { + $toContent = $message->forLocale($toCode); + if ($this->hideTranslated && $toContent) { + continue; + } + + $data[] = [ + 'id' => $message->id, + 'code' => $message->code, + 'from' => $message->forLocale($fromCode), + 'to' => $toContent, + 'found' => $message->found ? '' : Lang::get('rainlab.translate::lang.messages.not_found'), + ]; + } + + return $data; + } + + protected function updateTableData($message, $data) + { + if (!$message) { + return; + } + + $fromCode = post('locale_from'); + $toCode = post('locale_to'); + + // @todo This should be unified to a single save() + if ($fromCode) { + $fromValue = array_get($data, 'from'); + if ($fromValue != $message->forLocale($fromCode)) { + $message->toLocale($fromCode, $fromValue); + } + } + + if ($toCode) { + $toValue = array_get($data, 'to'); + if ($toValue != $message->forLocale($toCode)) { + $message->toLocale($toCode, $toValue); + } + } + } +} diff --git a/plugins/rainlab/translate/controllers/locales/_create_form.htm b/plugins/rainlab/translate/controllers/locales/_create_form.htm new file mode 100644 index 00000000..e33ba274 --- /dev/null +++ b/plugins/rainlab/translate/controllers/locales/_create_form.htm @@ -0,0 +1,55 @@ + 'createForm']) ?> + + + + fatalError): ?> + + + + + + + + + + + + + + diff --git a/plugins/rainlab/translate/controllers/locales/_hint.htm b/plugins/rainlab/translate/controllers/locales/_hint.htm new file mode 100644 index 00000000..45d85a49 --- /dev/null +++ b/plugins/rainlab/translate/controllers/locales/_hint.htm @@ -0,0 +1,4 @@ + +

    + +

    \ No newline at end of file diff --git a/plugins/rainlab/translate/controllers/locales/_list_toolbar.htm b/plugins/rainlab/translate/controllers/locales/_list_toolbar.htm new file mode 100644 index 00000000..126253ba --- /dev/null +++ b/plugins/rainlab/translate/controllers/locales/_list_toolbar.htm @@ -0,0 +1,15 @@ +
    diff --git a/plugins/rainlab/translate/controllers/locales/_reorder_toolbar.htm b/plugins/rainlab/translate/controllers/locales/_reorder_toolbar.htm new file mode 100644 index 00000000..30a2fd6e --- /dev/null +++ b/plugins/rainlab/translate/controllers/locales/_reorder_toolbar.htm @@ -0,0 +1,7 @@ + \ No newline at end of file diff --git a/plugins/rainlab/translate/controllers/locales/_update_form.htm b/plugins/rainlab/translate/controllers/locales/_update_form.htm new file mode 100644 index 00000000..c820921d --- /dev/null +++ b/plugins/rainlab/translate/controllers/locales/_update_form.htm @@ -0,0 +1,67 @@ + 'updateForm']) ?> + + + + + + fatalError): ?> + + + + + + + + + + + + + + diff --git a/plugins/rainlab/translate/controllers/locales/config_form.yaml b/plugins/rainlab/translate/controllers/locales/config_form.yaml new file mode 100644 index 00000000..781bcfbe --- /dev/null +++ b/plugins/rainlab/translate/controllers/locales/config_form.yaml @@ -0,0 +1,7 @@ +# =================================== +# Form Behavior Config +# =================================== + +form: ~/plugins/rainlab/translate/models/locale/fields.yaml +modelClass: RainLab\Translate\Models\Locale +defaultRedirect: rainlab/translate/locales diff --git a/plugins/rainlab/translate/controllers/locales/config_list.yaml b/plugins/rainlab/translate/controllers/locales/config_list.yaml new file mode 100644 index 00000000..6447ec88 --- /dev/null +++ b/plugins/rainlab/translate/controllers/locales/config_list.yaml @@ -0,0 +1,52 @@ +# =================================== +# List Behavior Config +# =================================== + +# Model List Column configuration +list: ~/plugins/rainlab/translate/models/locale/columns.yaml + +# Model Class name +modelClass: RainLab\Translate\Models\Locale + +# List Title +title: rainlab.translate::lang.locale.title + +# Link URL for each record +# recordUrl: rainlab/translate/locale/update/:id + +recordOnClick: $.translateLocales.clickRecord(:id) + +# Message to display if the list is empty +noRecordsMessage: backend::lang.list.no_records + +# Records to display per page +recordsPerPage: 20 + +# Displays the list column set up button +showSetup: true + +# Displays the sorting link on each column +showSorting: true + +# Default sorting column +defaultSort: + column: sort_order + direction: asc + +# Display checkboxes next to each record +# showCheckboxes: true + +# Toolbar widget configuration +toolbar: + # Partial for toolbar buttons + buttons: list_toolbar + + # Search widget configuration + search: + prompt: backend::lang.list.search_prompt + +# Reordering +structure: + showTree: false + showReorder: true + maxDepth: 1 diff --git a/plugins/rainlab/translate/controllers/locales/config_reorder.yaml b/plugins/rainlab/translate/controllers/locales/config_reorder.yaml new file mode 100644 index 00000000..797a21f5 --- /dev/null +++ b/plugins/rainlab/translate/controllers/locales/config_reorder.yaml @@ -0,0 +1,17 @@ +# =================================== +# Reorder Behavior Config +# =================================== + +# Reorder Title +title: rainlab.translate::lang.locale.reorder_title + +# Attribute name +nameFrom: name + +# Model Class name +modelClass: RainLab\Translate\Models\Locale + +# Toolbar widget configuration +toolbar: + # Partial for toolbar buttons + buttons: reorder_toolbar \ No newline at end of file diff --git a/plugins/rainlab/translate/controllers/locales/index.htm b/plugins/rainlab/translate/controllers/locales/index.htm new file mode 100644 index 00000000..5bdfd58e --- /dev/null +++ b/plugins/rainlab/translate/controllers/locales/index.htm @@ -0,0 +1,12 @@ + +
      +
    • +
    • pageTitle)) ?>
    • +
    + + +
    + makeHintPartial('translation_locales_hint', 'hint') ?> +
    + +listRender() ?> diff --git a/plugins/rainlab/translate/controllers/locales/reorder.htm b/plugins/rainlab/translate/controllers/locales/reorder.htm new file mode 100644 index 00000000..f820c6ae --- /dev/null +++ b/plugins/rainlab/translate/controllers/locales/reorder.htm @@ -0,0 +1,9 @@ + +
      +
    • +
    • +
    • pageTitle)) ?>
    • +
    + + +reorderRender() ?> diff --git a/plugins/rainlab/translate/controllers/messages/_hint.htm b/plugins/rainlab/translate/controllers/messages/_hint.htm new file mode 100644 index 00000000..18a041b5 --- /dev/null +++ b/plugins/rainlab/translate/controllers/messages/_hint.htm @@ -0,0 +1,6 @@ + +

    + + + +

    \ No newline at end of file diff --git a/plugins/rainlab/translate/controllers/messages/_messages.htm b/plugins/rainlab/translate/controllers/messages/_messages.htm new file mode 100644 index 00000000..87ed504c --- /dev/null +++ b/plugins/rainlab/translate/controllers/messages/_messages.htm @@ -0,0 +1,10 @@ +
    +
    + makePartial('table_headers') ?> +
    +
    + makePartial('table_toolbar') ?> +
    + + render() ?> +
    diff --git a/plugins/rainlab/translate/controllers/messages/_scan_messages_form.htm b/plugins/rainlab/translate/controllers/messages/_scan_messages_form.htm new file mode 100644 index 00000000..101f82a3 --- /dev/null +++ b/plugins/rainlab/translate/controllers/messages/_scan_messages_form.htm @@ -0,0 +1,80 @@ +
    + 'scanMessagesForm']) ?> + + + + + + +
    + + diff --git a/plugins/rainlab/translate/controllers/messages/_table_headers.htm b/plugins/rainlab/translate/controllers/messages/_table_headers.htm new file mode 100644 index 00000000..8a5c03c9 --- /dev/null +++ b/plugins/rainlab/translate/controllers/messages/_table_headers.htm @@ -0,0 +1,105 @@ + + + + + + + + + + + + + + diff --git a/plugins/rainlab/translate/controllers/messages/_table_toolbar.htm b/plugins/rainlab/translate/controllers/messages/_table_toolbar.htm new file mode 100644 index 00000000..eb24d787 --- /dev/null +++ b/plugins/rainlab/translate/controllers/messages/_table_toolbar.htm @@ -0,0 +1,24 @@ + diff --git a/plugins/rainlab/translate/controllers/messages/config_import_export.yaml b/plugins/rainlab/translate/controllers/messages/config_import_export.yaml new file mode 100644 index 00000000..3aaf39f1 --- /dev/null +++ b/plugins/rainlab/translate/controllers/messages/config_import_export.yaml @@ -0,0 +1,9 @@ +import: + title: 'rainlab.translate::lang.messages.import_messages_link' + modelClass: RainLab\Translate\Models\MessageImport + redirect: rainlab/translate/messages + +export: + title: 'rainlab.translate::lang.messages.export_messages_link' + modelClass: RainLab\Translate\Models\MessageExport + redirect: rainlab/translate/messages diff --git a/plugins/rainlab/translate/controllers/messages/config_table.yaml b/plugins/rainlab/translate/controllers/messages/config_table.yaml new file mode 100644 index 00000000..331f33ee --- /dev/null +++ b/plugins/rainlab/translate/controllers/messages/config_table.yaml @@ -0,0 +1,17 @@ +# =================================== +# Grid Widget Configuration +# =================================== + +dataSource: server +keyFrom: id +recordsPerPage: 500 +adding: false +searching: true + +columns: + from: + title: From + to: + title: To + found: + title: Scan errors diff --git a/plugins/rainlab/translate/controllers/messages/export.htm b/plugins/rainlab/translate/controllers/messages/export.htm new file mode 100644 index 00000000..589b1919 --- /dev/null +++ b/plugins/rainlab/translate/controllers/messages/export.htm @@ -0,0 +1,26 @@ + +
      +
    • +
    • +
    • pageTitle)) ?>
    • +
    + + + 'layout']) ?> + +
    + exportRender() ?> +
    + +
    + +
    + + diff --git a/plugins/rainlab/translate/controllers/messages/import.htm b/plugins/rainlab/translate/controllers/messages/import.htm new file mode 100644 index 00000000..95382159 --- /dev/null +++ b/plugins/rainlab/translate/controllers/messages/import.htm @@ -0,0 +1,26 @@ + +
      +
    • +
    • +
    • pageTitle)) ?>
    • +
    + + + 'layout']) ?> + +
    + importRender() ?> +
    + +
    + +
    + + diff --git a/plugins/rainlab/translate/controllers/messages/index.htm b/plugins/rainlab/translate/controllers/messages/index.htm new file mode 100644 index 00000000..3f3777af --- /dev/null +++ b/plugins/rainlab/translate/controllers/messages/index.htm @@ -0,0 +1,34 @@ + +
      +
    • +
    • pageTitle)) ?>
    • +
    + + +
    + makeHintPartial('translation_messages_hint', 'hint') ?> +
    + + 'messagesForm', 'class'=>'layout-item stretch layout-column', 'onsubmit'=>'return false']) ?> + +
    + makePartial('messages') ?> +
    + + + + + + + + + + diff --git a/plugins/rainlab/translate/formwidgets/MLMarkdownEditor.php b/plugins/rainlab/translate/formwidgets/MLMarkdownEditor.php new file mode 100644 index 00000000..ec746730 --- /dev/null +++ b/plugins/rainlab/translate/formwidgets/MLMarkdownEditor.php @@ -0,0 +1,107 @@ +initLocale(); + } + + /** + * {@inheritDoc} + */ + public function render() + { + $this->actAsParent(); + $parentContent = parent::render(); + $this->actAsParent(false); + + if (!$this->isAvailable) { + return $parentContent; + } + + $this->vars['markdowneditor'] = $parentContent; + return $this->makePartial('mlmarkdowneditor'); + } + + public function prepareVars() + { + parent::prepareVars(); + $this->prepareLocaleVars(); + } + + /** + * getSaveValue returns an array of translated values for this field + * @return array + */ + public function getSaveValue($value) + { + return $this->getLocaleSaveValue($value); + } + + /** + * {@inheritDoc} + */ + protected function loadAssets() + { + $this->actAsParent(); + parent::loadAssets(); + $this->actAsParent(false); + + if (Locale::isAvailable()) { + $this->loadLocaleAssets(); + $this->addJs('js/mlmarkdowneditor.js'); + } + } + + /** + * {@inheritDoc} + */ + protected function getParentViewPath() + { + return base_path().'/modules/backend/formwidgets/markdowneditor/partials'; + } + + /** + * {@inheritDoc} + */ + protected function getParentAssetPath() + { + return '/modules/backend/formwidgets/markdowneditor/assets'; + } +} diff --git a/plugins/rainlab/translate/formwidgets/MLMediaFinder.php b/plugins/rainlab/translate/formwidgets/MLMediaFinder.php new file mode 100644 index 00000000..5e21269f --- /dev/null +++ b/plugins/rainlab/translate/formwidgets/MLMediaFinder.php @@ -0,0 +1,104 @@ +initLocale(); + } + + /** + * @inheritDoc + */ + public function render() + { + $this->actAsParent(); + $parentContent = parent::render(); + $this->actAsParent(false); + + if (!$this->isAvailable) { + return $parentContent; + } + + $this->vars['mediafinder'] = $parentContent; + return $this->makePartial('mlmediafinder'); + } + + /** + * Prepares the form widget view data + */ + public function prepareVars() + { + parent::prepareVars(); + $this->prepareLocaleVars(); + // make root path of media files accessible + $this->vars['mediaPath'] = $this->mediaPath = MediaLibrary::url('/'); + } + + /** + * @inheritDoc + */ + public function getSaveValue($value) + { + return $this->getLocaleSaveValue($value); + } + + /** + * @inheritDoc + */ + public function loadAssets() + { + $this->actAsParent(); + parent::loadAssets(); + $this->actAsParent(false); + + if (Locale::isAvailable()) { + $this->loadLocaleAssets(); + $this->addJs('js/mlmediafinder.js'); + $this->addCss('css/mlmediafinder.css'); + } + } + + /** + * {@inheritDoc} + */ + protected function getParentViewPath() + { + return base_path().'/modules/backend/formwidgets/mediafinder/partials'; + } + + /** + * {@inheritDoc} + */ + protected function getParentAssetPath() + { + return '/modules/backend/formwidgets/mediafinder/assets'; + } +} diff --git a/plugins/rainlab/translate/formwidgets/MLMediaFinderv2.php b/plugins/rainlab/translate/formwidgets/MLMediaFinderv2.php new file mode 100644 index 00000000..250463b7 --- /dev/null +++ b/plugins/rainlab/translate/formwidgets/MLMediaFinderv2.php @@ -0,0 +1,108 @@ +initLocale(); + } + + /** + * @inheritDoc + */ + public function render() + { + $this->actAsParent(); + $parentContent = parent::render(); + $this->actAsParent(false); + + if (!$this->isAvailable) { + return $parentContent; + } + + $this->vars['mediafinder'] = $parentContent; + return $this->makePartial('mlmediafinder'); + } + + /** + * prepareVars prepares the form widget view data + */ + public function prepareVars() + { + parent::prepareVars(); + $this->prepareLocaleVars(); + // make root path of media files accessible + $this->vars['mediaPath'] = $this->mediaPath = MediaLibrary::url('/'); + } + + /** + * @inheritDoc + */ + public function getSaveValue($value) + { + if ($this->isAvailable) { + return $this->getLocaleSaveValue($value); + } + + return parent::getSaveValue($value); + } + + /** + * @inheritDoc + */ + public function loadAssets() + { + $this->actAsParent(); + parent::loadAssets(); + $this->actAsParent(false); + + if (Locale::isAvailable()) { + $this->loadLocaleAssets(); + $this->addJs('js/mlmediafinder.js'); + $this->addCss('../../mlmediafinder/assets/css/mlmediafinder.css'); + } + } + + /** + * {@inheritDoc} + */ + protected function getParentViewPath() + { + return base_path().'/modules/media/formwidgets/mediafinder/partials'; + } + + /** + * {@inheritDoc} + */ + protected function getParentAssetPath() + { + return '/modules/media/formwidgets/mediafinder/assets'; + } +} diff --git a/plugins/rainlab/translate/formwidgets/MLNestedForm.php b/plugins/rainlab/translate/formwidgets/MLNestedForm.php new file mode 100644 index 00000000..f04896b4 --- /dev/null +++ b/plugins/rainlab/translate/formwidgets/MLNestedForm.php @@ -0,0 +1,187 @@ +initLocale(); + } + + /** + * {@inheritDoc} + */ + public function render() + { + $this->actAsParent(); + $parentContent = parent::render(); + $this->actAsParent(false); + + if (!$this->isAvailable) { + return $parentContent; + } + + $this->vars['nestedform'] = $parentContent; + + return $this->makePartial('mlnestedform'); + } + + /** + * prepareVars for viewing + */ + public function prepareVars() + { + parent::prepareVars(); + + $this->prepareLocaleVars(); + } + + /** + * getSaveValue returns an array of translated values for this field + * @return array + */ + public function getSaveValue($value) + { + $this->rewritePostValues(); + + return $this->getLocaleSaveValue($value); + } + + /** + * {@inheritDoc} + */ + protected function loadAssets() + { + $this->actAsParent(); + parent::loadAssets(); + $this->actAsParent(false); + + if (Locale::isAvailable()) { + $this->loadLocaleAssets(); + $this->addJs('js/mlnestedform.js'); + } + } + + /** + * {@inheritDoc} + */ + protected function getParentViewPath() + { + return base_path().'/modules/backend/formwidgets/nestedform/partials'; + } + + /** + * {@inheritDoc} + */ + protected function getParentAssetPath() + { + return '/modules/backend/formwidgets/nestedform/assets'; + } + + /** + * onSwitchItemLocale handler + */ + public function onSwitchItemLocale() + { + if (!$locale = post('_nestedform_locale')) { + throw new ApplicationException('Unable to find a nested form locale for: '.$locale); + } + + // Store previous value + $previousLocale = post('_nestedform_previous_locale'); + $previousValue = $this->getPrimarySaveDataAsArray(); + + // Update widget to show form for switched locale + $lockerData = $this->getLocaleSaveDataAsArray($locale) ?: []; + $this->formWidget->setFormValues($lockerData); + + $this->actAsParent(); + $parentContent = parent::render(); + $this->actAsParent(false); + + return [ + '#'.$this->getId('mlNestedForm') => $parentContent, + 'updateValue' => json_encode($previousValue), + 'updateLocale' => $previousLocale, + ]; + } + + /** + * getPrimarySaveDataAsArray gets the active values from the selected locale. + */ + protected function getPrimarySaveDataAsArray(): array + { + return post($this->formField->getName()) ?: []; + } + + /** + * getLocaleSaveDataAsArray returns the stored locale data as an array. + */ + protected function getLocaleSaveDataAsArray($locale): ?array + { + $saveData = array_get($this->getLocaleSaveData(), $locale, []); + + if (!is_array($saveData)) { + $saveData = json_decode($saveData, true); + } + + return $saveData; + } + + /** + * rewritePostValues since the locker does always contain the latest values, + * this method will take the save data from the nested form and merge it in to + * the locker based on which ever locale is selected using an item map + */ + protected function rewritePostValues() + { + // Get the selected locale at postback + $data = post('RLTranslateNestedFormLocale'); + $fieldName = implode('.', HtmlHelper::nameToArray($this->fieldName)); + $locale = array_get($data, $fieldName); + + if (!$locale) { + return; + } + + // Splice the save data in to the locker data for selected locale + $data = $this->getPrimarySaveDataAsArray(); + $fieldName = 'RLTranslate.'.$locale.'.'.implode('.', HtmlHelper::nameToArray($this->fieldName)); + + $requestData = Request::all(); + array_set($requestData, $fieldName, json_encode($data)); + $this->mergeWithPost($requestData); + } + + /** + * mergeWithPost will apply postback values globally + */ + protected function mergeWithPost(array $values) + { + Request::merge($values); + $_POST = array_merge($_POST, $values); + } +} diff --git a/plugins/rainlab/translate/formwidgets/MLRepeater.php b/plugins/rainlab/translate/formwidgets/MLRepeater.php new file mode 100644 index 00000000..237d145b --- /dev/null +++ b/plugins/rainlab/translate/formwidgets/MLRepeater.php @@ -0,0 +1,239 @@ +initLocale(); + } + + /** + * {@inheritDoc} + */ + public function render() + { + $this->actAsParent(); + $parentContent = parent::render(); + $this->actAsParent(false); + + if (!$this->isAvailable) { + return $parentContent; + } + + $this->vars['repeater'] = $parentContent; + return $this->makePartial('mlrepeater'); + } + + /** + * prepareVars + */ + public function prepareVars() + { + parent::prepareVars(); + $this->prepareLocaleVars(); + } + + /** + * getSaveValue returns an array of translated values for this field + * @return array + */ + public function getSaveValue($value) + { + $this->rewritePostValues(); + + return $this->getLocaleSaveValue(is_array($value) ? array_values($value) : $value); + } + + /** + * {@inheritDoc} + */ + protected function loadAssets() + { + $this->actAsParent(); + parent::loadAssets(); + $this->actAsParent(false); + + if (Locale::isAvailable()) { + $this->loadLocaleAssets(); + $this->addJs('js/mlrepeater.js'); + } + } + + /** + * {@inheritDoc} + */ + protected function getParentViewPath() + { + return base_path().'/modules/backend/formwidgets/repeater/partials'; + } + + /** + * {@inheritDoc} + */ + protected function getParentAssetPath() + { + return '/modules/backend/formwidgets/repeater/assets'; + } + + /** + * onAddItem + */ + public function onAddItem() + { + $this->actAsParent(); + return parent::onAddItem(); + } + + /** + * onDuplicateItem + */ + public function onDuplicateItem() + { + $this->actAsParent(); + return parent::onDuplicateItem(); + } + + /** + * onSwitchItemLocale + */ + public function onSwitchItemLocale() + { + if (!$locale = post('_repeater_locale')) { + throw new ApplicationException('Unable to find a repeater locale for: '.$locale); + } + + // Store previous value + $previousLocale = post('_repeater_previous_locale'); + $previousValue = $this->getPrimarySaveDataAsArray(); + + // Update widget to show form for switched locale + $lockerData = $this->getLocaleSaveDataAsArray($locale) ?: []; + $this->reprocessLocaleItems($lockerData); + + foreach ($this->formWidgets as $key => $widget) { + $value = array_shift($lockerData); + if (!$value) { + unset($this->formWidgets[$key]); + } + else { + $widget->setFormValues($value); + } + } + + $this->actAsParent(); + $parentContent = parent::render(); + $this->actAsParent(false); + + return [ + '#'.$this->getId('mlRepeater') => $parentContent, + 'updateValue' => json_encode($previousValue), + 'updateLocale' => $previousLocale, + ]; + } + + /** + * reprocessLocaleItems ensures that the current locale data is processed by + * the repeater instead of the original non-translated data + * @return void + */ + protected function reprocessLocaleItems($data) + { + $this->formWidgets = []; + + $this->formField->value = $data; + + $key = implode('.', HtmlHelper::nameToArray($this->formField->getName())); + + $requestData = Request::all(); + + array_set($requestData, $key, $data); + + $this->mergeWithPost($requestData); + + $this->processItems(); + } + + /** + * getPrimarySaveDataAsArray gets the active values from the selected locale. + * @return array + */ + protected function getPrimarySaveDataAsArray() + { + $data = post($this->formField->getName()) ?: []; + + return $this->processSaveValue($data); + } + + /** + * getLocaleSaveDataAsArray returns the stored locale data as an array. + * @return array + */ + protected function getLocaleSaveDataAsArray($locale) + { + $saveData = array_get($this->getLocaleSaveData(), $locale, []); + + if (!is_array($saveData)) { + $saveData = json_decode($saveData, true); + } + + return $saveData; + } + + /** + * rewritePostValues since the locker does always contain the latest values, this method + * will take the save data from the repeater and merge it in to the + * locker based on which ever locale is selected using an item map + * @return void + */ + protected function rewritePostValues() + { + // Get the selected locale at postback + $data = post('RLTranslateRepeaterLocale'); + $fieldName = implode('.', HtmlHelper::nameToArray($this->fieldName)); + $locale = array_get($data, $fieldName); + + if (!$locale) { + return; + } + + // Splice the save data in to the locker data for selected locale + $data = $this->getPrimarySaveDataAsArray(); + $fieldName = 'RLTranslate.'.$locale.'.'.implode('.', HtmlHelper::nameToArray($this->fieldName)); + + $requestData = Request::all(); + array_set($requestData, $fieldName, json_encode($data)); + $this->mergeWithPost($requestData); + } + + /** + * mergeWithPost will apply postback values globally + */ + protected function mergeWithPost(array $values) + { + Request::merge($values); + $_POST = array_merge($_POST, $values); + } +} diff --git a/plugins/rainlab/translate/formwidgets/MLRichEditor.php b/plugins/rainlab/translate/formwidgets/MLRichEditor.php new file mode 100644 index 00000000..92f4bd28 --- /dev/null +++ b/plugins/rainlab/translate/formwidgets/MLRichEditor.php @@ -0,0 +1,119 @@ +initLocale(); + } + + /** + * {@inheritDoc} + */ + public function render() + { + $this->actAsParent(); + $parentContent = parent::render(); + $this->actAsParent(false); + + if (!$this->isAvailable) { + return $parentContent; + } + + $this->vars['richeditor'] = $parentContent; + return $this->makePartial('mlricheditor'); + } + + /** + * prepareVars + */ + public function prepareVars() + { + parent::prepareVars(); + $this->prepareLocaleVars(); + } + + /** + * getSaveValue returns an array of translated values for this field + * @return array + */ + public function getSaveValue($value) + { + return $this->getLocaleSaveValue($value); + } + + /** + * {@inheritDoc} + */ + protected function loadAssets() + { + $this->actAsParent(); + parent::loadAssets(); + $this->actAsParent(false); + + if (Locale::isAvailable()) { + $this->loadLocaleAssets(); + $this->addJs('js/mlricheditor.js'); + } + } + + /** + * {@inheritDoc} + */ + public function onLoadPageLinksForm() + { + $this->actAsParent(); + return parent::onLoadPageLinksForm(); + } + + /** + * {@inheritDoc} + */ + protected function getParentViewPath() + { + return base_path().'/modules/backend/formwidgets/richeditor/partials'; + } + + /** + * {@inheritDoc} + */ + protected function getParentAssetPath() + { + return '/modules/backend/formwidgets/richeditor/assets'; + } +} diff --git a/plugins/rainlab/translate/formwidgets/MLText.php b/plugins/rainlab/translate/formwidgets/MLText.php new file mode 100644 index 00000000..c0a74bcb --- /dev/null +++ b/plugins/rainlab/translate/formwidgets/MLText.php @@ -0,0 +1,59 @@ +initLocale(); + } + + /** + * {@inheritDoc} + */ + public function render() + { + $this->prepareLocaleVars(); + + if ($this->isAvailable) { + return $this->makePartial('mltext'); + } + else { + return $this->renderFallbackField(); + } + } + + /** + * getSaveValue returns an array of translated values for this field + * @return array + */ + public function getSaveValue($value) + { + return $this->getLocaleSaveValue($value); + } + + /** + * {@inheritDoc} + */ + protected function loadAssets() + { + $this->loadLocaleAssets(); + } +} diff --git a/plugins/rainlab/translate/formwidgets/MLTextarea.php b/plugins/rainlab/translate/formwidgets/MLTextarea.php new file mode 100644 index 00000000..12b0837c --- /dev/null +++ b/plugins/rainlab/translate/formwidgets/MLTextarea.php @@ -0,0 +1,64 @@ +initLocale(); + } + + /** + * {@inheritDoc} + */ + public function render() + { + $this->prepareLocaleVars(); + + if ($this->isAvailable) { + return $this->makePartial('mltextarea'); + } + else { + return $this->renderFallbackField(); + } + } + + /** + * getSaveValue returns an array of translated values for this field + * @return array + */ + public function getSaveValue($value) + { + return $this->getLocaleSaveValue($value); + } + + /** + * {@inheritDoc} + */ + protected function loadAssets() + { + $this->loadLocaleAssets(); + } +} diff --git a/plugins/rainlab/translate/formwidgets/mlmarkdowneditor/assets/js/mlmarkdowneditor.js b/plugins/rainlab/translate/formwidgets/mlmarkdowneditor/assets/js/mlmarkdowneditor.js new file mode 100644 index 00000000..e6b0f565 --- /dev/null +++ b/plugins/rainlab/translate/formwidgets/mlmarkdowneditor/assets/js/mlmarkdowneditor.js @@ -0,0 +1,107 @@ +/* + * MLMarkdownEditor plugin + * + * Data attributes: + * - data-control="mlmarkdowneditor" - enables the plugin on an element + * - data-textarea-element="textarea#id" - an option with a value + * + * JavaScript API: + * $('a#someElement').mlMarkdownEditor({ option: 'value' }) + * + */ + ++function ($) { "use strict"; + + var Base = $.oc.foundation.base, + BaseProto = Base.prototype + + // MLMARKDOWNEDITOR CLASS DEFINITION + // ============================ + + var MLMarkdownEditor = function(element, options) { + this.options = options + this.$el = $(element) + this.$textarea = $(options.textareaElement) + this.$markdownEditor = $('[data-control=markdowneditor]:first', this.$el) + + $.oc.foundation.controlUtils.markDisposable(element) + Base.call(this) + + // Init + this.init() + } + + MLMarkdownEditor.prototype = Object.create(BaseProto) + MLMarkdownEditor.prototype.constructor = MLMarkdownEditor + + MLMarkdownEditor.DEFAULTS = { + textareaElement: null, + placeholderField: null, + defaultLocale: 'en' + } + + MLMarkdownEditor.prototype.init = function() { + this.$el.multiLingual() + + this.$el.on('setLocale.oc.multilingual', this.proxy(this.onSetLocale)) + this.$textarea.on('changeContent.oc.markdowneditor', this.proxy(this.onChangeContent)) + + this.$el.one('dispose-control', this.proxy(this.dispose)) + } + + MLMarkdownEditor.prototype.dispose = function() { + this.$el.off('setLocale.oc.multilingual', this.proxy(this.onSetLocale)) + this.$textarea.off('changeContent.oc.markdowneditor', this.proxy(this.onChangeContent)) + this.$el.off('dispose-control', this.proxy(this.dispose)) + + this.$el.removeData('oc.mlMarkdownEditor') + + this.$textarea = null + this.$markdownEditor = null + this.$el = null + + this.options = null + + BaseProto.dispose.call(this) + } + + MLMarkdownEditor.prototype.onSetLocale = function(e, locale, localeValue) { + if (typeof localeValue === 'string' && this.$markdownEditor.data('oc.markdownEditor')) { + this.$markdownEditor.markdownEditor('setContent', localeValue); + } + } + + MLMarkdownEditor.prototype.onChangeContent = function(ev, markdowneditor, value) { + this.$el.multiLingual('setLocaleValue', value) + } + + var old = $.fn.mlMarkdownEditor + + $.fn.mlMarkdownEditor = function (option) { + var args = Array.prototype.slice.call(arguments, 1), result + + this.each(function () { + var $this = $(this) + var data = $this.data('oc.mlMarkdownEditor') + var options = $.extend({}, MLMarkdownEditor.DEFAULTS, $this.data(), typeof option == 'object' && option) + if (!data) $this.data('oc.mlMarkdownEditor', (data = new MLMarkdownEditor(this, options))) + if (typeof option == 'string') result = data[option].apply(data, args) + if (typeof result != 'undefined') return false + }) + + return result ? result : this + } + + $.fn.mlMarkdownEditor.Constructor = MLMarkdownEditor; + + $.fn.mlMarkdownEditor.noConflict = function () { + $.fn.mlMarkdownEditor = old + return this + } + + $(document).render(function (){ + $('[data-control="mlmarkdowneditor"]').mlMarkdownEditor() + }) + + +}(window.jQuery); diff --git a/plugins/rainlab/translate/formwidgets/mlmarkdowneditor/partials/_mlmarkdowneditor.htm b/plugins/rainlab/translate/formwidgets/mlmarkdowneditor/partials/_mlmarkdowneditor.htm new file mode 100644 index 00000000..03179f90 --- /dev/null +++ b/plugins/rainlab/translate/formwidgets/mlmarkdowneditor/partials/_mlmarkdowneditor.htm @@ -0,0 +1,23 @@ + + diff --git a/plugins/rainlab/translate/formwidgets/mlmediafinder/assets/css/mlmediafinder.css b/plugins/rainlab/translate/formwidgets/mlmediafinder/assets/css/mlmediafinder.css new file mode 100644 index 00000000..9bd687b1 --- /dev/null +++ b/plugins/rainlab/translate/formwidgets/mlmediafinder/assets/css/mlmediafinder.css @@ -0,0 +1,8 @@ +.field-multilingual-mediafinder > .ml-btn { + top: -28px; + right: 4px; +} + +.field-multilingual-mediafinder > .ml-dropdown-menu { + top: 1px; +} diff --git a/plugins/rainlab/translate/formwidgets/mlmediafinder/assets/js/mlmediafinder.js b/plugins/rainlab/translate/formwidgets/mlmediafinder/assets/js/mlmediafinder.js new file mode 100644 index 00000000..1b512341 --- /dev/null +++ b/plugins/rainlab/translate/formwidgets/mlmediafinder/assets/js/mlmediafinder.js @@ -0,0 +1,136 @@ +/* + * MLMediaFinder plugin + * + * Data attributes: + * - data-control="mlmediafinder" - enables the plugin on an element + * - data-option="value" - an option with a value + * + * JavaScript API: + * $('a#someElement').mlMediaFinder({ option: 'value' }) + * + * Dependences: + * - mediafinder (mediafinder.js) + */ + ++function($) { "use strict"; + var Base = $.oc.foundation.base, + BaseProto = Base.prototype + + // MLMEDIAFINDER CLASS DEFINITION + // ============================ + + var MLMediaFinder = function(element, options) { + this.options = options + this.$el = $(element) + this.$mediafinder = $('[data-control=mediafinder]', this.$el) + this.$findValue = $('[data-find-value]', this.$el) + + $.oc.foundation.controlUtils.markDisposable(element) + Base.call(this) + this.init() + } + + MLMediaFinder.prototype = Object.create(BaseProto) + MLMediaFinder.prototype.constructor = MLMediaFinder + + MLMediaFinder.DEFAULTS = { + placeholderField: null, + defaultLocale: 'en', + mediaPath: '/', + } + + MLMediaFinder.prototype.init = function() { + + this.$el.multiLingual() + this.$el.on('setLocale.oc.multilingual', this.proxy(this.onSetLocale)) + this.$el.one('dispose-control', this.proxy(this.dispose)) + // Listen for change event from mediafinder + this.$findValue.on('change', this.proxy(this.setValue)) + + // Stop here for preview mode + if (this.options.isPreview) + return + } + + // Simplify setPath + MLMediaFinder.prototype.setValue = function(e) { + this.setPath($(e.target).val()) + } + + MLMediaFinder.prototype.dispose = function() { + this.$el.off('setLocale.oc.multilingual', this.proxy(this.onSetLocale)) + this.$el.off('dispose-control', this.proxy(this.dispose)) + this.$findValue.off('change', this.proxy(this.setValue)) + + this.$el.removeData('oc.mlMediaFinder') + + this.$findValue = null + this.$mediafinder = null; + this.$el = null + + // In some cases options could contain callbacks, + // so it's better to clean them up too. + this.options = null + + BaseProto.dispose.call(this) + } + + + MLMediaFinder.prototype.onSetLocale = function(e, locale, localeValue) { + this.setPath(localeValue) + } + + MLMediaFinder.prototype.setPath = function(localeValue) { + if (typeof localeValue === 'string') { + this.$findValue = localeValue; + + var path = localeValue ? this.options.mediaPath + localeValue : '' + + $('[data-find-image]', this.$mediafinder).attr('src', path) + $('[data-find-file-name]', this.$mediafinder).text(localeValue.substring(1)) + + // if value is present display image/file, else display open icon for media manager + this.$mediafinder.toggleClass('is-populated', !!localeValue) + + this.$el.multiLingual('setLocaleValue', localeValue); + } + } + + // MLMEDIAFINDER PLUGIN DEFINITION + // ============================ + + var old = $.fn.mlMediaFinder + + $.fn.mlMediaFinder = function (option) { + var args = Array.prototype.slice.call(arguments, 1), result + this.each(function () { + var $this = $(this) + var data = $this.data('oc.mlMediaFinder') + var options = $.extend({}, MLMediaFinder.DEFAULTS, $this.data(), typeof option == 'object' && option) + if (!data) $this.data('oc.mlMediaFinder', (data = new MLMediaFinder(this, options))) + if (typeof option === 'string') result = data[option].apply(data, args) + if (typeof result !== 'undefined') return false + }) + + return result ? result : this + } + + $.fn.mlMediaFinder.Constructor = MLMediaFinder + + // MLMEDIAFINDER NO CONFLICT + // ================= + + $.fn.mlMediaFinder.noConflict = function () { + $.fn.mlMediaFinder = old + return this + } + + // MLMEDIAFINDER DATA-API + // =============== + + $(document).render(function () { + $('[data-control="mlmediafinder"]').mlMediaFinder() + }) + + +}(window.jQuery); diff --git a/plugins/rainlab/translate/formwidgets/mlmediafinder/partials/_mlmediafinder.htm b/plugins/rainlab/translate/formwidgets/mlmediafinder/partials/_mlmediafinder.htm new file mode 100644 index 00000000..8c0d86e5 --- /dev/null +++ b/plugins/rainlab/translate/formwidgets/mlmediafinder/partials/_mlmediafinder.htm @@ -0,0 +1,22 @@ + + diff --git a/plugins/rainlab/translate/formwidgets/mlmediafinderv2/assets/js/mlmediafinder.js b/plugins/rainlab/translate/formwidgets/mlmediafinderv2/assets/js/mlmediafinder.js new file mode 100644 index 00000000..7e0f562c --- /dev/null +++ b/plugins/rainlab/translate/formwidgets/mlmediafinderv2/assets/js/mlmediafinder.js @@ -0,0 +1,180 @@ +/* + * MLMediaFinder plugin + * + * Data attributes: + * - data-control="mlmediafinder" - enables the plugin on an element + * - data-option="value" - an option with a value + * + * JavaScript API: + * $('a#someElement').mlMediaFinder({ option: 'value' }) + * + * Dependences: + * - mediafinder (mediafinder.js) + */ + ++function($) { "use strict"; + var Base = $.oc.foundation.base, + BaseProto = Base.prototype + + // MLMEDIAFINDER CLASS DEFINITION + // ============================ + + var MLMediaFinder = function(element, options) { + this.options = options + this.$el = $(element) + this.$mediafinder = $('[data-control=mediafinder]', this.$el) + this.$dataLocker = $('[data-data-locker]', this.$el) + this.isMulti = this.$mediafinder.hasClass('is-multi') + + $.oc.foundation.controlUtils.markDisposable(element) + Base.call(this) + this.init() + } + + MLMediaFinder.prototype = Object.create(BaseProto) + MLMediaFinder.prototype.constructor = MLMediaFinder + + MLMediaFinder.DEFAULTS = { + placeholderField: null, + defaultLocale: 'en', + mediaPath: '/', + } + + MLMediaFinder.prototype.init = function() { + + this.$el.multiLingual() + this.$el.on('setLocale.oc.multilingual', this.proxy(this.onSetLocale)) + this.$el.one('dispose-control', this.proxy(this.dispose)) + + // Listen for change event from mediafinder + this.$dataLocker.on('change', this.proxy(this.setValue)) + + // Stop here for preview mode + if (this.options.isPreview) { + return; + } + } + + MLMediaFinder.prototype.dispose = function() { + this.$el.off('setLocale.oc.multilingual', this.proxy(this.onSetLocale)); + this.$el.off('dispose-control', this.proxy(this.dispose)); + this.$dataLocker.off('change', this.proxy(this.setValue)); + + this.$el.removeData('oc.mlMediaFinder'); + + this.$dataLocker = null; + this.$mediafinder = null; + this.$el = null; + + // In some cases options could contain callbacks, + // so it's better to clean them up too. + this.options = null; + + BaseProto.dispose.call(this) + } + + MLMediaFinder.prototype.setValue = function(e) { + var mediafinder = this.$mediafinder.data('oc.mediaFinder'), + value = mediafinder.getValue(); + + if (value) { + if (this.isMulti) { + value = JSON.stringify(value); + } + else { + value = value[0]; + } + } + + this.setPath(value); + } + + MLMediaFinder.prototype.onSetLocale = function(e, locale, localeValue) { + this.setPath(localeValue) + } + + MLMediaFinder.prototype.setPath = function(localeValue) { + if (typeof localeValue === 'string') { + var self = this, + isMulti = this.isMulti, + mediaFinder = this.$mediafinder.data('oc.mediaFinder'), + items = [], + localeValueArr = []; + + try { + localeValueArr = JSON.parse(localeValue); + if (!$.isArray(localeValueArr)) { + localeValueArr = [localeValueArr]; + } + } + catch(e) { + isMulti = false; + } + + mediaFinder.$filesContainer.empty(); + + if (isMulti) { + $.each(localeValueArr, function(k, v) { + if (v) { + items.push({ + path: v, + publicUrl: self.options.mediaPath + v, + title: v.substring(1) + }); + } + }); + } + else { + if (localeValue) { + items = [{ + path: localeValue, + publicUrl: this.options.mediaPath + localeValue, + title: localeValue.substring(1) + }]; + } + } + + mediaFinder.addItems(items); + mediaFinder.evalIsPopulated(); + mediaFinder.evalIsMaxReached(); + + this.$el.multiLingual('setLocaleValue', localeValue); + } + } + + // MLMEDIAFINDER PLUGIN DEFINITION + // ============================ + + var old = $.fn.mlMediaFinder + + $.fn.mlMediaFinder = function (option) { + var args = Array.prototype.slice.call(arguments, 1), result + this.each(function () { + var $this = $(this) + var data = $this.data('oc.mlMediaFinder') + var options = $.extend({}, MLMediaFinder.DEFAULTS, $this.data(), typeof option == 'object' && option) + if (!data) $this.data('oc.mlMediaFinder', (data = new MLMediaFinder(this, options))) + if (typeof option === 'string') result = data[option].apply(data, args) + if (typeof result !== 'undefined') return false + }) + + return result ? result : this + } + + $.fn.mlMediaFinder.Constructor = MLMediaFinder + + // MLMEDIAFINDER NO CONFLICT + // ================= + + $.fn.mlMediaFinder.noConflict = function () { + $.fn.mlMediaFinder = old + return this + } + + // MLMEDIAFINDER DATA-API + // =============== + + $(document).render(function () { + $('[data-control="mlmediafinder"]').mlMediaFinder() + }); +}(window.jQuery); diff --git a/plugins/rainlab/translate/formwidgets/mlmediafinderv2/partials/_mlmediafinder.htm b/plugins/rainlab/translate/formwidgets/mlmediafinderv2/partials/_mlmediafinder.htm new file mode 100644 index 00000000..8c0d86e5 --- /dev/null +++ b/plugins/rainlab/translate/formwidgets/mlmediafinderv2/partials/_mlmediafinder.htm @@ -0,0 +1,22 @@ + + diff --git a/plugins/rainlab/translate/formwidgets/mlnestedform/assets/js/mlnestedform.js b/plugins/rainlab/translate/formwidgets/mlnestedform/assets/js/mlnestedform.js new file mode 100644 index 00000000..ace10ad0 --- /dev/null +++ b/plugins/rainlab/translate/formwidgets/mlnestedform/assets/js/mlnestedform.js @@ -0,0 +1,126 @@ +/* + * MLNestedForm plugin + * + * Data attributes: + * - data-control="mlnestedform" - enables the plugin on an element + * + * JavaScript API: + * $('a#someElement').mlNestedForm({ option: 'value' }) + * + */ ++function ($) { "use strict"; + + var Base = $.oc.foundation.base, + BaseProto = Base.prototype + + // MLREPEATER CLASS DEFINITION + // ============================ + + var MLNestedForm = function(element, options) { + this.options = options; + this.$el = $(element); + this.$selector = $('[data-locale-dropdown]', this.$el); + this.$locale = $('[data-nestedform-active-locale]', this.$el); + this.locale = options.defaultLocale; + + $.oc.foundation.controlUtils.markDisposable(element); + Base.call(this); + + // Init + this.init(); + } + + MLNestedForm.prototype = Object.create(BaseProto); + MLNestedForm.prototype.constructor = MLNestedForm; + + MLNestedForm.DEFAULTS = { + switchHandler: null, + defaultLocale: 'en' + } + + MLNestedForm.prototype.init = function() { + this.$el.multiLingual(); + + this.$el.on('setLocale.oc.multilingual', this.proxy(this.onSetLocale)); + + this.$el.one('dispose-control', this.proxy(this.dispose)); + } + + MLNestedForm.prototype.dispose = function() { + this.$el.off('setLocale.oc.multilingual', this.proxy(this.onSetLocale)); + + this.$el.off('dispose-control', this.proxy(this.dispose)); + + this.$el.removeData('oc.mlNestedForm'); + + this.$selector = null; + this.$locale = null; + this.locale = null; + this.$el = null; + + this.options = null; + + BaseProto.dispose.call(this); + } + + MLNestedForm.prototype.onSetLocale = function(e, locale, localeValue) { + var self = this, + previousLocale = this.locale; + + this.$el + .addClass('loading-indicator-container size-form-field') + .loadIndicator(); + + this.locale = locale; + this.$locale.val(locale); + + this.$el.request(this.options.switchHandler, { + data: { + _nestedform_previous_locale: previousLocale, + _nestedform_locale: locale + }, + success: function(data) { + self.$el.multiLingual('setLocaleValue', data.updateValue, data.updateLocale); + self.$el.loadIndicator('hide'); + this.success(data); + } + }); + } + + // MLREPEATER PLUGIN DEFINITION + // ============================ + + var old = $.fn.mlNestedForm; + + $.fn.mlNestedForm = function (option) { + var args = Array.prototype.slice.call(arguments, 1), result; + this.each(function () { + var $this = $(this); + var data = $this.data('oc.mlNestedForm'); + var options = $.extend({}, MLNestedForm.DEFAULTS, $this.data(), typeof option == 'object' && option); + if (!data) $this.data('oc.mlNestedForm', (data = new MLNestedForm(this, options))); + if (typeof option == 'string') result = data[option].apply(data, args); + if (typeof result != 'undefined') return false; + }) + + return result ? result : this; + } + + $.fn.mlNestedForm.Constructor = MLNestedForm; + + // MLREPEATER NO CONFLICT + // ================= + + $.fn.mlNestedForm.noConflict = function () { + $.fn.mlNestedForm = old + return this + } + + // MLREPEATER DATA-API + // =============== + + $(document).render(function () { + $('[data-control="mlnestedform"]').mlNestedForm(); + }); + +}(window.jQuery); diff --git a/plugins/rainlab/translate/formwidgets/mlnestedform/partials/_mlnestedform.htm b/plugins/rainlab/translate/formwidgets/mlnestedform/partials/_mlnestedform.htm new file mode 100644 index 00000000..d46d6f93 --- /dev/null +++ b/plugins/rainlab/translate/formwidgets/mlnestedform/partials/_mlnestedform.htm @@ -0,0 +1,31 @@ + + diff --git a/plugins/rainlab/translate/formwidgets/mlrepeater/assets/js/mlrepeater.js b/plugins/rainlab/translate/formwidgets/mlrepeater/assets/js/mlrepeater.js new file mode 100644 index 00000000..d412b0d1 --- /dev/null +++ b/plugins/rainlab/translate/formwidgets/mlrepeater/assets/js/mlrepeater.js @@ -0,0 +1,139 @@ +/* + * MLRepeater plugin + * + * Data attributes: + * - data-control="mlrepeater" - enables the plugin on an element + * + * JavaScript API: + * $('a#someElement').mlRepeater({ option: 'value' }) + * + */ + ++function ($) { "use strict"; + + var Base = $.oc.foundation.base, + BaseProto = Base.prototype + + // MLREPEATER CLASS DEFINITION + // ============================ + + var MLRepeater = function(element, options) { + this.options = options + this.$el = $(element) + this.$selector = $('[data-locale-dropdown]', this.$el) + this.$locale = $('[data-repeater-active-locale]', this.$el) + this.locale = options.defaultLocale + + $.oc.foundation.controlUtils.markDisposable(element) + Base.call(this) + + // Init + this.init() + } + + MLRepeater.prototype = Object.create(BaseProto) + MLRepeater.prototype.constructor = MLRepeater + + MLRepeater.DEFAULTS = { + switchHandler: null, + defaultLocale: 'en' + } + + MLRepeater.prototype.init = function() { + this.$el.multiLingual() + + this.checkEmptyItems() + + $(document).on('render', this.proxy(this.checkEmptyItems)) + + this.$el.on('setLocale.oc.multilingual', this.proxy(this.onSetLocale)) + + this.$el.one('dispose-control', this.proxy(this.dispose)) + } + + MLRepeater.prototype.dispose = function() { + + $(document).off('render', this.proxy(this.checkEmptyItems)) + + this.$el.off('setLocale.oc.multilingual', this.proxy(this.onSetLocale)) + + this.$el.off('dispose-control', this.proxy(this.dispose)) + + this.$el.removeData('oc.mlRepeater') + + this.$selector = null + this.$locale = null + this.locale = null + this.$el = null + + this.options = null + + BaseProto.dispose.call(this) + } + + MLRepeater.prototype.checkEmptyItems = function() { + var isEmpty = !$('ul.field-repeater-items > li', this.$el).length + this.$el.toggleClass('is-empty', isEmpty) + } + + MLRepeater.prototype.onSetLocale = function(e, locale, localeValue) { + var self = this, + previousLocale = this.locale + + this.$el + .addClass('loading-indicator-container size-form-field') + .loadIndicator() + + this.locale = locale + this.$locale.val(locale) + + this.$el.request(this.options.switchHandler, { + data: { + _repeater_previous_locale: previousLocale, + _repeater_locale: locale + }, + success: function(data) { + self.$el.multiLingual('setLocaleValue', data.updateValue, data.updateLocale) + self.$el.loadIndicator('hide') + this.success(data) + } + }) + } + + // MLREPEATER PLUGIN DEFINITION + // ============================ + + var old = $.fn.mlRepeater + + $.fn.mlRepeater = function (option) { + var args = Array.prototype.slice.call(arguments, 1), result + this.each(function () { + var $this = $(this) + var data = $this.data('oc.mlRepeater') + var options = $.extend({}, MLRepeater.DEFAULTS, $this.data(), typeof option == 'object' && option) + if (!data) $this.data('oc.mlRepeater', (data = new MLRepeater(this, options))) + if (typeof option == 'string') result = data[option].apply(data, args) + if (typeof result != 'undefined') return false + }) + + return result ? result : this + } + + $.fn.mlRepeater.Constructor = MLRepeater + + // MLREPEATER NO CONFLICT + // ================= + + $.fn.mlRepeater.noConflict = function () { + $.fn.mlRepeater = old + return this + } + + // MLREPEATER DATA-API + // =============== + + $(document).render(function () { + $('[data-control="mlrepeater"]').mlRepeater() + }) + +}(window.jQuery); diff --git a/plugins/rainlab/translate/formwidgets/mlrepeater/partials/_mlrepeater.htm b/plugins/rainlab/translate/formwidgets/mlrepeater/partials/_mlrepeater.htm new file mode 100644 index 00000000..1d6480e9 --- /dev/null +++ b/plugins/rainlab/translate/formwidgets/mlrepeater/partials/_mlrepeater.htm @@ -0,0 +1,31 @@ + + diff --git a/plugins/rainlab/translate/formwidgets/mlricheditor/assets/js/mlricheditor.js b/plugins/rainlab/translate/formwidgets/mlricheditor/assets/js/mlricheditor.js new file mode 100644 index 00000000..807848a1 --- /dev/null +++ b/plugins/rainlab/translate/formwidgets/mlricheditor/assets/js/mlricheditor.js @@ -0,0 +1,135 @@ +/* + * MLRichEditor plugin + * + * Data attributes: + * - data-control="mlricheditor" - enables the plugin on an element + * - data-textarea-element="textarea#id" - an option with a value + * + * JavaScript API: + * $('a#someElement').mlRichEditor({ option: 'value' }) + * + */ + ++function ($) { "use strict"; + + var Base = $.oc.foundation.base, + BaseProto = Base.prototype + + // MLRICHEDITOR CLASS DEFINITION + // ============================ + + var MLRichEditor = function(element, options) { + this.options = options + this.$el = $(element) + this.$textarea = $(options.textareaElement) + this.$richeditor = $('[data-control=richeditor]:first', this.$el) + + $.oc.foundation.controlUtils.markDisposable(element) + Base.call(this) + + // Init + this.init() + } + + MLRichEditor.prototype = Object.create(BaseProto) + MLRichEditor.prototype.constructor = MLRichEditor + + MLRichEditor.DEFAULTS = { + textareaElement: null, + placeholderField: null, + defaultLocale: 'en' + } + + MLRichEditor.prototype.init = function() { + this.$el.multiLingual() + + this.$el.on('setLocale.oc.multilingual', this.proxy(this.onSetLocale)) + this.$textarea.on('syncContent.oc.richeditor', this.proxy(this.onSyncContent)) + + this.updateLayout() + + $(window).on('resize', this.proxy(this.updateLayout)) + $(window).on('oc.updateUi', this.proxy(this.updateLayout)) + this.$el.one('dispose-control', this.proxy(this.dispose)) + } + + MLRichEditor.prototype.dispose = function() { + this.$el.off('setLocale.oc.multilingual', this.proxy(this.onSetLocale)) + this.$textarea.off('syncContent.oc.richeditor', this.proxy(this.onSyncContent)) + $(window).off('resize', this.proxy(this.updateLayout)) + $(window).off('oc.updateUi', this.proxy(this.updateLayout)) + + this.$el.off('dispose-control', this.proxy(this.dispose)) + + this.$el.removeData('oc.mlRichEditor') + + this.$textarea = null + this.$richeditor = null + this.$el = null + + this.options = null + + BaseProto.dispose.call(this) + } + + MLRichEditor.prototype.onSetLocale = function(e, locale, localeValue) { + if (typeof localeValue === 'string' && this.$richeditor.data('oc.richEditor')) { + this.$richeditor.richEditor('setContent', localeValue); + } + } + + MLRichEditor.prototype.onSyncContent = function(ev, richeditor, value) { + this.$el.multiLingual('setLocaleValue', value.html) + } + + MLRichEditor.prototype.updateLayout = function() { + var $toolbar = $('.fr-toolbar', this.$el), + $btn = $('.ml-btn[data-active-locale]:first', this.$el), + $dropdown = $('.ml-dropdown-menu[data-locale-dropdown]:first', this.$el) + + if (!$toolbar.length) { + return + } + + var height = $toolbar.outerHeight(true) + $btn.css('top', height + 6) + $dropdown.css('top', height + 40) + } + + // MLRICHEDITOR PLUGIN DEFINITION + // ============================ + + var old = $.fn.mlRichEditor + + $.fn.mlRichEditor = function (option) { + var args = Array.prototype.slice.call(arguments, 1), result + this.each(function () { + var $this = $(this) + var data = $this.data('oc.mlRichEditor') + var options = $.extend({}, MLRichEditor.DEFAULTS, $this.data(), typeof option == 'object' && option) + if (!data) $this.data('oc.mlRichEditor', (data = new MLRichEditor(this, options))) + if (typeof option == 'string') result = data[option].apply(data, args) + if (typeof result != 'undefined') return false + }) + + return result ? result : this + } + + $.fn.mlRichEditor.Constructor = MLRichEditor + + // MLRICHEDITOR NO CONFLICT + // ================= + + $.fn.mlRichEditor.noConflict = function () { + $.fn.mlRichEditor = old + return this + } + + // MLRICHEDITOR DATA-API + // =============== + + $(document).render(function () { + $('[data-control="mlricheditor"]').mlRichEditor() + }) + +}(window.jQuery); diff --git a/plugins/rainlab/translate/formwidgets/mlricheditor/partials/_mlricheditor.htm b/plugins/rainlab/translate/formwidgets/mlricheditor/partials/_mlricheditor.htm new file mode 100644 index 00000000..6f3e19c0 --- /dev/null +++ b/plugins/rainlab/translate/formwidgets/mlricheditor/partials/_mlricheditor.htm @@ -0,0 +1,23 @@ + + diff --git a/plugins/rainlab/translate/formwidgets/mltext/partials/_mltext.htm b/plugins/rainlab/translate/formwidgets/mltext/partials/_mltext.htm new file mode 100644 index 00000000..b8a57350 --- /dev/null +++ b/plugins/rainlab/translate/formwidgets/mltext/partials/_mltext.htm @@ -0,0 +1,36 @@ + +previewMode): ?> + value ? e($field->value) : ' ' ?> + + + diff --git a/plugins/rainlab/translate/formwidgets/mltextarea/partials/_mltextarea.htm b/plugins/rainlab/translate/formwidgets/mltextarea/partials/_mltextarea.htm new file mode 100644 index 00000000..1a710bd2 --- /dev/null +++ b/plugins/rainlab/translate/formwidgets/mltextarea/partials/_mltextarea.htm @@ -0,0 +1,32 @@ + +previewMode): ?> +
    value)) ?>
    + + + diff --git a/plugins/rainlab/translate/lang/ar/ar.php b/plugins/rainlab/translate/lang/ar/ar.php new file mode 100644 index 00000000..aee91f44 --- /dev/null +++ b/plugins/rainlab/translate/lang/ar/ar.php @@ -0,0 +1,59 @@ + [ + 'name' => 'ترجمة', + 'description' => 'تفعيل تعدد اللغات للموقع.', + 'tab' => 'ترجمة', + 'manage_locales' => 'إدارة اللغات', + 'manage_messages' => 'إدارة النصوص', + ], + 'locale_picker' => [ + 'component_name' => 'محدد اللغات', + 'component_description' => 'عرض قائمة لتحديد لغة الواجهة.', + ], + 'alternate_hreflang' => [ + 'component_name' => 'عناصر بدائل hrefLang', + 'component_description' => 'إضافة بدائل اللغة للصفحة كعنصر hreflang ' + ], + 'locale' => [ + 'title' => 'إدارة اللغات', + 'update_title' => 'تحديث اللغة', + 'create_title' => 'إنشاء لغة', + 'select_label' => 'تحديد اللغة', + 'default_suffix' => 'افتراضي', + 'unset_default' => '":locale" بالفعل افتراضي ولايمكن نزع الافتراضية عنها.', + 'delete_default' => '":locale" هو افتراضي فلا يمكن حذفه.', + 'disabled_default' => '":locale" معطل فلايمكن تعيينه كافتراضي.', + 'name' => 'اسم', + 'code' => 'رمز', + 'is_default' => 'افتراضي', + 'is_default_help' => 'اللغة الافتراضية تمثل المحتوى قبل ترجمته.', + 'is_enabled' => 'مفعل', + 'is_enabled_help' => 'اللغات المعطلة ستكون غير متاحة في الواجهة.', + 'not_available_help' => 'لا توجد لغات أخرى لإعدادها.', + 'hint_locales' => 'إنشاء لغة جديدة هنا لترجمة محتويات الواجهة. اللغة الافتراضية تمثل المحتوى قبل ترجمته.', + 'reorder_title' => 'إعادة ترتيب لغات', + 'sort_order' => 'اتجاه الترتيب', + ], + 'messages' => [ + 'title' => 'ترجمة النصوص', + 'description' => 'تحديث النصوص', + 'clear_cache_link' => 'مسح المخبآت', + 'clear_cache_loading' => 'يتم مسح مخبآت التطبيق...', + 'clear_cache_success' => 'تم مسح مخبآت التطبيق بنجاح!', + 'clear_cache_hint' => 'ربما يجب عليك الضغط على مسح المخبآت لترى التغييرات في الواجهة.', + 'scan_messages_link' => 'استكشاف النصوص', + 'scan_messages_begin_scan' => 'بدء الاستكشاف', + 'scan_messages_loading' => 'يتم استكشاف نصوص جديدة...', + 'scan_messages_success' => 'تم استكشاف ملفات القالب بنجاح!', + 'scan_messages_hint' => 'عند الضغط على استكشاف النصوص سيتم استكشاف ملفات القالب النشط لإيجاد نصوص جديدة لترجمتها.', + 'scan_messages_process' => 'هذه العملية ستقوم باستكشاف ملفات القالب النشط لإيجاد أي نصوص جديدة يمكن ترجمتها.', + 'scan_messages_process_limitations' => 'بعض النصوص قد لا يتم التقاطها وستظهر بعد أول استعمال لها.', + 'scan_messages_purge_label' => 'إفراغ جميع الرسائل أولا', + 'scan_messages_purge_help' => 'عند تحديد هذه الخاصية سيتم حذف جميع النصوص قبل علية الاستكشاف.', + 'scan_messages_purge_confirm' => 'هل أنت متأكد من حذف جميع النصوص? لن يمكنك التراجع بعد الآن!', + 'hint_translate' => 'هنا يمكن ترجمة النصوص المستعملة في الواجهة، سيتم الحفظ آليا.', + 'hide_translated' => 'إخفاء النصوص المترجمة', + 'export_messages_link' => 'تصدير النصوص', + 'import_messages_link' => 'استيراد النصوص', + ], +]; diff --git a/plugins/rainlab/translate/lang/bg/lang.php b/plugins/rainlab/translate/lang/bg/lang.php new file mode 100644 index 00000000..99d362c3 --- /dev/null +++ b/plugins/rainlab/translate/lang/bg/lang.php @@ -0,0 +1,46 @@ + [ + 'name' => 'Превод', + 'description' => 'Активира функцията многоезични уебсайтове.', + 'tab' => 'Преводи', + 'manage_locales' => 'Настройка на локация', + 'manage_messages' => 'Настройка на съобщения' + ], + 'locale_picker' => [ + 'component_name' => 'Избор на местоположение', + 'component_description' => 'Показва меню за избор на езика на сайта.', + ], + 'locale' => [ + 'title' => 'Настройка на езици', + 'update_title' => 'Актуализация на език', + 'create_title' => 'Създай език', + 'select_label' => 'Избери език', + 'default_suffix' => 'по подразбиране', + 'unset_default' => '":locale" вече е по подразбиране и не може да бъде изключен по подразбиране.', + 'disabled_default' => '":locale" е забранен и не може да бъде зададен по подразбиране.', + 'name' => 'Име', + 'code' => 'Код', + 'is_default' => 'По подразбиране', + 'is_default_help' => 'Основният език представлява съдържанието на сайта преди превод.', + 'is_enabled' => 'Включен', + 'is_enabled_help' => 'Изключените езици няма да са налични за преглед в сайта.', + 'not_available_help' => 'Не съществуват други езици за настройка.', + 'hint_locales' => 'Създайте нови езици за превод на съдържанието на сайта. Основният език представлява съдържанието, преди той да бъде преведен.', + ], + 'messages' => [ + 'title' => 'Преведени Съобщения', + 'description' => 'Актуализарай Съобщения', + 'clear_cache_link' => 'Изтрий кеш-паметта', + 'clear_cache_loading' => 'Изчистване кеша на приложението...', + 'clear_cache_success' => 'Кеш кеш-паметта успешно изтрита!', + 'clear_cache_hint' => 'Може да е необходимо да кликнете на бутона Изтрий кеш-паметта за да видите промените на вашата страница.', + 'scan_messages_link' => 'Сканирай за съобщения', + 'scan_messages_loading' => 'Сканиране за нови съобщения...', + 'scan_messages_success' => 'Темата е сканирана успешно!', + 'scan_messages_hint' => 'Ако кликнете на Сканирай за съобщения това ще провери темата за нови съобщения (изречения) за превод.', + 'hint_translate' => 'Тук може да превеждате съобщенията (изреченията) на самият сайт, полетата ще се запазят автоматично.', + 'hide_translated' => 'Скрий преведените', + ], +]; \ No newline at end of file diff --git a/plugins/rainlab/translate/lang/cs/lang.php b/plugins/rainlab/translate/lang/cs/lang.php new file mode 100644 index 00000000..8bc9f5d4 --- /dev/null +++ b/plugins/rainlab/translate/lang/cs/lang.php @@ -0,0 +1,46 @@ + [ + 'name' => 'Překlady', + 'description' => 'Aktivuje vícejazyčné stránky a překlady.', + 'tab' => 'Překlad', + 'manage_locales' => 'Správa jazyků', + 'manage_messages' => 'Správa překladů' + ], + 'locale_picker' => [ + 'component_name' => 'Výběr jazyka', + 'component_description' => 'Zobrazí možnost výberu jazyka ve stránkách.', + ], + 'locale' => [ + 'title' => 'Správa jazyků', + 'update_title' => 'Upravit jazyk', + 'create_title' => 'Přidat jazyk', + 'select_label' => 'Výběr jazyka', + 'default_suffix' => 'výchozí', + 'unset_default' => '":locale" je již výchozí a nemůže být odnastavena. Zkuste nastavit jiný jazyk jako výchozí.', + 'disabled_default' => '":locale" je neaktivní, takže nemůže být nastavený jako výchozí.', + 'name' => 'Název', + 'code' => 'Kód', + 'is_default' => 'Výchozí', + 'is_default_help' => 'Výchozí jazyk je jazyk webových stránek před překladem.', + 'is_enabled' => 'Aktivní', + 'is_enabled_help' => 'Neaktivní jazyky nepůjdou vybrat na webových stránkách.', + 'not_available_help' => 'Nemáte nastavené žádné jiné jazyky.', + 'hint_locales' => 'Zde můžete přidat nový jazyk pro překlad webových stránek. Výchozí jazyk reprezentuje obsah stránek ještě před překladem.', + ], + 'messages' => [ + 'title' => 'Překlad textů', + 'description' => 'Upravit text', + 'clear_cache_link' => 'Vymazat cache', + 'clear_cache_loading' => 'Mazání aplikační cache...', + 'clear_cache_success' => 'Aplikační cache úspěšně vymazána!', + 'clear_cache_hint' => 'Možná bude potřeba kliknout na Vymazat cache, aby se změny projevily na webobých stránkách.', + 'scan_messages_link' => 'Najít texty k překladu', + 'scan_messages_loading' => 'Hledání textů k překladu...', + 'scan_messages_success' => 'Prohledávání šablon pro získání textů k překladu úspěšně dokončeno!', + 'scan_messages_hint' => 'Kliknutím na Najít texty k překladu zkontroluje soubory aktivních témat a najde texty k překladu.', + 'hint_translate' => 'Zde můžete přeložit texty použité na webových stránkách. Pole budou automaticky uložena.', + 'hide_translated' => 'Schovat přeložené', + ], +]; diff --git a/plugins/rainlab/translate/lang/de/lang.php b/plugins/rainlab/translate/lang/de/lang.php new file mode 100644 index 00000000..bd6e366b --- /dev/null +++ b/plugins/rainlab/translate/lang/de/lang.php @@ -0,0 +1,47 @@ + [ + 'name' => 'Translate', + 'description' => 'Ermöglicht mehrsprachige Seiten.', + 'manage_locales' => 'Sprachen verwalten', + 'manage_messages' => 'Übersetzungen verwalten', + ], + 'locale_picker' => [ + 'component_name' => 'Sprachauswahl', + 'component_description' => 'Zeigt ein Dropdown-Menü zur Auswahl der Sprache im Frontend.', + ], + 'locale' => [ + 'title' => 'Sprachen verwalten', + 'update_title' => 'Sprache bearbeiten', + 'create_title' => 'Sprache erstellen', + 'select_label' => 'Sprache auswählen', + 'default_suffix' => 'Standard', + 'unset_default' => '":locale" ist bereits die Standardsprache und kann nicht abgewählt werden.', + 'disabled_default' => '":locale" ist deaktiviert und kann deshalb nicht als Standardsprache festgelegt werden.', + 'name' => 'Name', + 'code' => 'Code', + 'is_default' => 'Standard', + 'is_default_help' => 'Die Übersetzung der Standardsprache wird verwendet, um Inhalte anzuzeigen, die in der Sprache des Nutzers nicht vorhanden sind.', + 'is_enabled' => 'Aktiv', + 'is_enabled_help' => 'Deaktivierte Sprachen sind im Frontend nicht verfügbar.', + 'not_available_help' => 'Es gibt keine anderen Sprachen.', + 'hint_locales' => 'Hier können neue Sprachen angelegt werden, in die Inhalte im Frontend übersetzt werden können. Die Standardsprache dient als Ausgangssprache für Übersetzungen.', + 'reorder_title' => 'Sprachen sortieren', + 'sort_order' => 'Sortierung', + ], + 'messages' => [ + 'title' => 'Übersetzungen verwalten', + 'description' => 'Inhalte verwalten und übersetzen', + 'clear_cache_link' => 'Cache leeren', + 'clear_cache_loading' => 'Leere Application-Cache...', + 'clear_cache_success' => 'Application-Cache erfolgreich geleert!', + 'clear_cache_hint' => 'Möglicherweise muss der Cache geleert werden (Button Cache leeren), bevor Änderungen im Frontend sichtbar werden.', + 'scan_messages_link' => 'Nach Inhalten suchen', + 'scan_messages_loading' => 'Suche nach neuen Inhalte...', + 'scan_messages_success' => 'Suche nach neuen Inhalte erfolgreich abgeschlossen!', + 'scan_messages_hint' => 'Ein Klick auf Nach Inhalten suchen sucht nach neuen Inhalten, die übersetzt werden können.', + 'hint_translate' => 'Hier können Inhalte aus dem Frontend übersetzt werden. Die Felder werden automatisch gespeichert.', + 'hide_translated' => 'Bereits übersetzte Inhalte ausblenden', + 'export_messages_link' => 'Übersetzungen exportieren', + 'import_messages_link' => 'Übersetzungen importieren', + ], +]; diff --git a/plugins/rainlab/translate/lang/en/lang.php b/plugins/rainlab/translate/lang/en/lang.php new file mode 100644 index 00000000..8fe2ce3f --- /dev/null +++ b/plugins/rainlab/translate/lang/en/lang.php @@ -0,0 +1,64 @@ + [ + 'name' => 'Translate', + 'description' => 'Enables multi-lingual websites.', + 'tab' => 'Translation', + 'manage_locales' => 'Manage locales', + 'manage_messages' => 'Manage messages', + ], + 'locale_picker' => [ + 'component_name' => 'Locale Picker', + 'component_description' => 'Shows a dropdown to select a front-end language.', + ], + 'alternate_hreflang' => [ + 'component_name' => 'Alternate hrefLang elements', + 'component_description' => 'Injects the language alternatives for page as hreflang elements' + ], + 'locale' => [ + 'title' => 'Manage Languages', + 'update_title' => 'Update Language', + 'create_title' => 'Create Language', + 'select_label' => 'Select Language', + 'default_suffix' => 'default', + 'unset_default' => '":locale" is already default and cannot be unset as default.', + 'delete_default' => '":locale" is the default and cannot be deleted.', + 'disabled_default' => '":locale" is disabled and cannot be set as default.', + 'name' => 'Name', + 'code' => 'Code', + 'is_default' => 'Default', + 'is_default_help' => 'The default language represents the content before translation.', + 'is_enabled' => 'Enabled', + 'is_enabled_help' => 'Disabled languages will not be available in the front-end.', + 'not_available_help' => 'There are no other languages set up.', + 'hint_locales' => 'Create new languages here for translating front-end content. The default language represents the content before it has been translated.', + 'reorder_title' => 'Reorder Languages', + 'sort_order' => 'Sort Order', + ], + 'messages' => [ + 'title' => 'Translate Messages', + 'description' => 'Update messages', + 'clear_cache_link' => 'Clear Cache', + 'clear_cache_loading' => 'Clearing application cache...', + 'clear_cache_success' => 'Cleared the application cache successfully!', + 'clear_cache_hint' => 'You may need to click Clear cache to see the changes on the front-end.', + 'scan_messages_link' => 'Scan for Messages', + 'scan_messages_begin_scan' => 'Begin Scan', + 'scan_messages_loading' => 'Scanning for new messages...', + 'scan_messages_success' => 'Scanned theme template files successfully!', + 'scan_messages_hint' => 'Clicking Scan for messages will check the active theme files for any new messages to translate.', + 'scan_messages_process' => 'This process will attempt to scan the active theme for messages that can be translated.', + 'scan_messages_process_limitations' => 'Some messages may not be captured and will only appear after the first time they are used.', + 'scan_messages_purge_label' => 'Purge all messages first', + 'scan_messages_purge_help' => 'If checked, this will delete all messages, including their translations, before performing the scan.', + 'scan_messages_purge_confirm' => 'Are you sure you want to delete all messages? This cannot be undone!', + 'scan_messages_purge_deleted_label' => 'Purge missing messages after scan', + 'scan_messages_purge_deleted_help' => 'If checked, after the scan is done, any messages the scanner did not find, including their translations, will be deleted. This cannot be undone!', + 'hint_translate' => 'Here you can translate messages used on the front-end, the fields will save automatically.', + 'hide_translated' => 'Hide translated', + 'export_messages_link' => 'Export Messages', + 'import_messages_link' => 'Import Messages', + 'not_found' => 'Not found', + 'found_help' => 'Whether any errors occurred during scanning.', + 'found_title' => 'Scan errors', + ], +]; diff --git a/plugins/rainlab/translate/lang/es/lang.php b/plugins/rainlab/translate/lang/es/lang.php new file mode 100644 index 00000000..f7a11c6c --- /dev/null +++ b/plugins/rainlab/translate/lang/es/lang.php @@ -0,0 +1,51 @@ + [ + 'name' => 'Multilenguaje', + 'description' => 'Permite sitios web multilingües', + 'manage_locales' => 'Manage locales', + 'manage_messages' => 'Manage messages' + ], + 'locale_picker' => [ + 'component_name' => 'Selección de idioma', + 'component_description' => 'Muestra una lista desplegable para seleccionar un idioma para el usuario', + ], + 'locale' => [ + 'title' => 'Administrar idiomas', + 'update_title' => 'Actualizar idioma', + 'create_title' => 'Crear idioma', + 'select_label' => 'Seleccionar idioma', + 'default_suffix' => 'Defecto', + 'unset_default' => '": locale" ya está predeterminado y no puede ser nulo por defecto.', + 'disabled_default' => '":locale" esta desactivado y no puede ser idioma por defecto', + 'name' => 'Nombre', + 'code' => 'Código', + 'is_default' => 'Por defecto', + 'is_default_help' => 'El idioma por defecto con el que se representa el contenido antes de la traducción.', + 'is_enabled' => 'Habilitado', + 'is_enabled_help' => 'Los idiomas desactivados no estarán disponibles en el front-end', + 'not_available_help' => 'No hay otros idiomas establecidos.', + 'hint_locales' => 'Crear nuevos idiomas aquí para traducir el contenido de front-end. El idioma por defecto representa el contenido antes de que haya sido traducido.', + ], + 'messages' => [ + 'title' => 'Traducir mensajes', + 'description' => 'Editar mensajes', + 'clear_cache_link' => 'Limpiar cache', + 'clear_cache_loading' => 'Borrado de la memoria caché de aplicaciones ...', + 'clear_cache_success' => 'Se ha borrado la memoria cache dela aplicación con éxito', + 'clear_cache_hint' => 'Es posible que tenga que hacer clic en Borrar caché para ver los cambios en el front-end.', + 'scan_messages_link' => 'Escanear mensajes', + 'scan_messages_loading' => 'Escaneando nuevos mensajes...', + 'scan_messages_success' => 'Escaneado de los archivos del tema completado!', + 'scan_messages_hint' => 'Al hacer click en Escanear comprobaremos los mensajes de los archivos de los temas activos para localizar nuevos mensajes a traducir.', + 'hint_translate' => 'Aquí usted puede traducir los mensajes utilizados en el front-end, los campos se guardará automáticamente.', + 'hide_translated' => 'Ocultar traducción', + ], +]; \ No newline at end of file diff --git a/plugins/rainlab/translate/lang/fa/lang.php b/plugins/rainlab/translate/lang/fa/lang.php new file mode 100644 index 00000000..ed608e0c --- /dev/null +++ b/plugins/rainlab/translate/lang/fa/lang.php @@ -0,0 +1,46 @@ + [ + 'name' => 'مترجم', + 'description' => 'فعال سازی وب سایت چند زبانه', + 'tab' => 'ترجمه', + 'manage_locales' => 'مدیریت مناطق', + 'manage_messages' => 'مدیریت پیغام ها' + ], + 'locale_picker' => [ + 'component_name' => 'انتخابگر منطقه', + 'component_description' => 'نمایش انتخابگر کشویی جهت انتخاب زبان.', + ], + 'locale' => [ + 'title' => 'مدیریت زبان ها', + 'update_title' => 'به روز رسانی زبان', + 'create_title' => 'ایجاد زبان', + 'select_label' => 'انتخاب زبان', + 'default_suffix' => 'پیشفرض', + 'unset_default' => '":locale" در حال حاظر پیشفرض می باشد و نمیتوانید آن را خارج کنید.', + 'disabled_default' => '":locale" غیر فعال می باشد و نمیتوانید آن را پیشفرض قرار دهید.', + 'name' => 'نام', + 'code' => 'کد یکتا', + 'is_default' => 'پیشفرض', + 'is_default_help' => 'زبان پیشفرضی که داده ها قبل از ترجمه به آن زبان وارد می شوند', + 'is_enabled' => 'فعال', + 'is_enabled_help' => 'زبان های غیر فعال در دسترس نخواهند بود.', + 'not_available_help' => 'زبان دیگری جهت نصب وجود ندارد.', + 'hint_locales' => 'زبان جدیدی را جهت ترجمه محتوی ایجاد نمایید.', + ], + 'messages' => [ + 'title' => 'ترجمه پیغام ها', + 'description' => 'به روز رسانی پیغام ها', + 'clear_cache_link' => 'پاکسازی حافظه کش', + 'clear_cache_loading' => 'در حال پاکسازی حافظه کش برنامه...', + 'clear_cache_success' => 'عملیات پاکسازی حافظه کش به اتمام رسید.', + 'clear_cache_hint' => 'جهت نمایش تغیرات در سایت نیاز است که شما بر رور پاکسازی حافظه کش کلیک نمایید.', + 'scan_messages_link' => 'جستجوی پیغام ها', + 'scan_messages_loading' => 'جستجوی پیغام های جدید...', + 'scan_messages_success' => 'جستجوی پیغام های جدید به اتمام رسید.', + 'scan_messages_hint' => 'جهت جستجو و نمایش پیغامهای جدیدی که باید ترجمه شوند بر روی جستجوی پیغام های جدید کلیک نمایید.', + 'hint_translate' => 'در این قسمت شما میتوانید پیغام ها را ترجمه نمایید. گزینه ها به صورت خودکار ذخیره میشوند.', + 'hide_translated' => 'مخفی سازی ترجمه شده ها', + ], +]; diff --git a/plugins/rainlab/translate/lang/fr/lang.php b/plugins/rainlab/translate/lang/fr/lang.php new file mode 100644 index 00000000..3d12f86e --- /dev/null +++ b/plugins/rainlab/translate/lang/fr/lang.php @@ -0,0 +1,59 @@ + [ + 'name' => 'Traductions', + 'description' => 'Permet de créer des sites Internet multilingues', + 'tab' => 'Traduction', + 'manage_locales' => 'Manage locales', + 'manage_messages' => 'Manage messages' + ], + 'locale_picker' => [ + 'component_name' => 'Sélection de la langue', + 'component_description' => 'Affiche un menu déroulant pour sélectionner la langue sur le site.', + ], + 'alternate_hreflang' => [ + 'component_name' => 'Éléments hrefLang alternatifs', + 'component_description' => "Injecte les alternatives linguistiques pour la page en tant qu'éléments hreflang" + ], + 'locale' => [ + 'title' => 'Gestion des langues', + 'update_title' => 'Mettre à jour la langue', + 'create_title' => 'Ajouter une langue', + 'select_label' => 'Sélectionner une langue', + 'default_suffix' => 'défaut', + 'unset_default' => '":locale" est déjà la langue par défaut et ne peut être désactivée', + 'delete_default' => '":locale" est la valeur par défaut et ne peut pas être supprimé.', + 'disabled_default' => '":locale" est désactivé et ne peut être utilisé comme paramètre par défaut.', + 'name' => 'Nom', + 'code' => 'Code', + 'is_default' => 'Défaut', + 'is_default_help' => 'La langue par défaut représente le contenu avant la traduction.', + 'is_enabled' => 'Activé', + 'is_enabled_help' => 'Les langues désactivées ne seront plus disponibles sur le site.', + 'not_available_help' => 'Aucune autre langue n\'est définie.', + 'hint_locales' => 'Vous pouvez ajouter de nouvelles langues et traduire les messages du site. La langue par défaut est celle utilisée pour les contenus avant toute traduction.', + 'reorder_title' => 'Réorganiser les langues', + 'sort_order' => 'Ordre de tri', + ], + 'messages' => [ + 'title' => 'Traduction des Messages', + 'description' => 'Mettre à jour Messages', + 'clear_cache_link' => 'Supprimer le cache', + 'clear_cache_loading' => 'Suppression du cache de l\'application...', + 'clear_cache_success' => 'Le cache de l\'application a été supprimé !', + 'clear_cache_hint' => 'Vous devez cliquer sur Supprimer le cache pour voir les modifications sur le site.', + 'scan_messages_link' => 'Rechercher des messages à traduire', + 'scan_messages_begin_scan' => 'Commencer la recherche', + 'scan_messages_loading' => 'Recherche de nouveaux messages...', + 'scan_messages_success' => 'Recherche dans les fichiers du thème effectuée !', + 'scan_messages_hint' => 'Cliquez sur Rechercher des messages à traduire pour parcourir les fichiers du thème actif à la recherche de messages à traduire.', + 'scan_messages_process' => 'Ce processus tentera d\'analyser le thème actif pour les messages qui peuvent être traduits.', + 'scan_messages_process_limitations' => 'Certains messages peuvent ne pas être capturés et n\'apparaîtront qu\'après leur première utilisation.', + 'scan_messages_purge_label' => 'Purger tous les messages en premier', + 'scan_messages_purge_help' => 'Si cette case est cochée, tous les messages seront supprimés avant d\'effectuer l\'analyse.', + 'scan_messages_purge_confirm' => 'Êtes-vous sûr de vouloir supprimer tous les messages? Cela ne peut pas être annulé!', + 'hint_translate' => 'Vous pouvez traduire les messages affichés sur le site, les champs s\'enregistrent automatiquement.', + 'hide_translated' => 'Masquer les traductions', + 'export_messages_link' => 'Exporter les messages', + 'import_messages_link' => 'Importer les messages', + ], +]; diff --git a/plugins/rainlab/translate/lang/gr/lang.php b/plugins/rainlab/translate/lang/gr/lang.php new file mode 100644 index 00000000..b49eac42 --- /dev/null +++ b/plugins/rainlab/translate/lang/gr/lang.php @@ -0,0 +1,64 @@ + [ + 'name' => 'Μετέφρασε', + 'description' => 'Ενεργοποίηση πολύγλωσσων ιστότοπων.', + 'tab' => 'Μετάφραση', + 'manage_locales' => 'Διαχείριση τοπικών ρυθμίσεων', + 'manage_messages' => 'Διαχείριση μηνυμάτων', + ], + 'locale_picker' => [ + 'component_name' => 'Επιλογή τοπικών ρυθμίσεων', + 'component_description' => 'Εμφάνιση αναπτυσσόμενου μενού για επιλογή γλώσσας front-end.', + ], + 'alternate_hreflang' => [ + 'component_name' => 'Εναλλακτικά στοιχεία hrefLang', + 'component_description' => 'Εισαγωγή εναλλακτικών γλωσσών για τη σελίδα ως στοιχεία hreflang' + ], + 'locale' => [ + 'title' => 'Διαχείριση γλώσσας', + 'update_title' => 'Ενημέρωση γλώσσας', + 'create_title' => 'Δημιουργία γλώσσας', + 'select_label' => 'Επιλογή γλώσσας', + 'default_suffix' => 'Προεπιλογή', + 'unset_default' => '":locale" είναι ήδη προεπιλογή και δεν μπορεί να οριστεί ως προεπιλογή.', + 'delete_default' => '":locale" είναι ήδη προεπιλογή και δεν μπορεί να διαγραφεί.', + 'disabled_default' => '":locale" είναι απενεργοποιημένο και δεν μπορεί να οριστεί ως προεπιλογή.', + 'name' => 'Όνομα', + 'code' => 'Κωδικός', + 'is_default' => 'Προεπιλογή', + 'is_default_help' => 'Η προεπιλεγμένη γλώσσα αντιπροσωπεύει το περιεχόμενο πριν από τη μετάφραση.', + 'is_enabled' => 'Ενεργοποίηση', + 'is_enabled_help' => 'Οι απενεργοποιημένες γλώσσες δεν θα είναι διαθέσιμες στο front-end.', + 'not_available_help' => 'Δεν έχουν ρυθμιστεί άλλες γλώσσες.', + 'hint_locales' => 'Δημιουργήστε νέες γλώσσες εδώ για τη μετάφραση περιεχομένου front-end. Η προεπιλεγμένη γλώσσα αντιπροσωπεύει το περιεχόμενο προτού μεταφραστεί.', + 'reorder_title' => 'Αναδιάταξη γλωσσών', + 'sort_order' => 'Σειρά ταξινόμησης', + ], + 'messages' => [ + 'title' => 'Μετάφραση μηνυμάτων', + 'description' => 'Ενημέρωση μηνυμάτων', + 'clear_cache_link' => 'Εκκαθάριση προσωρινής μνήμης', + 'clear_cache_loading' => 'Εκκαθάριση προσωρινής μνήμης εφαρμογής ...', + 'clear_cache_success' => 'Επιτυχής εκκαθάριση της προσωρινής μνήμης της εφαρμογής!', + 'clear_cache_hint' => 'Ίσως χρειαστεί να κάνετε κλικ στην επιλογή Clear cashe για να δείτε τις αλλαγές στο front-end.', + 'scan_messages_link' => 'Σάρωση για μηνύματα', + 'scan_messages_begin_scan' => 'Έναρξη σάρωσης', + 'scan_messages_loading' => 'Σάρωση για νέα μηνύματα ...', + 'scan_messages_success' => 'Επιτυχής σάρωση θεματικών πρότυπων αρχείων!', + 'scan_messages_hint' => 'Κάνοντας κλικ στην επιλογή Scan για messages θα ελεγθούν τα ενεργά αρχεία θεμάτων για τυχόν νέα μηνύματα που χρειάζονται μετάφραση.', + 'scan_messages_process' => 'Αυτή η διαδικασία θα προσπαθήσει να σαρώσει το ενεργό θέμα για μηνύματα που μπορούν να μεταφραστούν.', + 'scan_messages_process_limitations' => 'Ορισμένα μηνύματα ενδέχεται να μην καταγράφονται και θα εμφανίζονται μόνο μετά την πρώτη φορά που χρησιμοποιούνται.', + 'scan_messages_purge_label' => 'Εκκαθάριση πρώτα όλων των μηνυμάτων', + 'scan_messages_purge_help' => 'Επιλέγοντας εδώ θα διαγραφούν όλα τα μηνύματα, συμπεριλαμβανομένων και των μεταφράσεών τους, πριν από τη σάρωση.', + 'scan_messages_purge_confirm' => 'Είστε βέβαιοι ότι θέλετε να διαγράψετε όλα τα μηνύματα; Αυτό δεν μπορεί να αναιρεθεί!', + 'scan_messages_purge_deleted_label' => 'Εκκαθάριση απολεσθέντων μηνυμάτων μετά τη σάρωση', + 'scan_messages_purge_deleted_help' => 'Επιλέγοντας εδώ, αφού ολοκληρωθεί η σάρωση, τυχόν μηνύματα που δεν βρήκε ο σαρωτής, συμπεριλαμβανομένων και των μεταφράσεών τους, θα διαγραφούν. Αυτό δεν μπορεί να αναιρεθεί!', + 'hint_translate' => 'Εδώ μπορείτε να μεταφράσετε μηνύματα που χρησιμοποιούνται στο front-end, τα πεδία θα αποθηκεύονται αυτόματα.', + 'hide_translated' => 'Απόκρυψη μετάφρασης', + 'export_messages_link' => 'Εξαγωγή μηνυμάτων', + 'import_messages_link' => 'Εισαγωγή μηνυμάτων', + 'not_found' => 'Δεν βρέθηκε', + 'found_help' => 'Εάν τυχόν σφάλματα παρουσιάστηκαν κατά τη σάρωση.', + 'found_title' => 'Σάρωση σφαλμάτων', + ], +]; \ No newline at end of file diff --git a/plugins/rainlab/translate/lang/hu/lang.php b/plugins/rainlab/translate/lang/hu/lang.php new file mode 100644 index 00000000..364613f9 --- /dev/null +++ b/plugins/rainlab/translate/lang/hu/lang.php @@ -0,0 +1,66 @@ + [ + 'name' => 'Fordítás', + 'description' => 'Többnyelvű weboldal létrehozását teszi lehetővé.', + 'tab' => 'Fordítás', + 'manage_locales' => 'Nyelvek kezelése', + 'manage_messages' => 'Szövegek fordítása' + ], + 'locale_picker' => [ + 'component_name' => 'Nyelvi választó', + 'component_description' => 'Legördülő menüt jelenít meg a nyelv kiválasztásához.' + ], + 'alternate_hreflang' => [ + 'component_name' => 'Nyelvi oldalak', + 'component_description' => 'A hreflang HTML meta sorok generálása a keresők számára.' + ], + 'locale' => [ + 'title' => 'Nyelvek', + 'update_title' => 'Nyelv frissítése', + 'create_title' => 'Nyelv hozzáadása', + 'select_label' => 'Nyelv választása', + 'default_suffix' => 'alapértelmezett', + 'unset_default' => 'Már a(z) ":locale" nyelv az alapértelmezett, így nem használható alapértelmezettként.', + 'delete_default' => 'A(z) ":locale" nyelv az alapértelmezett, így nem törölhető.', + 'disabled_default' => 'A(z) ":locale" nyelv letiltott, így nem állítható be alapértelmezettként.', + 'name' => 'Név', + 'code' => 'Kód', + 'is_default' => 'Alapértelmezett', + 'is_default_help' => 'Az alapértelmezett nyelv a fordítás előtti tartalmat képviseli.', + 'is_enabled' => 'Engedélyezve', + 'is_enabled_help' => 'A letiltott nyelvek nem lesznek elérhetőek a látogatói oldalon.', + 'not_available_help' => 'Nincsenek más beállított nyelvek.', + 'hint_locales' => 'Itt hozhat létre új nyelveket a látogatói oldal tartalmának lefordításához. Az alapértelmezett nyelv képviseli a fordítás előtti tartalmat.', + 'reorder_title' => 'Rendezés', + 'sort_order' => 'Sorrend' + ], + 'messages' => [ + 'title' => 'Szövegek', + 'description' => 'Nyelvi változatok menedzselése.', + 'clear_cache_link' => 'Gyorsítótár kiürítése', + 'clear_cache_loading' => 'A weboldal gyorsítótár kiürítése...', + 'clear_cache_success' => 'Sikerült a weboldal gyorsítótár kiürítése!', + 'clear_cache_hint' => 'Kattintson a Gyorsítótár kiürítése gombra, hogy biztosan láthatóvá váljanak a beírt módosítások a látogatói oldalon is.', + 'scan_messages_link' => 'Szövegek keresése', + 'scan_messages_begin_scan' => 'Keresés indítása', + 'scan_messages_loading' => 'Új szövegek keresése...', + 'scan_messages_success' => 'Sikerült a szövegek beolvasása!', + 'scan_messages_hint' => 'A Szövegek keresése gombra kattintva pedig beolvashatja a lefordítandó szövegeket.', + 'scan_messages_process' => 'A folyamat megkisérli beolvasni az aktív témában lévő lefordítandó szövegeket.', + 'scan_messages_process_limitations' => 'Néhány szöveg nem biztos, hogy azonnal meg fog jelenni a listában.', + 'scan_messages_purge_label' => 'Szövegek törlése a művelet előtt', + 'scan_messages_purge_help' => 'Amennyiben bejelöli, úgy minden szöveg törlésre kerül a beolvasást megelőzően.', + 'scan_messages_purge_confirm' => 'Biztos, hogy töröljük az összes szöveget?', + 'scan_messages_purge_deleted_label' => 'Törölje a hiányzó üzeneteket a vizsgálat után', + 'scan_messages_purge_deleted_help' => 'Ha be van jelölve, akkor a keresés befejezése után minden olyan üzenet törlődik, amelyet a keresés nem talált. A művelet nem visszavonható!', + 'hint_translate' => 'Itt fordíthatja le a látogatók által elérhető oldalon megjelenő szövegeket. A beírt változtatások automatikusan mentésre kerülnek.', + 'hide_translated' => 'Lefordítottak elrejtése', + 'export_messages_link' => 'Szövegek exportálása', + 'import_messages_link' => 'Szövegek importálása', + 'not_found' => 'Nem található', + 'found_help' => 'Történt-e hiba a keresés során.', + 'found_title' => 'Hibák', + ] +]; diff --git a/plugins/rainlab/translate/lang/it/lang.php b/plugins/rainlab/translate/lang/it/lang.php new file mode 100644 index 00000000..d1213b49 --- /dev/null +++ b/plugins/rainlab/translate/lang/it/lang.php @@ -0,0 +1,53 @@ + [ + 'name' => 'Traduci', + 'description' => 'Abilita siti multi-lingua.', + 'tab' => 'Traduzioni', + 'manage_locales' => 'Gestisci lingue', + 'manage_messages' => 'Gestisci messaggi', + ], + 'locale_picker' => [ + 'component_name' => 'Selezione lingua', + 'component_description' => 'Mostra un elenco a discesa per selezionare una delle lingue del sito web.', + ], + 'locale' => [ + 'title' => 'Gestisci lingue', + 'update_title' => 'Aggiorna lingua', + 'create_title' => 'Crea lingua', + 'select_label' => 'Scegli lingua', + 'default_suffix' => 'default', + 'unset_default' => '":locale" è già impostato come default e non si può modificare.', + 'delete_default' => '":locale" è la lingua di default e non può essere cancellata.', + 'disabled_default' => '":locale" è disabilitata e non può essere impostata come default.', + 'name' => 'Nome', + 'code' => 'Codice', + 'is_default' => 'Default', + 'is_default_help' => 'La lingua di default rappresenta il contenuto prima che sia tradotto.', + 'is_enabled' => 'Abilitata', + 'is_enabled_help' => 'Le lingue disabilitate non saranno visualizzate nel sito web.', + 'not_available_help' => 'Non ci sono altre lingue da impostare.', + 'hint_locales' => 'Crea qui le nuove lingue per tradurre i contenuti del sito web. La lingua di default rappresenta il contenuto prima che sia tradotto', + 'reorder_title' => 'Riordina lingue', + 'sort_order' => 'Criterio di ordinamento', + ], + 'messages' => [ + 'title' => 'Traduci messaggi', + 'description' => 'Aggiorna messaggi', + 'clear_cache_link' => 'Pulisci cache', + 'clear_cache_loading' => 'Pulizia della cache dell\'applicazione in corso...', + 'clear_cache_success' => 'La cache è stata pulita con successo!', + 'clear_cache_hint' => 'Potrebbe essere necessario cliccare il bottobe Pulisci cache per vedere i cambiamenti nel sito web.', + 'scan_messages_link' => 'Scansiona per nuovi messaggi', + 'scan_messages_begin_scan' => 'Inizio scansione', + 'scan_messages_loading' => 'Scansione in corso per nuovi messaggi...', + 'scan_messages_success' => 'File del template scansionati con successo!', + 'scan_messages_hint' => 'Cliccando Scansiona per nuovi messaggi avvierai la ricerca di nuovi messaggi da tradurre nel template attivo.', + 'scan_messages_process' => 'Questo processo cercherà di scansionare il tema attivo per trovare messaggi che possono essere tradotti.', + 'scan_messages_process_limitations' => 'Qualche messaggio potrebbe non essere individuato e apparirà solo dopo la prima volta che verrà usato.', + 'scan_messages_purge_label' => 'Rimuovi prima tutti i messaggi', + 'scan_messages_purge_help' => 'Se selezionato, prima di eseguire la scansione verranno eliminati tutti i messaggi già presenti.', + 'scan_messages_purge_confirm' => 'Sei sicuro di voler cancellare tutti i messaggi? Questa operazione non può essere annullata!', + 'hint_translate' => 'Qui puoi tradurre i messaggi usati nel sito web, i campi verranno salvati automaticamente.', + 'hide_translated' => 'Nascondi i messaggi tradotti', + ], +]; diff --git a/plugins/rainlab/translate/lang/nl/lang.php b/plugins/rainlab/translate/lang/nl/lang.php new file mode 100644 index 00000000..26823d8f --- /dev/null +++ b/plugins/rainlab/translate/lang/nl/lang.php @@ -0,0 +1,57 @@ + [ + 'name' => 'Vertaal', + 'description' => 'Stelt meerdere talen in voor een website.', + 'tab' => 'Vertalingen', + 'manage_locales' => 'Beheer talen', + 'manage_messages' => 'Beheer vertaalde berichten' + ], + 'locale_picker' => [ + 'component_name' => 'Taalkeuze menu', + 'component_description' => 'Geeft een taal keuzemenu weer om de taal te wijzigen voor de website.', + ], + 'alternate_hreflang' => [ + 'component_name' => 'Alternatieve hrefLang elementen', + 'component_description' => 'Toont hreflang elementen voor de alt. talen' + ], + 'locale' => [ + 'title' => 'Beheer talen', + 'update_title' => 'Wijzig taal', + 'create_title' => 'Taal toevoegen', + 'select_label' => 'Selecteer taal', + 'default_suffix' => 'standaard', + 'unset_default' => '":locale" is de standaard taal en kan niet worden uitgeschakeld', + 'delete_default' => '":locale" is de standaard taal en kan niet worden verwijderd.', + 'disabled_default' => '":locale" is uitgeschakeld en kan niet als standaard taal worden ingesteld.', + 'name' => 'Naam', + 'code' => 'Code', + 'is_default' => 'Standaard', + 'is_default_help' => 'De standaard taal voor de inhoud.', + 'is_enabled' => 'Geactiveerd', + 'is_enabled_help' => 'Uitgeschakelde talen zijn niet beschikbaar op de website.', + 'not_available_help' => 'Er zijn geen andere talen beschikbaar.', + 'hint_locales' => 'Voeg hier nieuwe talen toe voor het vertalen van de website inhoud. De standaard taal geeft de inhoud weer voordat het is vertaald.', + 'reorder_title' => 'Talen rangschikken', + 'sort_order' => 'Volgorde', + ], + 'messages' => [ + 'title' => 'Vertaal berichten', + 'description' => 'Wijzig berichten', + 'clear_cache_link' => 'Leeg cache', + 'clear_cache_loading' => 'Applicatie cache legen...', + 'clear_cache_success' => 'De applicatie cache is succesvol geleegd.', + 'clear_cache_hint' => 'Het kan zijn dat het nodig is om op cache legen" te klikken om wijzigingen op de website te zien.', + 'scan_messages_link' => 'Zoek naar nieuwe berichten', + 'scan_messages_begin_scan' => 'Begin Scan', + 'scan_messages_loading' => 'Zoeken naar nieuwe berichten...', + 'scan_messages_success' => 'De thema bestanden zijn succesvol gescand!', + 'scan_messages_hint' => 'Klikken op Zoeken naar nieuwe berichten controleert de bestanden van het huidige thema op nieuwe berichten om te vertalen.', + 'scan_messages_process' => 'Dit proces zal proberen het huidige thema te scannen voor berichten om te vertalen', + 'scan_messages_process_limitations' => 'Sommige berichten zullen niet worden herkend en zullen pas verschijnen nadat ze voor de eerste keer zijn gebruikt', + 'scan_messages_purge_label' => 'Verwijder eerst alle berichten', + 'scan_messages_purge_help' => 'Als dit is aangevinkt zullen alle berichten worden verwijderd voordat de scan wordt uitgevoerd.', + 'scan_messages_purge_confirm' => 'Weet je zeker dat je alle berichten wilt verwijderen? Dit kan niet ongedaan gemaakt worden', + 'hint_translate' => 'Hier kan je berichten vertalen die worden gebruikt op de website. De velden worden automatisch opgeslagen.', + 'hide_translated' => 'Verberg vertaalde berichten', + ], +]; diff --git a/plugins/rainlab/translate/lang/pl/lang.php b/plugins/rainlab/translate/lang/pl/lang.php new file mode 100644 index 00000000..1b6e4afc --- /dev/null +++ b/plugins/rainlab/translate/lang/pl/lang.php @@ -0,0 +1,66 @@ + [ + 'name' => 'Tłumaczenia', + 'description' => 'Umożliwia tworzenie stron wielojęzycznych.', + 'tab' => 'Tłumaczenie', + 'manage_locales' => 'Zarządzaj językami', + 'manage_messages' => 'Zarządzaj treścią' + ], + 'locale_picker' => [ + 'component_name' => 'Lista języków', + 'component_description' => 'Wyświetla menu wyboru języków strony.', + ], + 'alternate_hreflang' => [ + 'component_name' => 'Alternatywne ustawienia hreflang', + 'component_description' => 'Ustawia alternatywne języki dla strony jako parametry hreflang' + ], + 'locale' => [ + 'title' => 'Zarządzaj językami', + 'update_title' => 'Edytuj język', + 'create_title' => 'Stwórz język', + 'select_label' => 'Wybierz język', + 'default_suffix' => 'domyślny', + 'unset_default' => 'Język ":locale" jest już domyślny i nie można go zmienić.', + 'delete_default' => 'Język ":locale" jest już domyślny i nie może zostać usunięty.', + 'disabled_default' => 'Język ":locale" jest wyłączony i nie można go ustawić jako domyślny.', + 'name' => 'Nazwa', + 'code' => 'Kod', + 'is_default' => 'Domyślny', + 'is_default_help' => 'Domyślny język to język treści strony przed tłumaczeniem.', + 'is_enabled' => 'Włączony', + 'is_enabled_help' => 'Wyłączone języki nie będą dostępne na stronie.', + 'not_available_help' => 'Nie skonfigurowano innych języków.', + 'hint_locales' => 'Stwórz nowe języki, na które chcesz tłumaczyć treść strony. Domyślny język to język treści strony przed tłumaczeniem. ', + 'reorder_title' => 'Zmień kolejność języków', + 'sort_order' => 'Sortowanie', + ], + 'messages' => [ + 'title' => 'Tłumacz Treść', + 'description' => 'Tłumaczenie treści strony', + 'clear_cache_link' => 'Wyczyść Cache', + 'clear_cache_loading' => 'Czyszczenie cache...', + 'clear_cache_success' => 'Pomyślnie wyczyszczono cache aplikacji!', + 'clear_cache_hint' => 'Jeśli nie widzisz zmian na stronie, kliknij przycisk Wyczyść cache.', + 'scan_messages_link' => 'Skanuj treść', + 'scan_messages_begin_scan' => 'Rozpocznij skanowanie', + 'scan_messages_loading' => 'Szukanie nowych pozycji...', + 'scan_messages_success' => 'Skanowanie plików motywu zakończyło się powodzeniem!', + 'scan_messages_hint' => 'Kliknięcie przycisku Skanuj treść rozpocznie skanowanie w poszukiwaniu nowych pozycji do przetłumaczenia.', + 'scan_messages_process' => 'Ten proces podejmie próbę przeskanowania aktywnego motywu w poszukiwaniu wiadomości, które można przetłumaczyć.', + 'scan_messages_process_limitations' => 'Niektóre wiadomości mogą nie zostać przechwycone i pojawią się dopiero po pierwszym użyciu.', + 'scan_messages_purge_label' => 'Najpierw usuń wszystkie wiadomości', + 'scan_messages_purge_help' => 'Zaznaczenie tej opcji spowoduje usunięcie wszystkich wiadomości, w tym ich tłumaczeń, przed wykonaniem skanowania.', + 'scan_messages_purge_confirm' => 'Czy jesteś pewny że chcesz usunąć wszystkie wiadomości? Po usunięciu nie będzie można ich przywrócić', + 'scan_messages_purge_deleted_label' => 'Usuń utracone wiadomości po zeskanowaniu', + 'scan_messages_purge_deleted_help' => 'Jeśli ta opcja jest zaznaczona, po zakończeniu skanowania wszystkie wiadomości, których skaner nie znalazł (w tym ich tłumaczenia) zostaną usunięte. Po zaznaczeniu tej opcji nie będzie możliwości przywrócenia zmian!', + 'hint_translate' => 'Możesz tu przetłumaczyć treść strony. Pola zapisują się automatycznie.', + 'hide_translated' => 'Ukryj przetłumaczone', + 'export_messages_link' => 'Wyeksportuj treść', + 'import_messages_link' => 'Zaimportuj treść', + 'not_found' => 'Nie znaleziono', + 'found_help' => 'Wystąpiły błędy podczas skanowania', + 'found_title' => 'Błąd skanowania', + ], +]; diff --git a/plugins/rainlab/translate/lang/pt-br/lang.php b/plugins/rainlab/translate/lang/pt-br/lang.php new file mode 100644 index 00000000..e1995f97 --- /dev/null +++ b/plugins/rainlab/translate/lang/pt-br/lang.php @@ -0,0 +1,53 @@ + [ + 'name' => 'Traduções', + 'description' => 'Permite sites com multi-idiomas.', + 'tab' => 'Tradução', + 'manage_locales' => 'Gerenciar locais', + 'manage_messages' => 'Gerenciar mensagens' + ], + 'locale_picker' => [ + 'component_name' => 'Seleção de idiomas', + 'component_description' => 'Exibe um campo de seleção de idiomas.', + ], + 'locale' => [ + 'title' => 'Gerenciar idiomas', + 'update_title' => 'Atualizar idioma', + 'create_title' => 'Criar idioma', + 'select_label' => 'Selecionar idioma', + 'default_suffix' => 'padrão', + 'unset_default' => '":locale" é o idioma padrão e não pode ser desativado.', + 'delete_default' => '":locale" é o padrão e não pode ser excluído.', + 'disabled_default' => '":locale" está desativado e não pode ser definido como padrão.', + 'name' => 'Nome', + 'code' => 'Código', + 'is_default' => 'Padrão', + 'is_default_help' => 'O idioma padrão apresenta o conteúdo antes das traduções.', + 'is_enabled' => 'Ativo', + 'is_enabled_help' => 'Idiomas desativados não estarão disponíveis na página.', + 'not_available_help' => 'Não há outros idiomas configurados.', + 'hint_locales' => 'Crie novos idiomas para traduzir o conteúdo da página. O idioma padrão apresenta o conteúdo antes das traduções.', + ], + 'messages' => [ + 'title' => 'Traduzir mensagens', + 'description' => 'Atualizar mensagens', + 'clear_cache_link' => 'Limpar cache', + 'clear_cache_loading' => 'Limpando o cache da aplicação...', + 'clear_cache_success' => 'Cache da aplicação limpo com sucesso!', + 'clear_cache_hint' => 'Talvez você terá que clicar em Limpar cache para visualizar as modificações na página.', + 'scan_messages_link' => 'Buscar por mensagens', + 'scan_messages_begin_scan' => 'Inciar Busca', + 'scan_messages_loading' => 'Buscando por novas mensagens...', + 'scan_messages_success' => 'Busca por novas mensagens nos arquivos concluída com sucesso!', + 'scan_messages_hint' => 'Clicando em Buscar por mensagens o sistema buscará por qualquer mensagem da aplicação que possa ser traduzida.', + 'scan_messages_process' => 'Este processo tentará scanear o tema ativo para mensagens que podem ser traduzidas.', + 'scan_messages_process_limitations' => 'Algumas mensagens podem não ser capturadas e só aparecerão depois da primeira vez que forem usadas.', + 'scan_messages_purge_label' => 'Limpar primeiro todas as mensagens', + 'scan_messages_purge_help' => 'Se selecionado, isso excluirá todas as mensagens antes de executar a verificação.', + 'scan_messages_purge_confirm' => 'Tem certeza de que deseja excluir todas as mensagens? Isto não pode ser desfeito!', + 'hint_translate' => 'Aqui você pode raduzir as mensagens utilizadas na página, os campos são salvos automaticamente.', + 'hide_translated' => 'Ocultar traduzidas', + ], +]; diff --git a/plugins/rainlab/translate/lang/ru/lang.php b/plugins/rainlab/translate/lang/ru/lang.php new file mode 100644 index 00000000..a3b9177c --- /dev/null +++ b/plugins/rainlab/translate/lang/ru/lang.php @@ -0,0 +1,61 @@ + [ + 'name' => 'Translate', + 'description' => 'Настройки мультиязычности сайта.', + 'tab' => 'Перевод', + 'manage_locales' => 'Управление языками', + 'manage_messages' => 'Управление сообщениями' + ], + 'locale_picker' => [ + 'component_name' => 'Выбор языка', + 'component_description' => 'Просмотр списка языков интерфейса.', + ], + 'alternate_hreflang' => [ + 'component_name' => 'Альтернативные элементы hrefLang', + 'component_description' => 'Внедряет языковые альтернативы для страницы в качестве элементов hreflang' + ], + 'locale' => [ + 'title' => 'Управление языками', + 'update_title' => 'Обновить язык', + 'create_title' => 'Создать язык', + 'select_label' => 'Выбрать язык', + 'default_suffix' => 'По умолчанию', + 'unset_default' => '":locale" уже установлен как язык по умолчанию.', + 'delete_default' => '":locale" используется по умолчанию и не может быть удален.', + 'disabled_default' => '":locale" отключен и не может быть использован как язык по умолчанию.', + 'name' => 'Название', + 'code' => 'Код', + 'is_default' => 'По умолчанию', + 'is_default_help' => 'Использовать этот язык, как язык по умолчанию.', + 'is_enabled' => 'Включено', + 'is_enabled_help' => 'Сделать язык доступным в интерфейсе сайта.', + 'not_available_help' => 'Нет настроек других языков.', + 'hint_locales' => 'Создание новых переводов содержимого интерфейса сайта.', + 'reorder_title' => 'Изменить порядок языков', + 'sort_order' => 'Порядок сортировки', + ], + 'messages' => [ + 'title' => 'Перевод сообщений', + 'description' => 'Перевод статических сообщений в шаблоне', + 'clear_cache_link' => 'Очистить кэш', + 'clear_cache_loading' => 'Очистка кэша приложения...', + 'clear_cache_success' => 'Очистка кэша завершена успешно!', + 'clear_cache_hint' => 'Используйте кнопку Очистить кэш, чтобы увидеть изменения в интерфейсе сайта.', + 'scan_messages_link' => 'Сканирование сообщений', + 'scan_messages_begin_scan' => 'Начать сканирование', + 'scan_messages_loading' => 'Сканирование наличия новых сообщений...', + 'scan_messages_success' => 'Сканирование файлов шаблона темы успешно завершено!', + 'scan_messages_hint' => 'Используйте кнопку Сканирование сообщений для поиска новых ключей перевода активной темы интерфейса сайта.', + 'scan_messages_process' => 'Этот процесс попытается отсканировать активную тему для сообщений, которые можно перевести.', + 'scan_messages_process_limitations' => 'Некоторые сообщения могут не быть отсканированы и появлятся только после первого использования.', + 'scan_messages_purge_label' => 'Сначала очистить все сообщения', + 'scan_messages_purge_help' => 'Если этот флажок установлен, это приведет к удалению всех сообщений перед выполнением сканирования.', + 'scan_messages_purge_confirm' => 'Вы действительно хотите удалить все сообщения? Операция не может быть отменена!', + 'hint_translate' => 'Здесь вы можете переводить сообщения, которые используются в интерфейсе сайта.', + 'hide_translated' => 'Скрыть перевод', + 'export_messages_link' => 'Экспорт сообщений', + 'import_messages_link' => 'Импорт сообщений', + ], +]; diff --git a/plugins/rainlab/translate/lang/sk/lang.php b/plugins/rainlab/translate/lang/sk/lang.php new file mode 100644 index 00000000..82b48278 --- /dev/null +++ b/plugins/rainlab/translate/lang/sk/lang.php @@ -0,0 +1,59 @@ + [ + 'name' => 'Preklady', + 'description' => 'Umožňuje viacjazyčné webové stránky.', + 'tab' => 'Preklad', + 'manage_locales' => 'Spravovať jazyky', + 'manage_messages' => 'Spravovať správy', + ], + 'locale_picker' => [ + 'component_name' => 'Výber jazyka', + 'component_description' => 'Zobrazí rozbaľovaciu ponuku na výber jazyka front-endu.', + ], + 'alternate_hreflang' => [ + 'component_name' => 'Alternatívne prvky hrefLang', + 'component_description' => 'Vloží jazykové alternatívy pre stránku ako prvky hreflang' + ], + 'locale' => [ + 'title' => 'Spravovať jazyky', + 'update_title' => 'Aktualizovať jazyk', + 'create_title' => 'Vytvoriť jazyk', + 'select_label' => 'Zvoliť jazyk', + 'default_suffix' => 'predvolený', + 'unset_default' => '":locale" je už predvolený a nemožno ho nastaviť ako predvolený.', + 'delete_default' => '":locale" je predvolený a nemôže byť zmazaný.', + 'disabled_default' => '":locale" je neaktívny a nemôýe byť nastavený ako predvolený.', + 'name' => 'Meno', + 'code' => 'Kód', + 'is_default' => 'Predvolený', + 'is_default_help' => 'Predvolený jazyk predstavuje obsah pred prekladom.', + 'is_enabled' => 'Aktívny', + 'is_enabled_help' => 'Neaktívne jazyky nebudú dostupné na front-ende.', + 'not_available_help' => 'Nie sú nastavené žiadne ďalšie jazyky.', + 'hint_locales' => 'Vytvárajte tu nové jazyky pre preklad obsahu front-endu. Predvolený jazyk predstavuje obsah pred tým, než bol preložený.', + 'reorder_title' => 'Zmeniť poradie jazykov', + 'sort_order' => 'Smer zoradenia', + ], + 'messages' => [ + 'title' => 'Preložiť správy', + 'description' => 'Aktualizovať správy', + 'clear_cache_link' => 'Vymazať vyrovnávaciu pamäť', + 'clear_cache_loading' => 'Čistenie vyrovnávacej pamäte aplikácie...', + 'clear_cache_success' => 'Vyrovnávacia pamäť aplikácie vyčistená!', + 'clear_cache_hint' => 'Možno budete musieť kliknúť Vymazať vyrovnávaciu pamäť aby sa zmeny prejavili na front-ende.', + 'scan_messages_link' => 'Vyhľadávanie správ', + 'scan_messages_begin_scan' => 'Začať vyhľadávanie', + 'scan_messages_loading' => 'Vyhľadávanie nových správ...', + 'scan_messages_success' => 'Vyhľadávanie nových správ úspešne ukončené!', + 'scan_messages_hint' => 'Kliknutie na Vyhľadávanie správ skontroluje súbory aktívnej témy a nájde nové správy na preklad.', + 'scan_messages_process' => 'Tento proces vyhľadí v aktívnej téme správy, ktoré môžu byť preložené.', + 'scan_messages_process_limitations' => 'Niektoré správy nemusia byť zachytené a objavia sa po ich prvom použití.', + 'scan_messages_purge_label' => 'Najprv vyčistiť všetky správy', + 'scan_messages_purge_help' => 'Ak zaškrtnuté zmaže všetky správy pred vykonaním vyhľadávania.', + 'scan_messages_purge_confirm' => 'Naozaj chcete odstrániť všetky správy? Toto sa nedá vrátiť späť!', + 'hint_translate' => 'Tu môžete preložiť správy používané na front-ende, polia sa ukladajú automaticky.', + 'hide_translated' => 'Skryť preložené', + 'export_messages_link' => 'Exportovať správy', + 'import_messages_link' => 'Importovať správy', + ], +]; diff --git a/plugins/rainlab/translate/lang/sl/lang.php b/plugins/rainlab/translate/lang/sl/lang.php new file mode 100644 index 00000000..db99d8e5 --- /dev/null +++ b/plugins/rainlab/translate/lang/sl/lang.php @@ -0,0 +1,59 @@ + [ + 'name' => 'Večjezičnost', + 'description' => 'Podpora za večjezične spletne strani.', + 'tab' => 'Večjezičnost', + 'manage_locales' => 'Upravljanje jezikov', + 'manage_messages' => 'Upravljanje besedil', + ], + 'locale_picker' => [ + 'component_name' => 'Izbirnik jezikov', + 'component_description' => 'Prikaže spustni seznam jezikov na spletni strani.', + ], + 'alternate_hreflang' => [ + 'component_name' => 'Jezikovne alternative', + 'component_description' => 'Vstavi jezikovne alternative za stran kot elemente "hreflang".', + ], + 'locale' => [ + 'title' => 'Upravljanje jezikov', + 'update_title' => 'Posodobitev jezika', + 'create_title' => 'Ustvari nov jezik', + 'select_label' => 'Izberi jezik', + 'default_suffix' => 'privzeto', + 'unset_default' => '":locale" je že privzeti jezik in ga ni možno nastaviti za ne-privzetega.', + 'delete_default' => '":locale" je privzeti jezik in se ga ne da izbrisati.', + 'disabled_default' => '":locale" je onemogočen jezik in ga ni možno nastaviti za privzetega.', + 'name' => 'Ime', + 'code' => 'Koda', + 'is_default' => 'Privzeto', + 'is_default_help' => 'Privzeti jezik predstavlja vsebino pred prevodom.', + 'is_enabled' => 'Omogočeno', + 'is_enabled_help' => 'Onemogočeni jeziki na spletni strani ne bodo na voljo.', + 'not_available_help' => 'Ni nastavljenih drugih jezikov.', + 'hint_locales' => 'Tukaj lahko ustvarite nove jezike za prevajanje vsebine. Privzeti jezik predstavlja vsebino, preden je prevedena.', + 'reorder_title' => 'Spremeni vrstni red jezikov', + 'sort_order' => 'Vrstni red', + ], + 'messages' => [ + 'title' => 'Prevajanje besedil', + 'description' => 'Urejanje prevodov besedil.', + 'clear_cache_link' => 'Počisti predpomnilnik', + 'clear_cache_loading' => 'Praznenje predpomnilnika aplikacije...', + 'clear_cache_success' => 'Predpomnilnik aplikacije je uspešno izpraznjen!', + 'clear_cache_hint' => 'Morda boste morali klikniti Počisti predpomnilnik za ogled sprememb na spletni strani.', + 'scan_messages_link' => 'Skeniraj besedila', + 'scan_messages_begin_scan' => 'Začni skeniranje', + 'scan_messages_loading' => 'Skeniram za nova besedila...', + 'scan_messages_success' => 'Datoteke aktivne teme so bile uspešno skenirane!', + 'scan_messages_hint' => 'Klik na Skeniraj besedila bo preveril datoteke aktivne teme, če vsebujejo morebitna besedila za prevode.', + 'scan_messages_process' => 'Ta postopek bo poskusil skenirati aktivno temo za besedila, ki jih je mogoče prevesti.', + 'scan_messages_process_limitations' => 'Nekatera besedila morda ne bodo prikazana in se bodo prikazala šele po prvi uporabi.', + 'scan_messages_purge_label' => 'Najprej izbriši vsa besedila', + 'scan_messages_purge_help' => 'Če je označeno, bodo pred skeniranjem izbrisana vsa besedila, vključno z njihovimi prevodi!', + 'scan_messages_purge_confirm' => 'Ali ste prepričani, da želite izbrisati vsa besedila? Tega ukaza ni mogoče razveljaviti!', + 'hint_translate' => 'Tukaj lahko prevedete besedila, uporabljena na spletni strani. Vrednosti v poljih se shranijo samodejno.', + 'hide_translated' => 'Skrij prevedena besedila', + 'export_messages_link' => 'Izvozi besedila', + 'import_messages_link' => 'Uvozi besedila', + ], +]; diff --git a/plugins/rainlab/translate/lang/tr/lang.php b/plugins/rainlab/translate/lang/tr/lang.php new file mode 100644 index 00000000..5a201475 --- /dev/null +++ b/plugins/rainlab/translate/lang/tr/lang.php @@ -0,0 +1,64 @@ + [ + 'name' => 'Çeviri', + 'description' => 'Çoklu dil destekli websiteleri oluşturmanızı sağlar.', + 'tab' => 'Çeviri', + 'manage_locales' => 'Dilleri yönet', + 'manage_messages' => 'Çevirileri yönet', + ], + 'locale_picker' => [ + 'component_name' => 'Çoklu Dil Seçimi', + 'component_description' => 'Sitenizin dilini değiştirebileceğiniz diller listesini gösterir.', + ], + 'alternate_hreflang' => [ + 'component_name' => 'Alternatif hrefLang elemanları', + 'component_description' => 'Sayfa için dil alternatiflerini hreflang elemanları olarak ekler.', + ], + 'locale' => [ + 'title' => 'Dilleri yönet', + 'update_title' => 'Dili güncelle', + 'create_title' => 'Dil ekle', + 'select_label' => 'Dil seç', + 'default_suffix' => 'ön tanımlı', + 'unset_default' => '":locale" zaten ön tanımlı olarak seçili.', + 'delete_default' => '":locale" ön tanımlı olarak seçilmiş olduğu için silinemez.', + 'disabled_default' => '":locale" pasifleştirilmiş olduğu için ön tanımlı yapılamaz.', + 'name' => 'Dil İsmi', + 'code' => 'Dil Kodu', + 'is_default' => 'Ön tanımlı', + 'is_default_help' => 'Ön tanımlı seçilen dil, sitenin orjinal içeriğini belirtmektedir.', + 'is_enabled' => 'Aktif', + 'is_enabled_help' => 'Pasifleştirilen diller site ön yüzünde görüntülenmez.', + 'not_available_help' => 'Başka dil ayarı yok.', + 'hint_locales' => 'Ön yüz çevirilerini yapmak için buradan dil ekleyebilirsiniz. Ön tanımlı seçilen dil, sitenin orjinal içeriğini belirtmektedir.', + 'reorder_title' => 'Dilleri sırala', + 'sort_order' => 'Sıralama', + ], + 'messages' => [ + 'title' => 'Metinleri çevir', + 'description' => 'Metinler ve çevirileri', + 'clear_cache_link' => 'Önbelleği temizle', + 'clear_cache_loading' => 'Önbellek temizleniyor..', + 'clear_cache_success' => 'Önbellek temizlendi!', + 'clear_cache_hint' => 'Yaptığınız çeviriler site ön yüzünde görünmüyorsa Önbelleği temizle butonuna tıklayabilirsiniz.', + 'scan_messages_link' => 'Yeni metinleri tara', + 'scan_messages_begin_scan' => 'Taramaya başla', + 'scan_messages_loading' => 'Yeni metinler taranıyor...', + 'scan_messages_success' => 'Tema dosyaları tarandı!', + 'scan_messages_hint' => 'Yeni metinleri tara butonuna tıklayarak tema içerisine yeni eklediğiniz metinleri de çevirebilirsiniz.', + 'scan_messages_process' => 'Bu işlem tema dosyaları içerisindeki çevrilecek metinleri tarar.', + 'scan_messages_process_limitations' => 'Bazı metinler yakalanamayabilir veya ilk kez kullanımdan sonra görüntülenebilir.', + 'scan_messages_purge_label' => 'Önce tüm eski çevirileri sil', + 'scan_messages_purge_help' => 'Eğer seçerseniz şimdiye kadar yaptığınız tüm çeviriler silinecek, tekrar çevirmeniz gerekecektir.', + 'scan_messages_purge_confirm' => 'Tüm çevirileri silmek istediğinize emin misiniz? Bu işlem geri alınamaz!', + 'scan_messages_purge_deleted_label' => 'Tarama tamamlandıktan sonra eksik çevirileri temizle', + 'scan_messages_purge_deleted_help' => 'İşaretlerseniz, tarama tamamlandıktan sonra tarayıcının bulamadığı tüm metinler (diğer dil çevirileri de dahil olmak üzere) silinecektir. Bu işlem geri alınamaz!', + 'hint_translate' => 'Bu kısımda site ön yüzünde görüntülenecek çeviri metinlerini bulabilirsiniz, çeviri yaptıktan sonra bir işlem yapmanıza gerek yoktur, hepsi otomatik kaydedilecek.', + 'hide_translated' => 'Çevrilen metinleri gizle', + 'export_messages_link' => 'Metinleri Dışa Aktar', + 'import_messages_link' => 'Metinleri İçe Aktar', + 'not_found' => 'Bulunamadı', + 'found_help' => 'Tarama sırasında herhangi bir hata oluşup oluşmadığı.', + 'found_title' => 'Tarama hataları', + ], +]; diff --git a/plugins/rainlab/translate/lang/zh-cn/lang.php b/plugins/rainlab/translate/lang/zh-cn/lang.php new file mode 100644 index 00000000..9ed1aa06 --- /dev/null +++ b/plugins/rainlab/translate/lang/zh-cn/lang.php @@ -0,0 +1,64 @@ + [ + 'name' => '翻译', + 'description' => '启用多语言网站。', + 'tab' => '翻译', + 'manage_locales' => '管理语言环境', + 'manage_messages' => '管理消息', + ], + 'locale_picker' => [ + 'component_name' => '语言环境选择器', + 'component_description' => '显示用于选择前端语言的下拉列表。', + ], + 'alternate_hreflang' => [ + 'component_name' => '备用 hrefLang 元素', + 'component_description' => '将页面的语言替代项作为 hreflang 元素注入' + ], + 'locale' => [ + 'title' => '管理语言', + 'update_title' => '更新语言', + 'create_title' => '创建语言', + 'select_label' => '选择语言', + 'default_suffix' => '默认', + 'unset_default' => '":locale" 已经是默认值,不能取消设置为默认值。', + 'delete_default' => '":locale" 是默认值,不能删除。', + 'disabled_default' => '":locale" 已禁用且不能设置为默认值。', + 'name' => '名称', + 'code' => '代码', + 'is_default' => '默认', + 'is_default_help' => '默认语言代表翻译前的内容。', + 'is_enabled' => '启用', + 'is_enabled_help' => '禁用的语言在前端将不可用。', + 'not_available_help' => '没有设置其他语言。', + 'hint_locales' => '在此处创建用于翻译前端内容的新语言。默认语言代表翻译之前的内容。', + 'reorder_title' => '重新排序语言', + 'sort_order' => '排序顺序', + ], + 'messages' => [ + 'title' => '翻译消息', + 'description' => '更新消息', + 'clear_cache_link' => '清除缓存', + 'clear_cache_loading' => '清除应用程序缓存...', + 'clear_cache_success' => '清除应用缓存成功!', + 'clear_cache_hint' => '您可能需要点击清除缓存才能查看前端的更改。', + 'scan_messages_link' => '扫描消息', + 'scan_messages_begin_scan' => '开始扫描', + 'scan_messages_loading' => '正在扫描新消息...', + 'scan_messages_success' => '已成功扫描主题模板文件!', + 'scan_messages_hint' => '点击扫描消息将检查活动主题文件中是否有任何要翻译的新消息。', + 'scan_messages_process' => '此进程将尝试扫描活动主题以查找可翻译的消息。', + 'scan_messages_process_limitations' => '有些消息可能无法捕获,只有在第一次使用后才会出现。', + 'scan_messages_purge_label' => '先清除所有消息', + 'scan_messages_purge_help' => '如果选中,这将在执行扫描之前删除所有消息,包括它们的翻译。', + 'scan_messages_purge_confirm' => '您确定要删除所有消息吗?这不能被撤消!', + 'scan_messages_purge_deleted_label' => '扫描后清除丢失的消息', + 'scan_messages_purge_deleted_help' => '如果选中,在扫描完成后,扫描器未找到的任何消息,包括它们的翻译,都将被删除。这不能被撤消!', + 'hint_translate' => '这里可以翻译前端使用的消息,字段会自动保存。', + 'hide_translated' => '隐藏翻译', + 'export_messages_link' => '导出消息', + 'import_messages_link' => '导入消息', + 'not_found' => '未找到', + 'found_help' => '扫描过程中是否发生任何错误。', + 'found_title' => '扫描错误', + ], +]; diff --git a/plugins/rainlab/translate/models/Attribute.php b/plugins/rainlab/translate/models/Attribute.php new file mode 100644 index 00000000..a6e7a412 --- /dev/null +++ b/plugins/rainlab/translate/models/Attribute.php @@ -0,0 +1,18 @@ + [] + ]; +} diff --git a/plugins/rainlab/translate/models/Locale.php b/plugins/rainlab/translate/models/Locale.php new file mode 100644 index 00000000..938643c2 --- /dev/null +++ b/plugins/rainlab/translate/models/Locale.php @@ -0,0 +1,218 @@ + 'required', + 'name' => 'required', + ]; + + public $timestamps = false; + + /** + * @var array Object cache of self, by code. + */ + protected static $cacheByCode = []; + + /** + * @var array A cache of enabled locales. + */ + protected static $cacheListEnabled; + + /** + * @var array A cache of available locales. + */ + protected static $cacheListAvailable; + + /** + * @var self Default locale cache. + */ + protected static $defaultLocale; + + public function afterCreate() + { + if ($this->is_default) { + $this->makeDefault(); + } + } + + public function beforeDelete() + { + if ($this->is_default) { + throw new ApplicationException(Lang::get('rainlab.translate::lang.locale.delete_default', ['locale'=>$this->name])); + } + } + + public function beforeUpdate() + { + if ($this->isDirty('is_default')) { + $this->makeDefault(); + + if (!$this->is_default) { + throw new ValidationException(['is_default' => Lang::get('rainlab.translate::lang.locale.unset_default', ['locale'=>$this->name])]); + } + } + } + + /** + * Makes this model the default + * @return void + */ + public function makeDefault() + { + if (!$this->is_enabled) { + throw new ValidationException(['is_enabled' => Lang::get('rainlab.translate::lang.locale.disabled_default', ['locale'=>$this->name])]); + } + + $this->newQuery()->where('id', $this->id)->update(['is_default' => true]); + $this->newQuery()->where('id', '<>', $this->id)->update(['is_default' => false]); + } + + /** + * Returns the default locale defined. + * @return self + */ + public static function getDefault() + { + if (self::$defaultLocale !== null) { + return self::$defaultLocale; + } + + if ($forceDefault = Config::get('rainlab.translate::forceDefaultLocale')) { + $locale = new self; + $locale->name = $locale->code = $forceDefault; + $locale->is_default = $locale->is_enabled = true; + return self::$defaultLocale = $locale; + } + + return self::$defaultLocale = self::where('is_default', true) + ->remember(1440, 'rainlab.translate.defaultLocale') + ->first() + ; + } + + /** + * Locate a locale table by its code, cached. + * @param string $code + * @return Model + */ + public static function findByCode($code = null) + { + if (!$code) { + return null; + } + + if (isset(self::$cacheByCode[$code])) { + return self::$cacheByCode[$code]; + } + + return self::$cacheByCode[$code] = self::whereCode($code)->first(); + } + + /** + * Scope for checking if model is enabled + * @param Builder $query + * @return Builder + */ + public function scopeIsEnabled($query) + { + return $query->where('is_enabled', true); + } + + /** + * Scope for ordering the locales + * @param Builder $query + * @return Builder + */ + public function scopeOrder($query) + { + return $query + ->orderBy('sort_order', 'asc') + ; + } + + /** + * Returns true if there are at least 2 locales available. + * @return boolean + */ + public static function isAvailable() + { + return count(self::listAvailable()) > 1; + } + + /** + * Lists available locales, used on the back-end. + * @return array + */ + public static function listAvailable() + { + if (self::$cacheListAvailable) { + return self::$cacheListAvailable; + } + + return self::$cacheListAvailable = self::order()->pluck('name', 'code')->all(); + } + + /** + * Lists the enabled locales, used on the front-end. + * @return array + */ + public static function listEnabled() + { + if (self::$cacheListEnabled) { + return self::$cacheListEnabled; + } + + $expiresAt = now()->addMinutes(1440); + $isEnabled = Cache::remember('rainlab.translate.locales', $expiresAt, function() { + return self::isEnabled()->order()->pluck('name', 'code')->all(); + }); + + return self::$cacheListEnabled = $isEnabled; + } + + /** + * Returns true if the supplied locale is valid. + * @return boolean + */ + public static function isValid($locale) + { + $languages = array_keys(Locale::listEnabled()); + + return in_array($locale, $languages); + } + + /** + * Clears all cache keys used by this model + * @return void + */ + public static function clearCache() + { + Cache::forget('rainlab.translate.locales'); + Cache::forget('rainlab.translate.defaultLocale'); + self::$cacheListEnabled = null; + self::$cacheListAvailable = null; + self::$cacheByCode = []; + } +} diff --git a/plugins/rainlab/translate/models/MLFile.php b/plugins/rainlab/translate/models/MLFile.php new file mode 100644 index 00000000..d3f04c3f --- /dev/null +++ b/plugins/rainlab/translate/models/MLFile.php @@ -0,0 +1,24 @@ +getFormFields() as $id => $field) { + if (!empty($field['translatable'])) { + $this->translatable[] = $id; + } + } + } +} diff --git a/plugins/rainlab/translate/models/Message.php b/plugins/rainlab/translate/models/Message.php new file mode 100644 index 00000000..e468585e --- /dev/null +++ b/plugins/rainlab/translate/models/Message.php @@ -0,0 +1,311 @@ +forLocale(Lang::getLocale()); + } + + /** + * Gets a message for a given locale, or the default. + * @param string $locale + * @return string + */ + public function forLocale($locale = null, $default = null) + { + if ($locale === null) { + $locale = self::DEFAULT_LOCALE; + } + + if (!array_key_exists($locale, $this->message_data)) { + // search parent locale (e.g. en-US -> en) before returning default + list($locale) = explode('-', $locale); + } + + if (array_key_exists($locale, $this->message_data)) { + return $this->message_data[$locale]; + } + + return $default; + } + + /** + * Writes a translated message to a locale. + * @param string $locale + * @param string $message + * @return void + */ + public function toLocale($locale = null, $message = null) + { + if ($locale === null) { + return; + } + + $data = $this->message_data; + $data[$locale] = $message; + + if (!$message) { + unset($data[$locale]); + } + + $this->message_data = $data; + $this->save(); + } + + /** + * Creates or finds an untranslated message string. + * @param string $messageId + * @param string $locale + * @return string + */ + public static function get($messageId, $locale = null) + { + $locale = $locale ?: self::$locale; + if (!$locale) { + return $messageId; + } + + $messageCode = static::makeMessageCode($messageId); + + /* + * Found in cache + */ + if (array_key_exists($locale . $messageCode, self::$cache)) { + return self::$cache[$locale . $messageCode]; + } + + /* + * Uncached item + */ + $item = static::firstOrNew([ + 'code' => $messageCode + ]); + + /* + * Create a default entry + */ + if (!$item->exists) { + $data = [static::DEFAULT_LOCALE => $messageId]; + $item->message_data = $item->message_data ?: $data; + $item->save(); + } + + /* + * Schedule new cache and go + */ + $msg = $item->forLocale($locale, $messageId); + self::$cache[$locale . $messageCode] = $msg; + self::$hasNew = true; + + return $msg; + } + + /** + * Import an array of messages. Only known messages are imported. + * @param array $messages + * @param string $locale + * @return void + */ + public static function importMessages($messages, $locale = null) + { + self::importMessageCodes(array_combine($messages, $messages), $locale); + } + + /** + * Import an array of messages. Only known messages are imported. + * @param array $messages + * @param string $locale + * @return void + */ + public static function importMessageCodes($messages, $locale = null) + { + if ($locale === null) { + $locale = static::DEFAULT_LOCALE; + } + + $existingIds = []; + + foreach ($messages as $code => $message) { + // Ignore empties + if (!strlen(trim($message))) { + continue; + } + + $code = static::makeMessageCode($code); + + $item = static::firstOrNew([ + 'code' => $code + ]); + + // Do not import non-default messages that do not exist + if (!$item->exists && $locale != static::DEFAULT_LOCALE) { + continue; + } + + $messageData = $item->exists || $item->message_data ? $item->message_data : []; + + // Do not overwrite existing translations. + if (isset($messageData[$locale])) { + $existingIds[] = $item->id; + continue; + } + + $messageData[$locale] = $message; + + $item->message_data = $messageData; + $item->found = true; + + $item->save(); + } + + // Set all messages found by the scanner as found + self::whereIn('id', $existingIds)->update(['found' => true]); + } + + /** + * Looks up and translates a message by its string. + * @param string $messageId + * @param array $params + * @param string $locale + * @return string + */ + public static function trans($messageId, $params = [], $locale = null) + { + $msg = static::get($messageId, $locale); + + $params = array_build($params, function($key, $value){ + return [':'.$key, e($value)]; + }); + + $msg = strtr($msg, $params); + + return $msg; + } + + /** + * Looks up and translates a message by its string WITHOUT escaping params. + * @param string $messageId + * @param array $params + * @param string $locale + * @return string + */ + public static function transRaw($messageId, $params = [], $locale = null) + { + $msg = static::get($messageId, $locale); + + $params = array_build($params, function($key, $value){ + return [':'.$key, $value]; + }); + + $msg = strtr($msg, $params); + + return $msg; + } + + /** + * Set the caching context, the page url. + * @param string $locale + * @param string $url + */ + public static function setContext($locale, $url = null) + { + if (!strlen($url)) { + $url = '/'; + } + + self::$url = $url; + self::$locale = $locale; + + if ($cached = Cache::get(static::makeCacheKey())) { + self::$cache = (array) $cached; + } + } + + /** + * Save context messages to cache. + * @return void + */ + public static function saveToCache() + { + if (!self::$hasNew || !self::$url || !self::$locale) { + return; + } + + $expiresAt = now()->addMinutes(Config::get('rainlab.translate::cacheTimeout', 1440)); + Cache::put(static::makeCacheKey(), self::$cache, $expiresAt); + } + + /** + * Creates a cache key for storing context messages. + * @return string + */ + protected static function makeCacheKey() + { + return 'translation.'.self::$locale.self::$url; + } + + /** + * Creates a sterile key for a message. + * @param string $messageId + * @return string + */ + protected static function makeMessageCode($messageId) + { + $separator = '.'; + + // Convert all dashes/underscores into separator + $messageId = preg_replace('!['.preg_quote('_').'|'.preg_quote('-').']+!u', $separator, $messageId); + + // Remove all characters that are not the separator, letters, numbers, or whitespace. + $messageId = preg_replace('![^'.preg_quote($separator).'\pL\pN\s]+!u', '', mb_strtolower($messageId)); + + // Replace all separator characters and whitespace by a single separator + $messageId = preg_replace('!['.preg_quote($separator).'\s]+!u', $separator, $messageId); + + return Str::limit(trim($messageId, $separator), 250); + } +} diff --git a/plugins/rainlab/translate/models/MessageExport.php b/plugins/rainlab/translate/models/MessageExport.php new file mode 100644 index 00000000..415e88d2 --- /dev/null +++ b/plugins/rainlab/translate/models/MessageExport.php @@ -0,0 +1,52 @@ +map(function ($message) use ($columns) { + $data = $message->message_data; + // Add code to data to simplify algorithm + $data[self::CODE_COLUMN_NAME] = $message->code; + + $result = []; + foreach ($columns as $column) { + $result[$column] = isset($data[$column]) ? $data[$column] : ''; + } + return $result; + })->toArray(); + } + + /** + * getColumns + * + * code, default column + all existing locales + * + * @return array + */ + public static function getColumns() + { + return array_merge([ + self::CODE_COLUMN_NAME => self::CODE_COLUMN_NAME, + Message::DEFAULT_LOCALE => self::DEFAULT_COLUMN_NAME, + ], Locale::lists(self::CODE_COLUMN_NAME, self::CODE_COLUMN_NAME)); + } +} diff --git a/plugins/rainlab/translate/models/MessageImport.php b/plugins/rainlab/translate/models/MessageImport.php new file mode 100644 index 00000000..3657727b --- /dev/null +++ b/plugins/rainlab/translate/models/MessageImport.php @@ -0,0 +1,70 @@ + 'required' + ]; + + /** + * Import the message data from a csv with the following schema: + * + * code | en | de | fr + * ------------------------------- + * title | Title | Titel | Titre + * name | Name | Name | Prénom + * ... + * + * The code column is required and must not be empty. + * + * Note: Messages with an existing code are not removed/touched if the import + * doesn't contain this code. As a result you can incrementally update the + * messages by just adding the new codes and messages to the csv. + * + * @param $results + * @param null $sessionKey + */ + public function importData($results, $sessionKey = null) + { + $codeName = MessageExport::CODE_COLUMN_NAME; + $defaultName = Message::DEFAULT_LOCALE; + + foreach ($results as $index => $result) { + try { + if (isset($result[$codeName]) && !empty($result[$codeName])) { + $code = $result[$codeName]; + + // Modify result to match the expected message_data schema + unset($result[$codeName]); + + $message = Message::firstOrNew(['code' => $code]); + + // Create empty array, if $message is new + $message->message_data = $message->message_data ?: []; + + if (!isset($message->message_data[$defaultName])) { + $default = (isset($result[$defaultName]) && !empty($result[$defaultName])) ? $result[$defaultName] : $code; + $result[$defaultName] = $default; + } + + $message->message_data = array_merge($message->message_data, $result); + + if ($message->exists) { + $this->logUpdated(); + } else { + $this->logCreated(); + } + + $message->save(); + } else { + $this->logSkipped($index, 'No code provided'); + } + } catch (\Exception $exception) { + $this->logError($index, $exception->getMessage()); + } + } + } +} diff --git a/plugins/rainlab/translate/models/locale/columns.yaml b/plugins/rainlab/translate/models/locale/columns.yaml new file mode 100644 index 00000000..602eae29 --- /dev/null +++ b/plugins/rainlab/translate/models/locale/columns.yaml @@ -0,0 +1,26 @@ +# =================================== +# Column Definitions +# =================================== + +columns: + name: + label: rainlab.translate::lang.locale.name + searchable: yes + + code: + label: rainlab.translate::lang.locale.code + searchable: yes + + is_default: + label: rainlab.translate::lang.locale.is_default + type: switch + + is_enabled: + label: rainlab.translate::lang.locale.is_enabled + type: switch + invisible: true + + sort_order: + label: rainlab.translate::lang.locale.sort_order + type: number + invisible: true diff --git a/plugins/rainlab/translate/models/locale/fields.yaml b/plugins/rainlab/translate/models/locale/fields.yaml new file mode 100644 index 00000000..dbda60d8 --- /dev/null +++ b/plugins/rainlab/translate/models/locale/fields.yaml @@ -0,0 +1,24 @@ +# =================================== +# Field Definitions +# =================================== + +fields: + name: + label: rainlab.translate::lang.locale.name + span: auto + + code: + label: rainlab.translate::lang.locale.code + span: auto + + is_enabled: + label: rainlab.translate::lang.locale.is_enabled + type: checkbox + comment: rainlab.translate::lang.locale.is_enabled_help + span: auto + + is_default: + label: rainlab.translate::lang.locale.is_default + type: checkbox + comment: rainlab.translate::lang.locale.is_default_help + span: auto diff --git a/plugins/rainlab/translate/phpunit.xml b/plugins/rainlab/translate/phpunit.xml new file mode 100644 index 00000000..1fb393c9 --- /dev/null +++ b/plugins/rainlab/translate/phpunit.xml @@ -0,0 +1,23 @@ + + + + + ./tests + + + + + + + + diff --git a/plugins/rainlab/translate/routes.php b/plugins/rainlab/translate/routes.php new file mode 100644 index 00000000..66f247bb --- /dev/null +++ b/plugins/rainlab/translate/routes.php @@ -0,0 +1,42 @@ +handleLocaleRoute(); + if (!$locale) { + return; + } + + /* + * Register routes + */ + Route::group(['prefix' => $locale, 'middleware' => 'web'], function () { + Route::any('{slug?}', 'Cms\Classes\CmsController@run')->where('slug', '(.*)?'); + }); + + Route::any($locale, 'Cms\Classes\CmsController@run')->middleware('web'); + + /* + * Ensure Url::action() retains the localized URL + * by re-registering the route after the CMS. + */ + Event::listen('cms.route', function () use ($locale) { + Route::group(['prefix' => $locale, 'middleware' => 'web'], function () { + Route::any('{slug?}', 'Cms\Classes\CmsController@run')->where('slug', '(.*)?'); + }); + }); +}); + +/* + * Save any used messages to the contextual cache. + */ +App::after(function ($request) { + if (class_exists('RainLab\Translate\Models\Message')) { + Message::saveToCache(); + } +}); diff --git a/plugins/rainlab/translate/tests/fixtures/classes/Feature.php b/plugins/rainlab/translate/tests/fixtures/classes/Feature.php new file mode 100644 index 00000000..eabe9872 --- /dev/null +++ b/plugins/rainlab/translate/tests/fixtures/classes/Feature.php @@ -0,0 +1,24 @@ + true], 'states']; + + /** + * @var string The database table used by the model. + */ + public $table = 'translate_test_countries'; + + /** + * @var array Guarded fields + */ + protected $guarded = []; + + /** + * @var array Jsonable fields + */ + protected $jsonable = ['states']; +} diff --git a/plugins/rainlab/translate/tests/fixtures/themes/test/.gitignore b/plugins/rainlab/translate/tests/fixtures/themes/test/.gitignore new file mode 100644 index 00000000..d6b7ef32 --- /dev/null +++ b/plugins/rainlab/translate/tests/fixtures/themes/test/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/plugins/rainlab/translate/tests/unit/behaviors/TranslatableCmsObjectTest.php b/plugins/rainlab/translate/tests/unit/behaviors/TranslatableCmsObjectTest.php new file mode 100644 index 00000000..12286564 --- /dev/null +++ b/plugins/rainlab/translate/tests/unit/behaviors/TranslatableCmsObjectTest.php @@ -0,0 +1,110 @@ +themePath = __DIR__ . '/../../fixtures/themes/test'; + + $this->seedSampleSourceAndData(); + } + + public function tearDown(): void + { + $this->cleanUp(); + } + + protected function cleanUp() + { + @unlink($this->themePath.'/features/winning.htm'); + @unlink($this->themePath.'/features-fr/winning.htm'); + File::deleteDirectory($this->themePath.'/features'); + File::deleteDirectory($this->themePath.'/features-fr'); + } + + protected function seedSampleSourceAndData() + { + $datasource = new FileDatasource($this->themePath, new Filesystem); + $resolver = new Resolver(['theme1' => $datasource]); + $resolver->setDefaultDatasource('theme1'); + Model::setDatasourceResolver($resolver); + + LocaleModel::unguard(); + + LocaleModel::firstOrCreate([ + 'code' => 'fr', + 'name' => 'French', + 'is_enabled' => 1 + ]); + + LocaleModel::reguard(); + + $this->recycleSampleData(); + } + + protected function recycleSampleData() + { + $this->cleanUp(); + + FeatureModel::create([ + 'fileName' => 'winning.htm', + 'settings' => ['title' => 'Hash tag winning'], + 'markup' => 'Awww yiss', + ]); + } + + public function testGetTranslationValue() + { + $obj = FeatureModel::first(); + + $this->assertEquals('Awww yiss', $obj->markup); + + $obj->translateContext('fr'); + + $this->assertEquals('Awww yiss', $obj->markup); + } + + public function testGetTranslationValueNoFallback() + { + $obj = FeatureModel::first(); + + $this->assertEquals('Awww yiss', $obj->markup); + + $obj->noFallbackLocale()->translateContext('fr'); + + $this->assertEquals(null, $obj->markup); + } + + public function testSetTranslationValue() + { + $this->recycleSampleData(); + + $obj = FeatureModel::first(); + $obj->markup = 'Aussie'; + $obj->save(); + + $obj->translateContext('fr'); + $obj->markup = 'Australie'; + $obj->save(); + + $obj = FeatureModel::first(); + $this->assertEquals('Aussie', $obj->markup); + + $obj->translateContext('fr'); + $this->assertEquals('Australie', $obj->markup); + } + +} diff --git a/plugins/rainlab/translate/tests/unit/behaviors/TranslatableModelTest.php b/plugins/rainlab/translate/tests/unit/behaviors/TranslatableModelTest.php new file mode 100644 index 00000000..f33509d8 --- /dev/null +++ b/plugins/rainlab/translate/tests/unit/behaviors/TranslatableModelTest.php @@ -0,0 +1,202 @@ +seedSampleTableAndData(); + } + + protected function seedSampleTableAndData() + { + if (Schema::hasTable('translate_test_countries')) { + return; + } + + Model::unguard(); + + Schema::create('translate_test_countries', function($table) + { + $table->engine = 'InnoDB'; + $table->increments('id'); + $table->string('name')->nullable(); + $table->string('code')->nullable(); + $table->text('states')->nullable(); + $table->timestamps(); + }); + + LocaleModel::firstOrCreate([ + 'code' => 'fr', + 'name' => 'French', + 'is_enabled' => 1 + ]); + + $this->recycleSampleData(); + + Model::reguard(); + } + + protected function recycleSampleData() + { + CountryModel::truncate(); + + CountryModel::create([ + 'name' => 'Australia', + 'code' => 'AU', + 'states' => ['NSW', 'ACT', 'QLD'], + ]); + } + + public function testGetTranslationValue() + { + $obj = CountryModel::first(); + + $this->assertEquals('Australia', $obj->name); + $this->assertEquals(['NSW', 'ACT', 'QLD'], $obj->states); + + $obj->translateContext('fr'); + + $this->assertEquals('Australia', $obj->name); + } + + public function testGetTranslationValueNoFallback() + { + $obj = CountryModel::first(); + + $this->assertEquals('Australia', $obj->name); + + $obj->noFallbackLocale()->translateContext('fr'); + + $this->assertEquals(null, $obj->name); + } + + public function testSetTranslationValue() + { + $this->recycleSampleData(); + + $obj = CountryModel::first(); + $obj->name = 'Aussie'; + $obj->states = ['VIC', 'SA', 'NT']; + $obj->save(); + + $obj->translateContext('fr'); + $obj->name = 'Australie'; + $obj->states = ['a', 'b', 'c']; + $obj->save(); + + $obj = CountryModel::first(); + $this->assertEquals('Aussie', $obj->name); + $this->assertEquals(['VIC', 'SA', 'NT'], $obj->states); + + $obj->translateContext('fr'); + $this->assertEquals('Australie', $obj->name); + $this->assertEquals(['a', 'b', 'c'], $obj->states); + } + + public function testGetTranslationValueEagerLoading() + { + $this->recycleSampleData(); + + $obj = CountryModel::first(); + $obj->translateContext('fr'); + $obj->name = 'Australie'; + $obj->states = ['a', 'b', 'c']; + $obj->save(); + + $objList = CountryModel::with([ + 'translations' + ])->get(); + + $obj = $objList[0]; + $this->assertEquals('Australia', $obj->name); + $this->assertEquals(['NSW', 'ACT', 'QLD'], $obj->states); + + $obj->translateContext('fr'); + $this->assertEquals('Australie', $obj->name); + $this->assertEquals(['a', 'b', 'c'], $obj->states); + } + + public function testTranslateWhere() + { + $this->recycleSampleData(); + + $obj = CountryModel::first(); + + $obj->translateContext('fr'); + $obj->name = 'Australie'; + $obj->save(); + + $this->assertEquals(0, CountryModel::transWhere('name', 'Australie')->count()); + + Translator::instance()->setLocale('fr'); + $this->assertEquals(1, CountryModel::transWhere('name', 'Australie')->count()); + + Translator::instance()->setLocale('en'); + } + + public function testTranslateOrderBy() + { + $this->recycleSampleData(); + + $obj = CountryModel::first(); + + $obj->translateContext('fr'); + $obj->name = 'Australie'; + $obj->save(); + + $obj = CountryModel::create([ + 'name' => 'Germany', + 'code' => 'DE' + ]); + + $obj->translateContext('fr'); + $obj->name = 'Allemagne'; + $obj->save(); + + $res = CountryModel::transOrderBy('name')->get()->pluck('name'); + $this->assertEquals(['Australia', 'Germany'], $res->toArray()); + + Translator::instance()->setLocale('fr'); + $res = CountryModel::transOrderBy('name')->get()->pluck('name'); + $this->assertEquals(['Allemagne', 'Australie'], $res->toArray()); + + Translator::instance()->setLocale('en'); + } + + public function testGetTranslationValueEagerLoadingWithMorphMap() + { + Relation::morphMap([ + 'morph.key' => CountryModel::class, + ]); + + $this->recycleSampleData(); + + $obj = CountryModel::first(); + $obj->translateContext('fr'); + $obj->name = 'Australie'; + $obj->states = ['a', 'b', 'c']; + $obj->save(); + + $objList = CountryModel::with([ + 'translations' + ])->get(); + + $obj = $objList[0]; + $this->assertEquals('Australia', $obj->name); + $this->assertEquals(['NSW', 'ACT', 'QLD'], $obj->states); + + $obj->translateContext('fr'); + $this->assertEquals('Australie', $obj->name); + $this->assertEquals(['a', 'b', 'c'], $obj->states); + } +} diff --git a/plugins/rainlab/translate/tests/unit/behaviors/TranslatablePageTest.php b/plugins/rainlab/translate/tests/unit/behaviors/TranslatablePageTest.php new file mode 100644 index 00000000..0bbfa448 --- /dev/null +++ b/plugins/rainlab/translate/tests/unit/behaviors/TranslatablePageTest.php @@ -0,0 +1,67 @@ +themePath = __DIR__ . '/../../fixtures/themes/test'; + + $datasource = new FileDatasource($this->themePath, new Filesystem); + $resolver = new Resolver(['theme1' => $datasource]); + $resolver->setDefaultDatasource('theme1'); + Model::setDatasourceResolver($resolver); + + TranslatablePage::extend(function($page) { + if (!$page->isClassExtendedWith('RainLab\Translate\Behaviors\TranslatablePage')) { + $page->addDynamicProperty('translatable', ['title']); + $page->extendClassWith('RainLab\Translate\Behaviors\TranslatablePage'); + } + }); + + } + + public function tearDown(): void + { + File::deleteDirectory($this->themePath.'/pages'); + } + + public function testUseFallback() + { + $page = TranslatablePage::create([ + 'fileName' => 'translatable', + 'title' => 'english title', + 'url' => '/test', + ]); + $page->translateContext('fr'); + $this->assertEquals('english title', $page->title); + $page->noFallbackLocale()->translateContext('fr'); + $this->assertEquals(null, $page->title); + } + + public function testAlternateLocale() + { + $page = TranslatablePage::create([ + 'fileName' => 'translatable', + 'title' => 'english title', + 'url' => '/test', + ]); + $page->setAttributeTranslated('title', 'titre francais', 'fr'); + $title_en = $page->title; + $this->assertEquals('english title', $title_en); + $page->translateContext('fr'); + $title_fr = $page->title; + $this->assertEquals('titre francais', $title_fr); + } +} diff --git a/plugins/rainlab/translate/tests/unit/models/ExportMessageTest.php b/plugins/rainlab/translate/tests/unit/models/ExportMessageTest.php new file mode 100644 index 00000000..76262c9b --- /dev/null +++ b/plugins/rainlab/translate/tests/unit/models/ExportMessageTest.php @@ -0,0 +1,98 @@ +exportData([]); + + $this->assertEquals($expected, $result); + } + + public function testCanHandleNoColumn() + { + $exportModel = new MessageExport(); + $this->createMessages(); + $expected = [[], []]; + + $result = $exportModel->exportData([]); + + $this->assertEquals($expected, $result); + } + + public function testExportSomeColumns() + { + $exportMode = new MessageExport(); + $this->createMessages(); + $expected = [ + ['code' => 'hello'], + ['code' => 'bye'], + ]; + + $result = $exportMode->exportData(['code']); + + $this->assertEquals($expected, $result); + } + + public function testExportAllColumns() + { + $exportMode = new MessageExport(); + $this->createMessages(); + $expected = [ + ['code' => 'hello', 'de' => 'Hallo, Welt', 'en' => 'Hello, World'], + ['code' => 'bye', 'de' => 'Auf Wiedersehen', 'en' => 'Goodbye'], + ]; + + $result = $exportMode->exportData(['code', 'de', 'en']); + + $this->assertEquals($expected, $result); + } + + public function testCanHandleNonExistingColumns() + { + $exportMode = new MessageExport(); + $this->createMessages(); + $expected = [ + ['dummy' => ''], + ['dummy' => ''], + ]; + + $result = $exportMode->exportData(['dummy']); + + $this->assertEquals($expected, $result); + } + + private function createMessages() + { + Message::create([ + 'code' => 'hello', 'message_data' => ['de' => 'Hallo, Welt', 'en' => 'Hello, World'] + ]); + Message::create([ + 'code' => 'bye', 'message_data' => ['de' => 'Auf Wiedersehen', 'en' => 'Goodbye'] + ]); + } + + public function testGetColumns() + { + Locale::unguard(); + Locale::create(['code' => 'de', 'name' => 'German', 'is_enabled' => true]); + + $columns = MessageExport::getColumns(); + + $this->assertEquals([ + MessageExport::CODE_COLUMN_NAME => MessageExport::CODE_COLUMN_NAME, + Message::DEFAULT_LOCALE => MessageExport::DEFAULT_COLUMN_NAME, + 'en' => 'en', + 'de' => 'de', + ], $columns); + } +} diff --git a/plugins/rainlab/translate/tests/unit/models/ImportMessageTest.php b/plugins/rainlab/translate/tests/unit/models/ImportMessageTest.php new file mode 100644 index 00000000..8b9c02ab --- /dev/null +++ b/plugins/rainlab/translate/tests/unit/models/ImportMessageTest.php @@ -0,0 +1,95 @@ +importData($data); + + $stats = $messageImport->getResultStats(); + $this->assertEquals(false, $stats->hasMessages); + } + + public function testCreateMessage() + { + $messageImport = new MessageImport(); + $data = [ + ['code' => 'new', 'de' => 'Neu', 'en' => 'new'] + ]; + + $messageImport->importData($data); + + $stats = $messageImport->getResultStats(); + $this->assertEquals(1, $stats->created); + $this->assertEquals(0, $stats->updated); + $this->assertEquals(0, $stats->skippedCount); + $this->assertEquals(false, $stats->hasMessages); + } + + public function testUpdateMessage() + { + $messageImport = new MessageImport(); + Message::create(['code' => 'update', 'message_data' => ['en' => 'update', 'de' => 'aktualisieren']]); + $data = [ + ['code' => 'update', 'de' => 'Neu 2', 'en' => 'new 2'] + ]; + $expected = [ + Message::DEFAULT_LOCALE => 'update', 'de' => 'Neu 2', 'en' => 'new 2' + ]; + + $messageImport->importData($data); + + $stats = $messageImport->getResultStats(); + $this->assertEquals(0, $stats->created); + $this->assertEquals(1, $stats->updated); + $this->assertEquals(0, $stats->skippedCount); + $this->assertEquals(false, $stats->hasMessages); + $updatedMessage = Message::whereCode('update')->first(); + $this->assertEquals($expected, $updatedMessage->message_data); + } + + public function testMissingCodeIsSkipped() + { + $messageImport = new MessageImport(); + $data = [ + ['de' => 'Neu 2', 'en' => 'new 2'] + ]; + + $messageImport->importData($data); + + $stats = $messageImport->getResultStats(); + $this->assertEquals(0, $stats->created); + $this->assertEquals(0, $stats->updated); + $this->assertEquals(1, $stats->skippedCount); + $this->assertEquals(true, $stats->hasMessages); + $this->assertEquals(Message::count(), 0); + } + + public function testDefaultLocaleIsImported() + { + $messageImport = new MessageImport(); + $data = [ + ['code' => 'test.me', 'x' => 'foo bar', 'de' => 'Neu 2', 'en' => 'new 2'] + ]; + + $messageImport->importData($data); + + $stats = $messageImport->getResultStats(); + $this->assertEquals(1, $stats->created); + $this->assertEquals(0, $stats->updated); + $this->assertEquals(0, $stats->skippedCount); + $this->assertEquals(false, $stats->hasMessages); + $this->assertEquals(Message::count(), 1); + + $message = Message::where('code', 'test.me')->first(); + + $this->assertEquals('foo bar', $message->message_data['x']); + } +} diff --git a/plugins/rainlab/translate/tests/unit/models/MessageTest.php b/plugins/rainlab/translate/tests/unit/models/MessageTest.php new file mode 100644 index 00000000..32a33eb3 --- /dev/null +++ b/plugins/rainlab/translate/tests/unit/models/MessageTest.php @@ -0,0 +1,61 @@ +assertNotNull(Message::whereCode('hello.world')->first()); + $this->assertNotNull(Message::whereCode('hello.piñata')->first()); + + Message::truncate(); + } + + public function testMakeMessageCode() + { + $this->assertEquals('hello.world', Message::makeMessageCode('hello world')); + $this->assertEquals('hello.world', Message::makeMessageCode(' hello world ')); + $this->assertEquals('hello.world', Message::makeMessageCode('hello-world')); + $this->assertEquals('hello.world', Message::makeMessageCode('hello--world')); + + // casing + $this->assertEquals('hello.world', Message::makeMessageCode('Hello World')); + $this->assertEquals('hello.world', Message::makeMessageCode('Hello World!')); + + // underscores + $this->assertEquals('hello.world', Message::makeMessageCode('hello_world')); + $this->assertEquals('hello.world', Message::makeMessageCode('hello__world')); + + // length limit + $veryLongString = str_repeat("10 charstr", 30); + $this->assertTrue(strlen($veryLongString) > 250); + $this->assertEquals(253, strlen(Message::makeMessageCode($veryLongString))); + $this->assertStringEndsWith('...', Message::makeMessageCode($veryLongString)); + + // unicode characters + // brrowered some test cases from Stringy, the library Laravel's + // `slug()` function depends on + // https://github.com/danielstjules/Stringy/blob/master/tests/CommonTest.php + $this->assertEquals('fòô.bàř', Message::makeMessageCode('fòô bàř')); + $this->assertEquals('ťéśţ', Message::makeMessageCode(' ŤÉŚŢ ')); + $this->assertEquals('φ.ź.3', Message::makeMessageCode('φ = ź = 3')); + $this->assertEquals('перевірка', Message::makeMessageCode('перевірка')); + $this->assertEquals('лысая.гора', Message::makeMessageCode('лысая гора')); + $this->assertEquals('щука', Message::makeMessageCode('щука')); + $this->assertEquals('foo.漢字', Message::makeMessageCode('foo 漢字')); // Chinese + $this->assertEquals('xin.chào.thế.giới', Message::makeMessageCode('xin chào thế giới')); + $this->assertEquals('xin.chào.thế.giới', Message::makeMessageCode('XIN CHÀO THẾ GIỚI')); + $this->assertEquals('đấm.phát.chết.luôn', Message::makeMessageCode('đấm phát chết luôn')); + $this->assertEquals('foo', Message::makeMessageCode('foo ')); // no-break space (U+00A0) + $this->assertEquals('foo', Message::makeMessageCode('foo           ')); // spaces U+2000 to U+200A + $this->assertEquals('foo', Message::makeMessageCode('foo ')); // narrow no-break space (U+202F) + $this->assertEquals('foo', Message::makeMessageCode('foo ')); // medium mathematical space (U+205F) + $this->assertEquals('foo', Message::makeMessageCode('foo ')); // ideographic space (U+3000) + } +} diff --git a/plugins/rainlab/translate/traits/MLControl.php b/plugins/rainlab/translate/traits/MLControl.php new file mode 100644 index 00000000..cbef3974 --- /dev/null +++ b/plugins/rainlab/translate/traits/MLControl.php @@ -0,0 +1,279 @@ +defaultLocale = Locale::getDefault(); + $this->isAvailable = Locale::isAvailable(); + } + + /** + * getParentViewPath returns the parent control's view path + * + * @return string + */ + protected function getParentViewPath() + { + // return base_path().'/modules/backend/formwidgets/parentcontrol/partials'; + } + + /** + * getParentAssetPath returns the parent control's asset path + * + * @return string + */ + protected function getParentAssetPath() + { + // return '/modules/backend/formwidgets/parentcontrol/assets'; + } + + /** + * actAsParent swaps the asset & view paths with the parent control's to + * act as the parent control + * + * @param boolean $switch Defaults to true, determines whether to act as the parent or revert to current + */ + protected function actAsParent($switch = true) + { + if ($switch) { + $this->originalAssetPath = $this->assetPath; + $this->originalViewPath = $this->viewPath; + $this->assetPath = $this->getParentAssetPath(); + $this->viewPath = $this->getParentViewPath(); + } + else { + $this->assetPath = $this->originalAssetPath; + $this->viewPath = $this->originalViewPath; + } + } + + /** + * {@inheritDoc} + */ + public function renderFallbackField() + { + return $this->makeMLPartial('fallback_field'); + } + + /** + * makeMLPartial is used by child classes to render in context of this view path. + * @param string $partial The view to load. + * @param array $params Parameter variables to pass to the view. + * @return string The view contents. + */ + public function makeMLPartial($partial, $params = []) + { + $oldViewPath = $this->viewPath; + $this->viewPath = $this->guessViewPathFrom(__TRAIT__, '/partials'); + $result = $this->makePartial($partial, $params); + $this->viewPath = $oldViewPath; + + return $result; + } + + /** + * prepareLocaleVars prepares the list data + */ + public function prepareLocaleVars() + { + $this->vars['defaultLocale'] = $this->defaultLocale; + $this->vars['locales'] = Locale::listAvailable(); + $this->vars['field'] = $this->makeRenderFormField(); + } + + /** + * loadLocaleAssets loads assets specific to ML Controls + */ + public function loadLocaleAssets() + { + $this->addJs('/plugins/rainlab/translate/assets/js/multilingual.js', 'RainLab.Translate'); + $this->addCss('/plugins/rainlab/translate/assets/css/multilingual.css', 'RainLab.Translate'); + + if (!class_exists('System')) { + $this->addCss('/plugins/rainlab/translate/assets/css/multilingual-v1.css', 'RainLab.Translate'); + } + } + + /** + * getLocaleValue returns a translated value for a given locale. + * @param string $locale + * @return string + */ + public function getLocaleValue($locale) + { + $key = $this->valueFrom ?: $this->fieldName; + + /* + * Get the translated values from the model + */ + $studKey = Str::studly(implode(' ', HtmlHelper::nameToArray($key))); + $mutateMethod = 'get'.$studKey.'AttributeTranslated'; + + if ($this->objectMethodExists($this->model, $mutateMethod)) { + $value = $this->model->$mutateMethod($locale); + } + elseif ($this->objectMethodExists($this->model, 'getAttributeTranslated') && $this->defaultLocale->code != $locale) { + $value = $this->model->noFallbackLocale()->getAttributeTranslated($key, $locale); + } + else { + $value = $this->formField->value; + } + + return $value; + } + + /** + * makeRenderFormField if translation is unavailable, render the original field type (text). + */ + protected function makeRenderFormField() + { + if ($this->isAvailable) { + return $this->formField; + } + + $field = clone $this->formField; + $field->type = $this->getFallbackType(); + + return $field; + } + + /** + * {@inheritDoc} + */ + public function getLocaleSaveValue($value) + { + $localeData = $this->getLocaleSaveData(); + $key = $this->valueFrom ?: $this->fieldName; + + /* + * Set the translated values to the model + */ + $studKey = Str::studly(implode(' ', HtmlHelper::nameToArray($key))); + $mutateMethod = 'set'.$studKey.'AttributeTranslated'; + + if ($this->objectMethodExists($this->model, $mutateMethod)) { + foreach ($localeData as $locale => $value) { + $this->model->$mutateMethod($value, $locale); + } + } + elseif ($this->objectMethodExists($this->model, 'setAttributeTranslated')) { + foreach ($localeData as $locale => $value) { + $this->model->setAttributeTranslated($key, $value, $locale); + } + } + + return array_get($localeData, $this->defaultLocale->code, $value); + } + + /** + * getLocaleSaveData returns an array of translated values for this field + * @return array + */ + public function getLocaleSaveData() + { + $values = []; + $data = post('RLTranslate'); + + if (!is_array($data)) { + return $values; + } + + $fieldName = implode('.', HtmlHelper::nameToArray($this->fieldName)); + $isJson = $this->isLocaleFieldJsonable(); + + foreach ($data as $locale => $_data) { + $value = array_get($_data, $fieldName); + $values[$locale] = $isJson ? json_decode($value, true) : $value; + } + + return $values; + } + + /** + * getFallbackType returns the fallback field type. + * @return string + */ + public function getFallbackType() + { + return defined('static::FALLBACK_TYPE') ? static::FALLBACK_TYPE : 'text'; + } + + /** + * isLocaleFieldJsonable returns true if widget is a repeater, or the field is specified + * as jsonable in the model. + * @return bool + */ + public function isLocaleFieldJsonable() + { + if ( + $this instanceof \Backend\FormWidgets\Repeater || + $this instanceof \Backend\FormWidgets\NestedForm + ) { + return true; + } + + if ($this instanceof \Media\FormWidgets\MediaFinder && $this->maxItems !== 1) { + return true; + } + + if ( + method_exists($this->model, 'isJsonable') && + $this->model->isJsonable($this->fieldName) + ) { + return true; + } + + return false; + } + + /** + * objectMethodExists is an internal helper for method existence checks. + * + * @param object $object + * @param string $method + * @return boolean + */ + protected function objectMethodExists($object, $method) + { + if (method_exists($object, 'methodExists')) { + return $object->methodExists($method); + } + + return method_exists($object, $method); + } +} diff --git a/plugins/rainlab/translate/traits/mlcontrol/partials/_fallback_field.htm b/plugins/rainlab/translate/traits/mlcontrol/partials/_fallback_field.htm new file mode 100644 index 00000000..0ffd1abc --- /dev/null +++ b/plugins/rainlab/translate/traits/mlcontrol/partials/_fallback_field.htm @@ -0,0 +1,3 @@ +
    + makePartial('~/modules/backend/widgets/form/partials/_field_'.$field->type.'.htm', ['field' => $field]) ?> +
    \ No newline at end of file diff --git a/plugins/rainlab/translate/traits/mlcontrol/partials/_locale_selector.htm b/plugins/rainlab/translate/traits/mlcontrol/partials/_locale_selector.htm new file mode 100644 index 00000000..fb2c0b6e --- /dev/null +++ b/plugins/rainlab/translate/traits/mlcontrol/partials/_locale_selector.htm @@ -0,0 +1,18 @@ + + diff --git a/plugins/rainlab/translate/traits/mlcontrol/partials/_locale_values.htm b/plugins/rainlab/translate/traits/mlcontrol/partials/_locale_values.htm new file mode 100644 index 00000000..ae24007a --- /dev/null +++ b/plugins/rainlab/translate/traits/mlcontrol/partials/_locale_values.htm @@ -0,0 +1,15 @@ + + $name): ?> + getLocaleValue($code); + $value = $this->isLocaleFieldJsonable() ? json_encode($value) : $value; + if (is_array($value)) $value = array_first($value); + ?> + getAttributes() ?> + /> + diff --git a/plugins/rainlab/translate/updates/builder_table_update_rainlab_translate_locales.php b/plugins/rainlab/translate/updates/builder_table_update_rainlab_translate_locales.php new file mode 100644 index 00000000..0c1f3191 --- /dev/null +++ b/plugins/rainlab/translate/updates/builder_table_update_rainlab_translate_locales.php @@ -0,0 +1,44 @@ +integer('sort_order')->default(0); + }); + } + + $locales = Locale::all(); + foreach($locales as $locale) { + $locale->sort_order = $locale->id; + $locale->save(); + } + } + + public function down() + { + if (!Schema::hasTable(self::TABLE_NAME)) { + return; + } + + if (Schema::hasColumn(self::TABLE_NAME, 'sort_order')) { + Schema::table(self::TABLE_NAME, function($table) + { + $table->dropColumn(['sort_order']); + }); + } + } +} diff --git a/plugins/rainlab/translate/updates/create_attributes_table.php b/plugins/rainlab/translate/updates/create_attributes_table.php new file mode 100644 index 00000000..7dc5b96b --- /dev/null +++ b/plugins/rainlab/translate/updates/create_attributes_table.php @@ -0,0 +1,27 @@ +engine = 'InnoDB'; + $table->increments('id'); + $table->string('locale')->index(); + $table->string('model_id')->index()->nullable(); + $table->string('model_type')->index()->nullable(); + $table->mediumText('attribute_data')->nullable(); + }); + } + + public function down() + { + Schema::dropIfExists('rainlab_translate_attributes'); + } + +} diff --git a/plugins/rainlab/translate/updates/create_indexes_table.php b/plugins/rainlab/translate/updates/create_indexes_table.php new file mode 100644 index 00000000..2e90ecec --- /dev/null +++ b/plugins/rainlab/translate/updates/create_indexes_table.php @@ -0,0 +1,28 @@ +engine = 'InnoDB'; + $table->increments('id'); + $table->string('locale')->index(); + $table->string('model_id')->index()->nullable(); + $table->string('model_type')->index()->nullable(); + $table->string('item')->nullable()->index(); + $table->mediumText('value')->nullable(); + }); + } + + public function down() + { + Schema::dropIfExists('rainlab_translate_indexes'); + } + +} diff --git a/plugins/rainlab/translate/updates/create_locales_table.php b/plugins/rainlab/translate/updates/create_locales_table.php new file mode 100644 index 00000000..4d24e9ee --- /dev/null +++ b/plugins/rainlab/translate/updates/create_locales_table.php @@ -0,0 +1,27 @@ +engine = 'InnoDB'; + $table->increments('id'); + $table->string('code')->index(); + $table->string('name')->index()->nullable(); + $table->boolean('is_default')->default(0); + $table->boolean('is_enabled')->default(0); + }); + } + + public function down() + { + Schema::dropIfExists('rainlab_translate_locales'); + } + +} diff --git a/plugins/rainlab/translate/updates/create_messages_table.php b/plugins/rainlab/translate/updates/create_messages_table.php new file mode 100644 index 00000000..25324058 --- /dev/null +++ b/plugins/rainlab/translate/updates/create_messages_table.php @@ -0,0 +1,25 @@ +engine = 'InnoDB'; + $table->increments('id'); + $table->string('code')->index()->nullable(); + $table->mediumText('message_data')->nullable(); + }); + } + + public function down() + { + Schema::dropIfExists('rainlab_translate_messages'); + } + +} diff --git a/plugins/rainlab/translate/updates/migrate_morphed_attributes.php b/plugins/rainlab/translate/updates/migrate_morphed_attributes.php new file mode 100644 index 00000000..10b9b07f --- /dev/null +++ b/plugins/rainlab/translate/updates/migrate_morphed_attributes.php @@ -0,0 +1,32 @@ +getTable(); + foreach (Relation::$morphMap as $alias => $class) { + Db::table($table)->where('model_type', $class)->update(['model_type' => $alias]); + } + } + + public function down() + { + $table = (new Attribute())->getTable(); + foreach (Relation::$morphMap as $alias => $class) { + Db::table($table)->where('model_type', $alias)->update(['model_type' => $class]); + } + } +} diff --git a/plugins/rainlab/translate/updates/migrate_morphed_indexes.php b/plugins/rainlab/translate/updates/migrate_morphed_indexes.php new file mode 100644 index 00000000..29300f3e --- /dev/null +++ b/plugins/rainlab/translate/updates/migrate_morphed_indexes.php @@ -0,0 +1,32 @@ + $class) { + Db::table($this->table)->where('model_type', $class)->update(['model_type' => $alias]); + } + } + + public function down() + { + foreach (Relation::$morphMap as $alias => $class) { + Db::table($this->table)->where('model_type', $alias)->update(['model_type' => $class]); + } + } +} diff --git a/plugins/rainlab/translate/updates/seed_all_tables.php b/plugins/rainlab/translate/updates/seed_all_tables.php new file mode 100644 index 00000000..31576810 --- /dev/null +++ b/plugins/rainlab/translate/updates/seed_all_tables.php @@ -0,0 +1,21 @@ + 'en', + 'name' => 'English', + 'is_default' => true, + 'is_enabled' => true + ]); + } + } + +} diff --git a/plugins/rainlab/translate/updates/update_messages_table.php b/plugins/rainlab/translate/updates/update_messages_table.php new file mode 100644 index 00000000..5d9955bc --- /dev/null +++ b/plugins/rainlab/translate/updates/update_messages_table.php @@ -0,0 +1,37 @@ +boolean('found')->default(1); + }); + } + } + + public function down() + { + if (!Schema::hasTable(self::TABLE_NAME)) { + return; + } + + if (Schema::hasColumn(self::TABLE_NAME, 'found')) { + Schema::table(self::TABLE_NAME, function($table) + { + $table->dropColumn(['found']); + }); + } + } +} diff --git a/plugins/rainlab/translate/updates/version.yaml b/plugins/rainlab/translate/updates/version.yaml new file mode 100644 index 00000000..49d3b30a --- /dev/null +++ b/plugins/rainlab/translate/updates/version.yaml @@ -0,0 +1,98 @@ +v1.0.1: + - First version of Translate + - create_messages_table.php + - create_attributes_table.php + - create_locales_table.php +v1.0.2: Languages and Messages can now be deleted. +v1.0.3: Minor updates for latest October release. +v1.0.4: Locale cache will clear when updating a language. +v1.0.5: Add Spanish language and fix plugin config. +v1.0.6: Minor improvements to the code. +v1.0.7: Fixes major bug where translations are skipped entirely! +v1.0.8: Minor bug fixes. +v1.0.9: Fixes an issue where newly created models lose their translated values. +v1.0.10: Minor fix for latest build. +v1.0.11: Fix multilingual rich editor when used in stretch mode. +v1.1.0: Introduce compatibility with RainLab.Pages plugin. +v1.1.1: Minor UI fix to the language picker. +v1.1.2: Add support for translating Static Content files. +v1.1.3: Improved support for the multilingual rich editor. +v1.1.4: Adds new multilingual markdown editor. +v1.1.5: Minor update to the multilingual control API. +v1.1.6: Minor improvements in the message editor. +v1.1.7: Fixes bug not showing content when first loading multilingual textarea controls. +v1.2.0: CMS pages now support translating the URL. +v1.2.1: Minor update in the rich editor and code editor language control position. +v1.2.2: Static Pages now support translating the URL. +v1.2.3: Fixes Rich Editor when inserting a page link. +v1.2.4: + - Translatable attributes can now be declared as indexes. + - create_indexes_table.php +v1.2.5: Adds new multilingual repeater form widget. +v1.2.6: Fixes repeater usage with static pages plugin. +v1.2.7: Fixes placeholder usage with static pages plugin. +v1.2.8: Improvements to code for latest October build compatibility. +v1.2.9: Fixes context for translated strings when used with Static Pages. +v1.2.10: Minor UI fix to the multilingual repeater. +v1.2.11: Fixes translation not working with partials loaded via AJAX. +v1.2.12: Add support for translating the new grouped repeater feature. +v1.3.0: Added search to the translate messages page. +v1.3.1: + - Added reordering to languages + - builder_table_update_rainlab_translate_locales.php + - seed_all_tables.php +v1.3.2: Improved compatibility with RainLab.Pages, added ability to scan Mail Messages for translatable variables. +v1.3.3: Fix to the locale picker session handling in Build 420 onwards. +v1.3.4: Add alternate hreflang elements and adds prefixDefaultLocale setting. +v1.3.5: Fix MLRepeater bug when switching locales. +v1.3.6: Fix Middleware to use the prefixDefaultLocale setting introduced in 1.3.4 +v1.3.7: Fix config reference in LocaleMiddleware +v1.3.8: Keep query string when switching locales +v1.4.0: Add importer and exporter for messages +v1.4.1: Updated Hungarian translation. Added Arabic translation. Fixed issue where default texts are overwritten by import. Fixed issue where the language switcher for repeater fields would overlap with the first repeater row. +v1.4.2: Add multilingual MediaFinder +v1.4.3: "!!! Please update OctoberCMS to Build 444 before updating this plugin. Added ability to translate CMS Pages fields (e.g. title, description, meta-title, meta-description)" +v1.4.4: Minor improvements to compatibility with Laravel framework. +v1.4.5: Fixed issue when using the language switcher +v1.5.0: Compatibility fix with Build 451 +v1.6.0: Make File Upload widget properties translatable. Merge Repeater core changes into MLRepeater widget. Add getter method to retrieve original translate data. +v1.6.1: Add ability for models to provide translated computed data, add option to disable locale prefix routing +v1.6.2: Implement localeUrl filter, add per-locale theme configuration support +v1.6.3: Add eager loading for translations, restore support for accessors & mutators +v1.6.4: Fixes PHP 7.4 compatibility +v1.6.5: Fixes compatibility issue when other plugins use a custom model morph map +v1.6.6: + - Introduce migration to patch existing translations using morph map + - migrate_morphed_attributes.php +v1.6.7: + - Introduce migration to patch existing indexes using morph map + - migrate_morphed_indexes.php +v1.6.8: Add support for transOrderBy; Add translation support for ThemeData; Update russian localization. +v1.6.9: Clear Static Page menu cache after saving the model; CSS fix for Text/Textarea input fields language selector. +v1.6.10: + - Add option to purge deleted messages when scanning messages, Add Scan error column on Messages page, Fix translations that were lost when clicking locale twice while holding ctrl key, Fix error with nested fields default locale value, Escape Message translate params value. + - update_messages_table.php +v1.7.0: "!!! Breaking change for the Message::trans() method (params are now escaped), fix message translation documentation, fix string translation key for scan errors column header." +v1.7.1: Fix YAML issue with previous tag/release. +v1.7.2: Fix regex when "|_" filter is followed by another filter, Try locale without country before returning default translation, Allow exporting default locale, Fire 'rainlab.translate.themeScanner.afterScan' event in the theme scanner for extendability. +v1.7.3: Make plugin ready for Laravel 6 update, Add support for translating RainLab.Pages MenuItem properties (requires RainLab.Pages v1.3.6), Restore multilingual button position for textarea, Fix translatableAttributes. +v1.7.4: Faster version of transWhere, Mail templates/views can now be localized, Fix messages table layout on mobile, Fix scopeTransOrderBy duplicates, Polish localization updates, Turkish localization updates, Add Greek language localization. +v1.8.0: Adds initial support for October v2.0 +v1.8.1: Minor bugfix +v1.8.2: Fixes translated file models and theme data for v2.0. The parent model must implement translatable behavior for their related file models to be translated. +v1.8.4: Fixes the multilingual mediafinder to work with the media module. +v1.8.6: Fixes invisible checkboxes when scanning for messages. +v1.8.7: Fixes Markdown editor translation. +v1.8.8: Fixes Laravel compatibility in custom Repeater. +v1.9.0: Restores ability to translate URLs with CMS Editor in October v2.0 +v1.9.1: Minor styling improvements +v1.9.2: Fixes issue creating new content in CMS Editor +v1.9.3: Improves support when using child themes +v1.10.0: Adds new multilingual nested form widget. Adds withFallbackLocale method. +v1.10.1: Improve support with October v2.0 +v1.10.2: Improve support with October v2.2 +v1.10.3: Multilingual control improvements +v1.10.4: Improve media finder support with October v2.2 +v1.10.5: Fixes media finder when only 1 locale is available +v1.11.0: Update to latest Media Finder changes in October v2.2 +v1.11.1: Improve support with October v3.0 diff --git a/plugins/site21/fields/Plugin.php b/plugins/site21/fields/Plugin.php new file mode 100644 index 00000000..b1057be6 --- /dev/null +++ b/plugins/site21/fields/Plugin.php @@ -0,0 +1,169 @@ + e(trans('site21.fields::lang.plugin.name')), + 'description' => e(trans('site21.fields::lang.plugin.description')), + 'author' => 'Site21', + 'icon' => 'icon-leaf', + 'homepage' => 'https://site21.ru' + ]; + } + + public function register() + { + + } + + public function boot() + { + Event::listen('backend.menu.extendItems', function($manager) { + $manager->addSideMenuItems('Lovata.Shopaholic', 'shopaholic-menu-main', [ + 'fields' => [ + 'code' => 'fields', + 'label' => e(trans('site21.fields::lang.menu.label')), + 'icon' => 'icon-tags', + 'url' => Backend::url('site21/fields/fields'), + 'order' => 550, + 'permissions' => [ + 'site21.fields.add_field_permission', + ], + ], + ]); + }); + if (Schema::hasTable('site21_shopaholic_fields')) { + $models = [ + Product::class, + Category::class, + Offer::class, + Brand::class, + ]; + foreach($models as $module) { + $model = new $module; + $model->extend(function ($model) { + $module = (new Field)->getModuleTable($model->table, 'key'); + if(!$module) { + return; + } + $query = Field::where('module', $module)->where('active', 1)->select('slug','settings'); + $fields = $query->get(); + if($fields) { + $array = []; + foreach($fields as $field) { + $model->fillable[] = $field->slug; + } + } + + // добавляем кэшируемые поля + $cached = $query->where('settings->cached', '1')->pluck('slug')->toArray(); + if($cached) { + $model->addCachedField($cached); + } + + // добавляем переводимые поля + $array = []; + foreach($fields as $field) { + if($field->settings['translatable'] == 1) { + $array[] = $field->slug; + } + } + if($array) { + $model->translatable = array_merge($model->translatable,$array); + } + }); + + Event::listen('backend.form.extendFieldsBefore', function($widget) { + if ($widget->isNested || $widget->alias != 'form') { + return; + } + $module = (new Field)->getModuleTable($widget->model->table, 'key'); + if(!$module) { + return; + } + // Получаем все кастомные поля только для обходимого модуля + $fields = Field::where('module', $module) + ->where('active', 1) + ->select('name', 'slug', 'type', 'tab', 'span', 'size', 'comment') + ->get(); + + if($fields) { + foreach($fields as $field) { + $widget->tabs['fields'][$field->slug] = [ + 'label' => $field->name, + 'type' => $field->type, + 'tab' => $field->tab, + 'span' => $field->span, + 'size' => $field->size, + 'comment' => $field->comment + ]; + } + } + }); + + Event::listen('backend.list.extendColumns', function ($listWidget) { + + $module = (new Field)->getModuleTable($listWidget->model->table, 'key'); + if(!$module) { + return; + } + + $fields = Field::where('module', $module)->where('active', 1)->where('settings->inlist', '1')->select('slug', 'name', 'type')->get(); + + if($fields) { + $columns = []; + foreach($fields as $field) { + $columns = [ + $field->slug => [ + 'label' => $field->name, + 'type' => $field->type, + ] + ]; + } + } + $listWidget->addColumns($columns); + }); + } + } + + } + + public function registerComponents() + { + return []; // Remove this line to activate + + return [ + 'Site21\Fields\Components\MyComponent' => 'myComponent', + ]; + } + + public function registerPermissions() + { + return [ + 'site21.fields.add_field_permission' => [ + 'tab' => e(trans('site21.fields::lang.plugin.name')), + 'label' => e(trans('site21.fields::lang.permissions.add')) + ], + ]; + } + + +} diff --git a/plugins/site21/fields/README.md b/plugins/site21/fields/README.md new file mode 100644 index 00000000..ff8a9548 --- /dev/null +++ b/plugins/site21/fields/README.md @@ -0,0 +1,28 @@ +# oc-shopaholic-fields +Простой плагин добавления в админку плагина Shopaholic для OctoberCMS своих текстовых полей. + +Поддерживает RainLab.Translate + +Подробная информация о том как разработан плагин описана на странице "[Shopaholic добавить поля](https://site21.ru/blog/shopaholic-add-fields)" + +На данные момент можно добавить следующие типы полей: +- text +- textarea + +В следующие модули Shopaholic: +- товары +- торговые предложения +- бренды +- категории + +## Todo: +Плагин будет поддеживаться и разваваться. +В следующей версии добавлю возможность добавления: +- выбора таба куда выводить поле +- select +- checkbox +- radio и т.д. + + + +[Мой блог об OctoberCMS](https://site21.ru/blog/tag/octobercms) diff --git a/plugins/site21/fields/composer.json b/plugins/site21/fields/composer.json new file mode 100644 index 00000000..62c1f11b --- /dev/null +++ b/plugins/site21/fields/composer.json @@ -0,0 +1,10 @@ +{ + "name": "site21/fields-plugin", + "type": "october-plugin", + "description": "A simple plugin for adding custom fields to Shopaholic admin panel (category, brand, offer or product)", + "require": { + "lovata/shopaholic-plugin": "^1.30", + "lovata/toolbox-plugin": "^1.33", + "composer/installers": "~1.0" + } +} diff --git a/plugins/site21/fields/controllers/Fields.php b/plugins/site21/fields/controllers/Fields.php new file mode 100644 index 00000000..254cc695 --- /dev/null +++ b/plugins/site21/fields/controllers/Fields.php @@ -0,0 +1,39 @@ + + + + + + +
    diff --git a/plugins/site21/fields/controllers/fields/config_form.yaml b/plugins/site21/fields/controllers/fields/config_form.yaml new file mode 100644 index 00000000..2dc78a5d --- /dev/null +++ b/plugins/site21/fields/controllers/fields/config_form.yaml @@ -0,0 +1,31 @@ +# =================================== +# Form Behavior Config +# =================================== + +# Record name +name: 'site21.fields::lang.fields.name' + +# Model Form Field configuration +form: $/site21/fields/models/field/fields.yaml + +# Model Class name +modelClass: Site21\Fields\Models\Field + +# Default redirect location +defaultRedirect: site21/fields/fields + +# Create page +create: + title: backend::lang.form.create_title + redirect: site21/fields/fields/update/:id + redirectClose: site21/fields/fields + +# Update page +update: + title: backend::lang.form.update_title + redirect: site21/fields/fields + redirectClose: site21/fields/fields + +# Preview page +preview: + title: backend::lang.form.preview_title diff --git a/plugins/site21/fields/controllers/fields/config_list.yaml b/plugins/site21/fields/controllers/fields/config_list.yaml new file mode 100644 index 00000000..a4021ea3 --- /dev/null +++ b/plugins/site21/fields/controllers/fields/config_list.yaml @@ -0,0 +1,47 @@ +# =================================== +# List Behavior Config +# =================================== + +# Model List Column configuration +list: $/site21/fields/models/field/columns.yaml + +# Model Class name +modelClass: Site21\Fields\Models\Field + +# List Title +title: 'site21.fields::lang.fields.manage' + +# Link URL for each record +recordUrl: site21/fields/fields/update/:id + +# Message to display if the list is empty +noRecordsMessage: backend::lang.list.no_records + +# Records to display per page +recordsPerPage: 20 + +# Display page numbers with pagination, disable to improve performance +showPageNumbers: true + +# Displays the list column set up button +showSetup: true + +# Displays the sorting link on each column +showSorting: true + +# Default sorting column +# defaultSort: +# column: created_at +# direction: desc + +# Display checkboxes next to each record +showCheckboxes: true + +# Toolbar widget configuration +toolbar: + # Partial for toolbar buttons + buttons: list_toolbar + + # Search widget configuration + search: + prompt: backend::lang.list.search_prompt diff --git a/plugins/site21/fields/controllers/fields/create.htm b/plugins/site21/fields/controllers/fields/create.htm new file mode 100644 index 00000000..e0608618 --- /dev/null +++ b/plugins/site21/fields/controllers/fields/create.htm @@ -0,0 +1,48 @@ + +
      +
    • +
    • pageTitle) ?>
    • +
    + + +fatalError): ?> + + 'layout']) ?> + +
    + formRender() ?> +
    + +
    +
    + + + + or + +
    +
    + + + + + +

    fatalError) ?>

    +

    Return to fields list

    + + diff --git a/plugins/site21/fields/controllers/fields/index.htm b/plugins/site21/fields/controllers/fields/index.htm new file mode 100644 index 00000000..766877d9 --- /dev/null +++ b/plugins/site21/fields/controllers/fields/index.htm @@ -0,0 +1,2 @@ + +listRender() ?> diff --git a/plugins/site21/fields/controllers/fields/preview.htm b/plugins/site21/fields/controllers/fields/preview.htm new file mode 100644 index 00000000..701f259a --- /dev/null +++ b/plugins/site21/fields/controllers/fields/preview.htm @@ -0,0 +1,19 @@ + + + + +fatalError): ?> + +
    + formRenderPreview() ?> +
    + + + +

    fatalError) ?>

    +

    Return to fields list

    + + diff --git a/plugins/site21/fields/controllers/fields/update.htm b/plugins/site21/fields/controllers/fields/update.htm new file mode 100644 index 00000000..a94ac02e --- /dev/null +++ b/plugins/site21/fields/controllers/fields/update.htm @@ -0,0 +1,56 @@ + +
      +
    • +
    • pageTitle) ?>
    • +
    + + +fatalError): ?> + + 'layout']) ?> + +
    + formRender() ?> +
    + +
    +
    + + + + + or + +
    +
    + + + + + +

    fatalError) ?>

    +

    Return to fields list

    + + diff --git a/plugins/site21/fields/lang/en/lang.php b/plugins/site21/fields/lang/en/lang.php new file mode 100644 index 00000000..9244ec02 --- /dev/null +++ b/plugins/site21/fields/lang/en/lang.php @@ -0,0 +1,81 @@ + [ + 'name' => 'Additional fields for Shopaholic', + 'description' => 'Free plugin extending the functionality of the Shopaholic component', + ], + 'menu' => [ + 'label' => 'Fields', + ], + 'form' => [ + 'list_title' => 'Additional fields', + 'create' => 'Create', + 'create_and_close' => 'Create and close', + 'saving' => 'The field is saved', + 'cancel' => 'Cancel', + 'delete_sure' => 'Are you sure you want to delete this field?', + 'deleting' => 'Removed', + 'save' => 'Save', + 'save_and_close' => 'Save and close', + ], + 'btn' => [ + 'new_field' => 'New field', + 'delete_selected' => 'Delete selected', + 'delete_selected_sure' => 'Are you sure you want to delete these fields?', + 'saving' => 'Persists ...', + 'save' => 'Save', + 'save_and_close' => 'Save and Close', + ], + 'fields' => [ + 'name' => 'field', + 'manage' => 'Manage fields', + 'tab' => 'Other', + ], + 'field' => [ + 'name' => 'Field name', + 'slug' => 'Slug', + 'type' => 'Field type', + 'module' => 'Module', + 'options' => 'Options', + 'tab' => 'Tab', + 'active' => 'Active field', + 'comment' => 'Comment', + 'span' => 'Span', + 'size' => 'Size', + 'translatable' => 'Translatable', + 'cached' => 'Cached', + 'inlist' => 'Add column to List widget', + ], + 'type' => [ + 'text' => 'Text', + 'textarea' => 'Textarea', + 'dropdown' => 'Dropdown', + 'checkbox' => 'Checkbox', + 'radio' => 'Radio', + 'number' => 'Integer', + 'email' => 'E-mail', + 'switch' => 'Switch', + ], + 'module' => [ + 'product' => 'Product', + 'offer' => 'Offer', + 'category' => 'Category', + 'brand' => 'Brand', + ], + 'active' => [ + 'on' => 'Yea', + 'off' => 'No', + 'comment' => 'If disabled, it is not displayed in forms', + ], + 'tab' => [ + 'settings' => 'Main settings', + 'additional' => 'Additional', + ], + 'comments' => [ + 'comment' => 'Description, displayed under the field', + 'size' => 'Sets the field size for fields where possible, for example, for textarea', + 'translatable' => 'For Translate plugin', + ], + 'permissions' => [ + 'add' => 'Add additional fields to Shopaholic', + ], +]; \ No newline at end of file diff --git a/plugins/site21/fields/lang/ru/lang.php b/plugins/site21/fields/lang/ru/lang.php new file mode 100644 index 00000000..e40dfa44 --- /dev/null +++ b/plugins/site21/fields/lang/ru/lang.php @@ -0,0 +1,81 @@ + [ + 'name' => 'Дополнительные поля для Shopaholic', + 'description' => 'Бесплатный плагин расширяющий функционал компонента Shopaholic', + ], + 'menu' => [ + 'label' => 'Доп. поля', + ], + 'form' => [ + 'list_title' => 'Дополнительные поля', + 'create' => 'Создать', + 'create_and_close' => 'Создать и закрыть', + 'saving' => 'Поле сохраняется', + 'cancel' => 'Отмена', + 'delete_sure' => 'Уверены что хотите удалить это поле?', + 'deleting' => 'Удаляется', + 'save' => 'Сохранить', + 'save_and_close' => 'Сохранить и закрыть', + ], + 'btn' => [ + 'new_field' => 'Новое поле', + 'delete_selected' => 'Удалить выбранное', + 'delete_selected_sure' => 'Вы уверены что хотите удалить данные поля?', + 'saving' => 'Сохраняется...', + 'save' => 'Сохранить', + 'save_and_close' => 'Сохранить и Закрыть', + ], + 'fields' => [ + 'name' => 'поля', + 'manage' => 'Управление доп. полями', + 'tab' => 'Разное', + ], + 'field' => [ + 'name' => 'Название поля', + 'slug' => 'Слаг', + 'type' => 'Тип поля', + 'module' => 'Модуль', + 'options' => 'Опции', + 'tab' => 'Таб', + 'active' => 'Активное поле', + 'comment' => 'Комментарий', + 'span' => 'Выравнивание', + 'size' => 'Размер', + 'translatable' => 'Переводимый', + 'cached' => 'Кэшируемый', + 'inlist' => 'Добавить колонку в список', + ], + 'type' => [ + 'text' => 'Текстовое поле', + 'textarea' => 'Текстовая область', + 'dropdown' => 'Выпадающий список', + 'checkbox' => 'Чекбокс', + 'radio' => 'Радио', + 'number' => 'Целое число', + 'email' => 'E-mail', + 'switch' => 'Переключатель', + ], + 'module' => [ + 'product' => 'Товар', + 'offer' => 'Торговое предложение', + 'category' => 'Категория', + 'brand' => 'Брэнд', + ], + 'active' => [ + 'on' => 'Да', + 'off' => 'Нет', + 'comment' => 'Если выключено, то не выводится в формах', + ], + 'tab' => [ + 'settings' => 'Настройки', + 'additional' => 'Дополнительные', + ], + 'comments' => [ + 'comment' => 'Описание, выводится под полем', + 'size' => 'Задает размер поля для полей, где это возможно, например, для textarea', + 'translatable' => 'Для плагина Translate', + ], + 'permissions' => [ + 'add' => 'Добалять дополнительные поля в Shopaholic', + ], +]; \ No newline at end of file diff --git a/plugins/site21/fields/models/Field.php b/plugins/site21/fields/models/Field.php new file mode 100644 index 00000000..08c06060 --- /dev/null +++ b/plugins/site21/fields/models/Field.php @@ -0,0 +1,195 @@ + 'required', + 'slug' => 'required|unique:site21_shopaholic_fields', + 'type' => 'required', + 'module' => 'required', + 'tab' => 'required', + ]; + + /** + * @var array Attributes to be cast to native types + */ + protected $casts = []; + + /** + * @var array Attributes to be cast to JSON + */ + protected $jsonable = ['settings']; + + /** + * @var array Attributes to be appended to the API representation of the model (ex. toArray()) + */ + protected $appends = []; + + /** + * @var array Attributes to be removed from the API representation of the model (ex. toArray()) + */ + protected $hidden = []; + + /** + * @var array Attributes to be cast to Argon (Carbon) instances + */ + protected $dates = [ + 'created_at', + 'updated_at' + ]; + + /** + * @var array Relations + */ + public $hasOne = []; + public $hasMany = []; + public $hasOneThrough = []; + public $hasManyThrough = []; + public $belongsTo = []; + public $belongsToMany = []; + public $morphTo = []; + public $morphOne = []; + public $morphMany = []; + public $attachOne = []; + public $attachMany = []; + + + + function getModuleTable($module, $get = FALSE){ + $moduleTable = [ + 'product' => 'lovata_shopaholic_products', + 'offer' => 'lovata_shopaholic_offers', + 'category' => 'lovata_shopaholic_categories', + 'brand' => 'lovata_shopaholic_brands', + ]; + if($get == 'key') { + $obTable = array_keys($moduleTable, $module); + } else { + $obTable = $moduleTable[$module]; + } + return $obTable; + } + + public function afterCreate() + { + $obTable = $this->getModuleTable($this->module); + if (!Schema::hasTable($obTable) || Schema::hasColumn($obTable, $this->slug)) { + return; + } + + Schema::table($obTable, function ($table) { + $type = [ + 'text' => 'string', + 'textarea' => 'text', + 'number' => 'integer', + 'email' => 'string', + 'switch' => 'string', + ]; + $obType = $type[$this->type]; + $table->$obType($this->slug)->nullable(); + }); + } + + + public function afterDelete() + { + Schema::table($this->getModuleTable($this->module), function ($table) { + $table->dropColumn($this->slug); + }); + } + + public function afterUpdate() + { + if ($this->slug != $this->original['slug']) { + Schema::table($this->getModuleTable($this->module), function ($table) { + $table->renameColumn($this->original['slug'], $this->slug); + }); + } + } + + + public function getTypeOptions() { + return [ + 'text' => 'site21.fields::lang.type.text', + 'textarea' => 'site21.fields::lang.type.textarea', + 'number' => 'site21.fields::lang.type.number', + 'email' => 'site21.fields::lang.type.email', + 'switch' => 'site21.fields::lang.type.switch', + ]; + } + + public function getModuleOptions() { + return [ + 'product' => 'site21.fields::lang.module.product', + 'offer' => 'site21.fields::lang.module.offer', + 'category' => 'site21.fields::lang.module.category', + 'brand' => 'site21.fields::lang.module.brand' + ]; + } + + public function getTabOptions($value, $data) + { + $module = isset($data->module) ? $data->module : key($this->getModuleOptions()); + $options = [ + 0 => 'site21.fields::lang.fields.tab', + 'lovata.toolbox::lang.tab.settings' => 'lovata.toolbox::lang.tab.settings', + 'lovata.toolbox::lang.tab.description' => 'lovata.toolbox::lang.tab.description', + 'lovata.toolbox::lang.tab.images' => 'lovata.toolbox::lang.tab.images', + ]; + if($module) { + if ($module == 'product') { + $options['lovata.shopaholic::lang.tab.offer'] = 'lovata.shopaholic::lang.tab.offer'; + } + elseif ($module == 'offer') { + $options['lovata.shopaholic::lang.tab.price'] = 'lovata.shopaholic::lang.tab.price'; + } + } + return $options; + } + + public function getSpanOptions() { + return [ + 'full' => 'full', + 'auto' => 'auto', + 'left' => 'left', + 'right' => 'right' + ]; + } + + public function getSizeOptions() { + return [ + 'tiny' => 'tiny', + 'small' => 'small', + 'large' => 'large', + 'huge' => 'huge', + 'giant' => 'giant' + ]; + } + +} diff --git a/plugins/site21/fields/models/field/columns.yaml b/plugins/site21/fields/models/field/columns.yaml new file mode 100644 index 00000000..11aad9e4 --- /dev/null +++ b/plugins/site21/fields/models/field/columns.yaml @@ -0,0 +1,30 @@ +# =================================== +# List Column Definitions +# =================================== + +columns: + id: + label: ID + searchable: true + + name: + label: 'site21.fields::lang.field.name' + searchable: true + + slug: + label: 'site21.fields::lang.field.slug' + searchable: true + + type: + label: 'site21.fields::lang.field.type' + searchable: false + + module: + label: 'site21.fields::lang.field.module' + searchable: false + + active: + label: 'site21.fields::lang.field.active' + searchable: false + + diff --git a/plugins/site21/fields/models/field/fields.yaml b/plugins/site21/fields/models/field/fields.yaml new file mode 100644 index 00000000..b66b0ba7 --- /dev/null +++ b/plugins/site21/fields/models/field/fields.yaml @@ -0,0 +1,113 @@ +# =================================== +# Form Field Definitions +# =================================== + +fields: + id: + label: ID + disabled: true + hidden: true + + active: + label: 'site21.fields::lang.field.active' + span: left + type: switch + default: 1 + comment: 'site21.fields::lang.active.comment' + on: 'site21.fields::lang.active.on' + off: 'site21.fields::lang.active.off' + + settings[translatable]: + label: 'site21.fields::lang.field.translatable' + span: right + type: switch + default: 0 + tab: 'site21.fields::lang.tab.additional' + comment: 'site21.fields::lang.comments.translatable' + on: 'site21.fields::lang.active.on' + off: 'site21.fields::lang.active.off' + + name: + label: 'site21.fields::lang.field.name' + type: text + span: left + required: 1 + + slug: + label: 'site21.fields::lang.field.slug' + span: right + required: 1 + preset: + field: name + type: slug + type: text + +tabs: + fields: + type: + label: 'site21.fields::lang.field.type' + span: left + required: 1 + type: dropdown + default: text + context: + - create + tab: 'site21.fields::lang.tab.settings' + + + module: + label: 'site21.fields::lang.field.module' + span: right + required: 1 + type: dropdown + default: product + context: + - create + tab: 'site21.fields::lang.tab.settings' + + tab: + label: 'site21.fields::lang.field.tab' + span: auto + type: dropdown + default: 'site21.fields::lang.fields.tab' + dependsOn: module + tab: 'site21.fields::lang.tab.settings' + + comment: + label: 'site21.fields::lang.field.comment' + span: auto + tab: 'site21.fields::lang.tab.additional' + comment: 'site21.fields::lang.comments.comment' + + span: + label: 'site21.fields::lang.field.span' + span: auto + type: dropdown + default: auto + tab: 'site21.fields::lang.tab.additional' + + size: + label: 'site21.fields::lang.field.size' + span: auto + type: dropdown + default: tiny + tab: 'site21.fields::lang.tab.additional' + comment: 'site21.fields::lang.comments.size' + + settings[cached]: + label: 'site21.fields::lang.field.cached' + span: auto + type: switch + default: 1 + tab: 'site21.fields::lang.tab.additional' + on: 'site21.fields::lang.active.on' + off: 'site21.fields::lang.active.off' + + settings[inlist]: + label: 'site21.fields::lang.field.inlist' + span: auto + type: switch + default: 0 + tab: 'site21.fields::lang.tab.additional' + on: 'site21.fields::lang.active.on' + off: 'site21.fields::lang.active.off' \ No newline at end of file diff --git a/plugins/site21/fields/updates/add_module_field.php b/plugins/site21/fields/updates/add_module_field.php new file mode 100644 index 00000000..6c90ac59 --- /dev/null +++ b/plugins/site21/fields/updates/add_module_field.php @@ -0,0 +1,35 @@ +string('module')->default(0); + }); + } + + public function down() + { + if (!Schema::hasTable(self::TABLE_NAME) || !Schema::hasColumn(self::TABLE_NAME, 'module')) { + return; + } + + Schema::table(self::TABLE_NAME, function (Blueprint $obTable) + { + $obTable->dropColumn(['module']); + }); + } +} \ No newline at end of file diff --git a/plugins/site21/fields/updates/add_settings_field.php b/plugins/site21/fields/updates/add_settings_field.php new file mode 100644 index 00000000..91d62b96 --- /dev/null +++ b/plugins/site21/fields/updates/add_settings_field.php @@ -0,0 +1,35 @@ +json('settings')->nullable(); + }); + } + + public function down() + { + if (!Schema::hasTable(self::TABLE_NAME) || !Schema::hasColumn(self::TABLE_NAME, 'settings')) { + return; + } + + Schema::table(self::TABLE_NAME, function (Blueprint $obTable) + { + $obTable->dropColumn(['settings']); + }); + } +} \ No newline at end of file diff --git a/plugins/site21/fields/updates/add_span_size_comment.php b/plugins/site21/fields/updates/add_span_size_comment.php new file mode 100644 index 00000000..b96b8750 --- /dev/null +++ b/plugins/site21/fields/updates/add_span_size_comment.php @@ -0,0 +1,42 @@ +string('span')->nullable(); + $obTable->string('size')->nullable(); + $obTable->string('comment')->nullable(); + }); + } + + public function down() + { + if (!Schema::hasTable(self::TABLE_NAME) || !Schema::hasColumn(self::TABLE_NAME, 'span') || !Schema::hasColumn(self::TABLE_NAME, 'size') || !Schema::hasColumn(self::TABLE_NAME, 'comment')) { + return; + } + + Schema::table(self::TABLE_NAME, function (Blueprint $obTable) + { + $obTable->dropColumn(['span', 'size', 'comment']); + }); + } +} \ No newline at end of file diff --git a/plugins/site21/fields/updates/add_tab_field.php b/plugins/site21/fields/updates/add_tab_field.php new file mode 100644 index 00000000..c59768a5 --- /dev/null +++ b/plugins/site21/fields/updates/add_tab_field.php @@ -0,0 +1,35 @@ +string('tab')->nullable(); + }); + } + + public function down() + { + if (!Schema::hasTable(self::TABLE_NAME) || !Schema::hasColumn(self::TABLE_NAME, 'tab')) { + return; + } + + Schema::table(self::TABLE_NAME, function (Blueprint $obTable) + { + $obTable->dropColumn(['tab']); + }); + } +} \ No newline at end of file diff --git a/plugins/site21/fields/updates/create_fields_table.php b/plugins/site21/fields/updates/create_fields_table.php new file mode 100644 index 00000000..07b2052e --- /dev/null +++ b/plugins/site21/fields/updates/create_fields_table.php @@ -0,0 +1,26 @@ +engine = 'InnoDB'; + $table->increments('id'); + $table->string('name'); + $table->string('slug'); + $table->string('type'); + $table->boolean('active')->default(true); + $table->timestamps(); + }); + } + + public function down() + { + Schema::dropIfExists('site21_shopaholic_fields'); + } +} diff --git a/plugins/site21/fields/updates/version.yaml b/plugins/site21/fields/updates/version.yaml new file mode 100644 index 00000000..337fca79 --- /dev/null +++ b/plugins/site21/fields/updates/version.yaml @@ -0,0 +1,31 @@ +1.0.1: First version of Fields +1.0.2: + - Create table with Fields + - create_fields_table.php +1.0.3: + - Add module field to table + - add_module_field.php +1.0.4: + - Add tab field to table + - add_tab_field. +1.0.5: + - Add span, size and comments fields to table + - add_span_size_comment.php +1.0.5.1: + - Fixed bug in version.yaml +1.0.5.2: + - Fixed bug in version.yaml +1.0.6: + - Fixed bug with tab in version.yaml + - add_tab_field.php +1.0.7: + - Add permission to add +1.0.7.1: + - Error fixing in add_span_size_comment.php +1.0.8: + - Add support for the Translate plugin. Add cached fields + - add_settings_field.php +1.0.9: + - Ability to add columns to List widget +1.0.9.1: + - Fixed one error \ No newline at end of file diff --git a/themes/30coffe/assets/css/bootstrap.min.css b/themes/30coffe/assets/css/bootstrap.min.css new file mode 100644 index 00000000..5d19d826 --- /dev/null +++ b/themes/30coffe/assets/css/bootstrap.min.css @@ -0,0 +1,6 @@ +@charset "UTF-8";/*! + * Bootstrap v5.1.3 (https://getbootstrap.com/) + * Copyright 2011-2021 The Bootstrap Authors + * Copyright 2011-2021 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + */:root{--bs-blue:#0d6efd;--bs-indigo:#6610f2;--bs-purple:#6f42c1;--bs-pink:#d63384;--bs-red:#dc3545;--bs-orange:#fd7e14;--bs-yellow:#ffc107;--bs-green:#198754;--bs-teal:#20c997;--bs-cyan:#0dcaf0;--bs-white:#fff;--bs-gray:#6c757d;--bs-gray-dark:#343a40;--bs-gray-100:#f8f9fa;--bs-gray-200:#e9ecef;--bs-gray-300:#dee2e6;--bs-gray-400:#ced4da;--bs-gray-500:#adb5bd;--bs-gray-600:#6c757d;--bs-gray-700:#495057;--bs-gray-800:#343a40;--bs-gray-900:#212529;--bs-primary:#0d6efd;--bs-secondary:#6c757d;--bs-success:#198754;--bs-info:#0dcaf0;--bs-warning:#ffc107;--bs-danger:#dc3545;--bs-light:#f8f9fa;--bs-dark:#212529;--bs-primary-rgb:13,110,253;--bs-secondary-rgb:108,117,125;--bs-success-rgb:25,135,84;--bs-info-rgb:13,202,240;--bs-warning-rgb:255,193,7;--bs-danger-rgb:220,53,69;--bs-light-rgb:248,249,250;--bs-dark-rgb:33,37,41;--bs-white-rgb:255,255,255;--bs-black-rgb:0,0,0;--bs-body-color-rgb:33,37,41;--bs-body-bg-rgb:255,255,255;--bs-font-sans-serif:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans","Liberation Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--bs-font-monospace:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--bs-gradient:linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));--bs-body-font-family:var(--bs-font-sans-serif);--bs-body-font-size:1rem;--bs-body-font-weight:400;--bs-body-line-height:1.5;--bs-body-color:#212529;--bs-body-bg:#fff}*,::after,::before{box-sizing:border-box}@media (prefers-reduced-motion:no-preference){:root{scroll-behavior:smooth}}body{margin:0;font-family:var(--bs-body-font-family);font-size:var(--bs-body-font-size);font-weight:var(--bs-body-font-weight);line-height:var(--bs-body-line-height);color:var(--bs-body-color);text-align:var(--bs-body-text-align);background-color:var(--bs-body-bg);-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}hr{margin:1rem 0;color:inherit;background-color:currentColor;border:0;opacity:.25}hr:not([size]){height:1px}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem;font-weight:500;line-height:1.2}.h1,h1{font-size:calc(1.375rem + 1.5vw)}@media (min-width:1200px){.h1,h1{font-size:2.5rem}}.h2,h2{font-size:calc(1.325rem + .9vw)}@media (min-width:1200px){.h2,h2{font-size:2rem}}.h3,h3{font-size:calc(1.3rem + .6vw)}@media (min-width:1200px){.h3,h3{font-size:1.75rem}}.h4,h4{font-size:calc(1.275rem + .3vw)}@media (min-width:1200px){.h4,h4{font-size:1.5rem}}.h5,h5{font-size:1.25rem}.h6,h6{font-size:1rem}p{margin-top:0;margin-bottom:1rem}abbr[data-bs-original-title],abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul{padding-left:2rem}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}.small,small{font-size:.875em}.mark,mark{padding:.2em;background-color:#fcf8e3}sub,sup{position:relative;font-size:.75em;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#0d6efd;text-decoration:underline}a:hover{color:#0a58ca}a:not([href]):not([class]),a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:var(--bs-font-monospace);font-size:1em;direction:ltr;unicode-bidi:bidi-override}pre{display:block;margin-top:0;margin-bottom:1rem;overflow:auto;font-size:.875em}pre code{font-size:inherit;color:inherit;word-break:normal}code{font-size:.875em;color:#d63384;word-wrap:break-word}a>code{color:inherit}kbd{padding:.2rem .4rem;font-size:.875em;color:#fff;background-color:#212529;border-radius:.2rem}kbd kbd{padding:0;font-size:1em;font-weight:700}figure{margin:0 0 1rem}img,svg{vertical-align:middle}table{caption-side:bottom;border-collapse:collapse}caption{padding-top:.5rem;padding-bottom:.5rem;color:#6c757d;text-align:left}th{text-align:inherit;text-align:-webkit-match-parent}tbody,td,tfoot,th,thead,tr{border-color:inherit;border-style:solid;border-width:0}label{display:inline-block}button{border-radius:0}button:focus:not(:focus-visible){outline:0}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}select:disabled{opacity:1}[list]::-webkit-calendar-picker-indicator{display:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}::-moz-focus-inner{padding:0;border-style:none}textarea{resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{float:left;width:100%;padding:0;margin-bottom:.5rem;font-size:calc(1.275rem + .3vw);line-height:inherit}@media (min-width:1200px){legend{font-size:1.5rem}}legend+*{clear:left}::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-fields-wrapper,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-text,::-webkit-datetime-edit-year-field{padding:0}::-webkit-inner-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:textfield}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-color-swatch-wrapper{padding:0}::-webkit-file-upload-button{font:inherit}::file-selector-button{font:inherit}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}iframe{border:0}summary{display:list-item;cursor:pointer}progress{vertical-align:baseline}[hidden]{display:none!important}.lead{font-size:1.25rem;font-weight:300}.display-1{font-size:calc(1.625rem + 4.5vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-1{font-size:5rem}}.display-2{font-size:calc(1.575rem + 3.9vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-2{font-size:4.5rem}}.display-3{font-size:calc(1.525rem + 3.3vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-3{font-size:4rem}}.display-4{font-size:calc(1.475rem + 2.7vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-4{font-size:3.5rem}}.display-5{font-size:calc(1.425rem + 2.1vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-5{font-size:3rem}}.display-6{font-size:calc(1.375rem + 1.5vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-6{font-size:2.5rem}}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none}.list-inline-item{display:inline-block}.list-inline-item:not(:last-child){margin-right:.5rem}.initialism{font-size:.875em;text-transform:uppercase}.blockquote{margin-bottom:1rem;font-size:1.25rem}.blockquote>:last-child{margin-bottom:0}.blockquote-footer{margin-top:-1rem;margin-bottom:1rem;font-size:.875em;color:#6c757d}.blockquote-footer::before{content:"— "}.img-fluid{max-width:100%;height:auto}.img-thumbnail{padding:.25rem;background-color:#fff;border:1px solid #dee2e6;border-radius:.25rem;max-width:100%;height:auto}.figure{display:inline-block}.figure-img{margin-bottom:.5rem;line-height:1}.figure-caption{font-size:.875em;color:#6c757d}.container,.container-fluid,.container-lg,.container-md,.container-sm,.container-xl,.container-xxl{width:100%;padding-right:var(--bs-gutter-x,.75rem);padding-left:var(--bs-gutter-x,.75rem);margin-right:auto;margin-left:auto}@media (min-width:576px){.container,.container-sm{max-width:540px}}@media (min-width:768px){.container,.container-md,.container-sm{max-width:720px}}@media (min-width:992px){.container,.container-lg,.container-md,.container-sm{max-width:960px}}@media (min-width:1200px){.container,.container-lg,.container-md,.container-sm,.container-xl{max-width:1140px}}@media (min-width:1400px){.container,.container-lg,.container-md,.container-sm,.container-xl,.container-xxl{max-width:1320px}}.row{--bs-gutter-x:1.5rem;--bs-gutter-y:0;display:flex;flex-wrap:wrap;margin-top:calc(-1 * var(--bs-gutter-y));margin-right:calc(-.5 * var(--bs-gutter-x));margin-left:calc(-.5 * var(--bs-gutter-x))}.row>*{flex-shrink:0;width:100%;max-width:100%;padding-right:calc(var(--bs-gutter-x) * .5);padding-left:calc(var(--bs-gutter-x) * .5);margin-top:var(--bs-gutter-y)}.col{flex:1 0 0%}.row-cols-auto>*{flex:0 0 auto;width:auto}.row-cols-1>*{flex:0 0 auto;width:100%}.row-cols-2>*{flex:0 0 auto;width:50%}.row-cols-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-4>*{flex:0 0 auto;width:25%}.row-cols-5>*{flex:0 0 auto;width:20%}.row-cols-6>*{flex:0 0 auto;width:16.6666666667%}.col-auto{flex:0 0 auto;width:auto}.col-1{flex:0 0 auto;width:8.33333333%}.col-2{flex:0 0 auto;width:16.66666667%}.col-3{flex:0 0 auto;width:25%}.col-4{flex:0 0 auto;width:33.33333333%}.col-5{flex:0 0 auto;width:41.66666667%}.col-6{flex:0 0 auto;width:50%}.col-7{flex:0 0 auto;width:58.33333333%}.col-8{flex:0 0 auto;width:66.66666667%}.col-9{flex:0 0 auto;width:75%}.col-10{flex:0 0 auto;width:83.33333333%}.col-11{flex:0 0 auto;width:91.66666667%}.col-12{flex:0 0 auto;width:100%}.offset-1{margin-left:8.33333333%}.offset-2{margin-left:16.66666667%}.offset-3{margin-left:25%}.offset-4{margin-left:33.33333333%}.offset-5{margin-left:41.66666667%}.offset-6{margin-left:50%}.offset-7{margin-left:58.33333333%}.offset-8{margin-left:66.66666667%}.offset-9{margin-left:75%}.offset-10{margin-left:83.33333333%}.offset-11{margin-left:91.66666667%}.g-0,.gx-0{--bs-gutter-x:0}.g-0,.gy-0{--bs-gutter-y:0}.g-1,.gx-1{--bs-gutter-x:0.25rem}.g-1,.gy-1{--bs-gutter-y:0.25rem}.g-2,.gx-2{--bs-gutter-x:0.5rem}.g-2,.gy-2{--bs-gutter-y:0.5rem}.g-3,.gx-3{--bs-gutter-x:1rem}.g-3,.gy-3{--bs-gutter-y:1rem}.g-4,.gx-4{--bs-gutter-x:1.5rem}.g-4,.gy-4{--bs-gutter-y:1.5rem}.g-5,.gx-5{--bs-gutter-x:3rem}.g-5,.gy-5{--bs-gutter-y:3rem}@media (min-width:576px){.col-sm{flex:1 0 0%}.row-cols-sm-auto>*{flex:0 0 auto;width:auto}.row-cols-sm-1>*{flex:0 0 auto;width:100%}.row-cols-sm-2>*{flex:0 0 auto;width:50%}.row-cols-sm-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-sm-4>*{flex:0 0 auto;width:25%}.row-cols-sm-5>*{flex:0 0 auto;width:20%}.row-cols-sm-6>*{flex:0 0 auto;width:16.6666666667%}.col-sm-auto{flex:0 0 auto;width:auto}.col-sm-1{flex:0 0 auto;width:8.33333333%}.col-sm-2{flex:0 0 auto;width:16.66666667%}.col-sm-3{flex:0 0 auto;width:25%}.col-sm-4{flex:0 0 auto;width:33.33333333%}.col-sm-5{flex:0 0 auto;width:41.66666667%}.col-sm-6{flex:0 0 auto;width:50%}.col-sm-7{flex:0 0 auto;width:58.33333333%}.col-sm-8{flex:0 0 auto;width:66.66666667%}.col-sm-9{flex:0 0 auto;width:75%}.col-sm-10{flex:0 0 auto;width:83.33333333%}.col-sm-11{flex:0 0 auto;width:91.66666667%}.col-sm-12{flex:0 0 auto;width:100%}.offset-sm-0{margin-left:0}.offset-sm-1{margin-left:8.33333333%}.offset-sm-2{margin-left:16.66666667%}.offset-sm-3{margin-left:25%}.offset-sm-4{margin-left:33.33333333%}.offset-sm-5{margin-left:41.66666667%}.offset-sm-6{margin-left:50%}.offset-sm-7{margin-left:58.33333333%}.offset-sm-8{margin-left:66.66666667%}.offset-sm-9{margin-left:75%}.offset-sm-10{margin-left:83.33333333%}.offset-sm-11{margin-left:91.66666667%}.g-sm-0,.gx-sm-0{--bs-gutter-x:0}.g-sm-0,.gy-sm-0{--bs-gutter-y:0}.g-sm-1,.gx-sm-1{--bs-gutter-x:0.25rem}.g-sm-1,.gy-sm-1{--bs-gutter-y:0.25rem}.g-sm-2,.gx-sm-2{--bs-gutter-x:0.5rem}.g-sm-2,.gy-sm-2{--bs-gutter-y:0.5rem}.g-sm-3,.gx-sm-3{--bs-gutter-x:1rem}.g-sm-3,.gy-sm-3{--bs-gutter-y:1rem}.g-sm-4,.gx-sm-4{--bs-gutter-x:1.5rem}.g-sm-4,.gy-sm-4{--bs-gutter-y:1.5rem}.g-sm-5,.gx-sm-5{--bs-gutter-x:3rem}.g-sm-5,.gy-sm-5{--bs-gutter-y:3rem}}@media (min-width:768px){.col-md{flex:1 0 0%}.row-cols-md-auto>*{flex:0 0 auto;width:auto}.row-cols-md-1>*{flex:0 0 auto;width:100%}.row-cols-md-2>*{flex:0 0 auto;width:50%}.row-cols-md-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-md-4>*{flex:0 0 auto;width:25%}.row-cols-md-5>*{flex:0 0 auto;width:20%}.row-cols-md-6>*{flex:0 0 auto;width:16.6666666667%}.col-md-auto{flex:0 0 auto;width:auto}.col-md-1{flex:0 0 auto;width:8.33333333%}.col-md-2{flex:0 0 auto;width:16.66666667%}.col-md-3{flex:0 0 auto;width:25%}.col-md-4{flex:0 0 auto;width:33.33333333%}.col-md-5{flex:0 0 auto;width:41.66666667%}.col-md-6{flex:0 0 auto;width:50%}.col-md-7{flex:0 0 auto;width:58.33333333%}.col-md-8{flex:0 0 auto;width:66.66666667%}.col-md-9{flex:0 0 auto;width:75%}.col-md-10{flex:0 0 auto;width:83.33333333%}.col-md-11{flex:0 0 auto;width:91.66666667%}.col-md-12{flex:0 0 auto;width:100%}.offset-md-0{margin-left:0}.offset-md-1{margin-left:8.33333333%}.offset-md-2{margin-left:16.66666667%}.offset-md-3{margin-left:25%}.offset-md-4{margin-left:33.33333333%}.offset-md-5{margin-left:41.66666667%}.offset-md-6{margin-left:50%}.offset-md-7{margin-left:58.33333333%}.offset-md-8{margin-left:66.66666667%}.offset-md-9{margin-left:75%}.offset-md-10{margin-left:83.33333333%}.offset-md-11{margin-left:91.66666667%}.g-md-0,.gx-md-0{--bs-gutter-x:0}.g-md-0,.gy-md-0{--bs-gutter-y:0}.g-md-1,.gx-md-1{--bs-gutter-x:0.25rem}.g-md-1,.gy-md-1{--bs-gutter-y:0.25rem}.g-md-2,.gx-md-2{--bs-gutter-x:0.5rem}.g-md-2,.gy-md-2{--bs-gutter-y:0.5rem}.g-md-3,.gx-md-3{--bs-gutter-x:1rem}.g-md-3,.gy-md-3{--bs-gutter-y:1rem}.g-md-4,.gx-md-4{--bs-gutter-x:1.5rem}.g-md-4,.gy-md-4{--bs-gutter-y:1.5rem}.g-md-5,.gx-md-5{--bs-gutter-x:3rem}.g-md-5,.gy-md-5{--bs-gutter-y:3rem}}@media (min-width:992px){.col-lg{flex:1 0 0%}.row-cols-lg-auto>*{flex:0 0 auto;width:auto}.row-cols-lg-1>*{flex:0 0 auto;width:100%}.row-cols-lg-2>*{flex:0 0 auto;width:50%}.row-cols-lg-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-lg-4>*{flex:0 0 auto;width:25%}.row-cols-lg-5>*{flex:0 0 auto;width:20%}.row-cols-lg-6>*{flex:0 0 auto;width:16.6666666667%}.col-lg-auto{flex:0 0 auto;width:auto}.col-lg-1{flex:0 0 auto;width:8.33333333%}.col-lg-2{flex:0 0 auto;width:16.66666667%}.col-lg-3{flex:0 0 auto;width:25%}.col-lg-4{flex:0 0 auto;width:33.33333333%}.col-lg-5{flex:0 0 auto;width:41.66666667%}.col-lg-6{flex:0 0 auto;width:50%}.col-lg-7{flex:0 0 auto;width:58.33333333%}.col-lg-8{flex:0 0 auto;width:66.66666667%}.col-lg-9{flex:0 0 auto;width:75%}.col-lg-10{flex:0 0 auto;width:83.33333333%}.col-lg-11{flex:0 0 auto;width:91.66666667%}.col-lg-12{flex:0 0 auto;width:100%}.offset-lg-0{margin-left:0}.offset-lg-1{margin-left:8.33333333%}.offset-lg-2{margin-left:16.66666667%}.offset-lg-3{margin-left:25%}.offset-lg-4{margin-left:33.33333333%}.offset-lg-5{margin-left:41.66666667%}.offset-lg-6{margin-left:50%}.offset-lg-7{margin-left:58.33333333%}.offset-lg-8{margin-left:66.66666667%}.offset-lg-9{margin-left:75%}.offset-lg-10{margin-left:83.33333333%}.offset-lg-11{margin-left:91.66666667%}.g-lg-0,.gx-lg-0{--bs-gutter-x:0}.g-lg-0,.gy-lg-0{--bs-gutter-y:0}.g-lg-1,.gx-lg-1{--bs-gutter-x:0.25rem}.g-lg-1,.gy-lg-1{--bs-gutter-y:0.25rem}.g-lg-2,.gx-lg-2{--bs-gutter-x:0.5rem}.g-lg-2,.gy-lg-2{--bs-gutter-y:0.5rem}.g-lg-3,.gx-lg-3{--bs-gutter-x:1rem}.g-lg-3,.gy-lg-3{--bs-gutter-y:1rem}.g-lg-4,.gx-lg-4{--bs-gutter-x:1.5rem}.g-lg-4,.gy-lg-4{--bs-gutter-y:1.5rem}.g-lg-5,.gx-lg-5{--bs-gutter-x:3rem}.g-lg-5,.gy-lg-5{--bs-gutter-y:3rem}}@media (min-width:1200px){.col-xl{flex:1 0 0%}.row-cols-xl-auto>*{flex:0 0 auto;width:auto}.row-cols-xl-1>*{flex:0 0 auto;width:100%}.row-cols-xl-2>*{flex:0 0 auto;width:50%}.row-cols-xl-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-xl-4>*{flex:0 0 auto;width:25%}.row-cols-xl-5>*{flex:0 0 auto;width:20%}.row-cols-xl-6>*{flex:0 0 auto;width:16.6666666667%}.col-xl-auto{flex:0 0 auto;width:auto}.col-xl-1{flex:0 0 auto;width:8.33333333%}.col-xl-2{flex:0 0 auto;width:16.66666667%}.col-xl-3{flex:0 0 auto;width:25%}.col-xl-4{flex:0 0 auto;width:33.33333333%}.col-xl-5{flex:0 0 auto;width:41.66666667%}.col-xl-6{flex:0 0 auto;width:50%}.col-xl-7{flex:0 0 auto;width:58.33333333%}.col-xl-8{flex:0 0 auto;width:66.66666667%}.col-xl-9{flex:0 0 auto;width:75%}.col-xl-10{flex:0 0 auto;width:83.33333333%}.col-xl-11{flex:0 0 auto;width:91.66666667%}.col-xl-12{flex:0 0 auto;width:100%}.offset-xl-0{margin-left:0}.offset-xl-1{margin-left:8.33333333%}.offset-xl-2{margin-left:16.66666667%}.offset-xl-3{margin-left:25%}.offset-xl-4{margin-left:33.33333333%}.offset-xl-5{margin-left:41.66666667%}.offset-xl-6{margin-left:50%}.offset-xl-7{margin-left:58.33333333%}.offset-xl-8{margin-left:66.66666667%}.offset-xl-9{margin-left:75%}.offset-xl-10{margin-left:83.33333333%}.offset-xl-11{margin-left:91.66666667%}.g-xl-0,.gx-xl-0{--bs-gutter-x:0}.g-xl-0,.gy-xl-0{--bs-gutter-y:0}.g-xl-1,.gx-xl-1{--bs-gutter-x:0.25rem}.g-xl-1,.gy-xl-1{--bs-gutter-y:0.25rem}.g-xl-2,.gx-xl-2{--bs-gutter-x:0.5rem}.g-xl-2,.gy-xl-2{--bs-gutter-y:0.5rem}.g-xl-3,.gx-xl-3{--bs-gutter-x:1rem}.g-xl-3,.gy-xl-3{--bs-gutter-y:1rem}.g-xl-4,.gx-xl-4{--bs-gutter-x:1.5rem}.g-xl-4,.gy-xl-4{--bs-gutter-y:1.5rem}.g-xl-5,.gx-xl-5{--bs-gutter-x:3rem}.g-xl-5,.gy-xl-5{--bs-gutter-y:3rem}}@media (min-width:1400px){.col-xxl{flex:1 0 0%}.row-cols-xxl-auto>*{flex:0 0 auto;width:auto}.row-cols-xxl-1>*{flex:0 0 auto;width:100%}.row-cols-xxl-2>*{flex:0 0 auto;width:50%}.row-cols-xxl-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-xxl-4>*{flex:0 0 auto;width:25%}.row-cols-xxl-5>*{flex:0 0 auto;width:20%}.row-cols-xxl-6>*{flex:0 0 auto;width:16.6666666667%}.col-xxl-auto{flex:0 0 auto;width:auto}.col-xxl-1{flex:0 0 auto;width:8.33333333%}.col-xxl-2{flex:0 0 auto;width:16.66666667%}.col-xxl-3{flex:0 0 auto;width:25%}.col-xxl-4{flex:0 0 auto;width:33.33333333%}.col-xxl-5{flex:0 0 auto;width:41.66666667%}.col-xxl-6{flex:0 0 auto;width:50%}.col-xxl-7{flex:0 0 auto;width:58.33333333%}.col-xxl-8{flex:0 0 auto;width:66.66666667%}.col-xxl-9{flex:0 0 auto;width:75%}.col-xxl-10{flex:0 0 auto;width:83.33333333%}.col-xxl-11{flex:0 0 auto;width:91.66666667%}.col-xxl-12{flex:0 0 auto;width:100%}.offset-xxl-0{margin-left:0}.offset-xxl-1{margin-left:8.33333333%}.offset-xxl-2{margin-left:16.66666667%}.offset-xxl-3{margin-left:25%}.offset-xxl-4{margin-left:33.33333333%}.offset-xxl-5{margin-left:41.66666667%}.offset-xxl-6{margin-left:50%}.offset-xxl-7{margin-left:58.33333333%}.offset-xxl-8{margin-left:66.66666667%}.offset-xxl-9{margin-left:75%}.offset-xxl-10{margin-left:83.33333333%}.offset-xxl-11{margin-left:91.66666667%}.g-xxl-0,.gx-xxl-0{--bs-gutter-x:0}.g-xxl-0,.gy-xxl-0{--bs-gutter-y:0}.g-xxl-1,.gx-xxl-1{--bs-gutter-x:0.25rem}.g-xxl-1,.gy-xxl-1{--bs-gutter-y:0.25rem}.g-xxl-2,.gx-xxl-2{--bs-gutter-x:0.5rem}.g-xxl-2,.gy-xxl-2{--bs-gutter-y:0.5rem}.g-xxl-3,.gx-xxl-3{--bs-gutter-x:1rem}.g-xxl-3,.gy-xxl-3{--bs-gutter-y:1rem}.g-xxl-4,.gx-xxl-4{--bs-gutter-x:1.5rem}.g-xxl-4,.gy-xxl-4{--bs-gutter-y:1.5rem}.g-xxl-5,.gx-xxl-5{--bs-gutter-x:3rem}.g-xxl-5,.gy-xxl-5{--bs-gutter-y:3rem}}.table{--bs-table-bg:transparent;--bs-table-accent-bg:transparent;--bs-table-striped-color:#212529;--bs-table-striped-bg:rgba(0, 0, 0, 0.05);--bs-table-active-color:#212529;--bs-table-active-bg:rgba(0, 0, 0, 0.1);--bs-table-hover-color:#212529;--bs-table-hover-bg:rgba(0, 0, 0, 0.075);width:100%;margin-bottom:1rem;color:#212529;vertical-align:top;border-color:#dee2e6}.table>:not(caption)>*>*{padding:.5rem .5rem;background-color:var(--bs-table-bg);border-bottom-width:1px;box-shadow:inset 0 0 0 9999px var(--bs-table-accent-bg)}.table>tbody{vertical-align:inherit}.table>thead{vertical-align:bottom}.table>:not(:first-child){border-top:2px solid currentColor}.caption-top{caption-side:top}.table-sm>:not(caption)>*>*{padding:.25rem .25rem}.table-bordered>:not(caption)>*{border-width:1px 0}.table-bordered>:not(caption)>*>*{border-width:0 1px}.table-borderless>:not(caption)>*>*{border-bottom-width:0}.table-borderless>:not(:first-child){border-top-width:0}.table-striped>tbody>tr:nth-of-type(odd)>*{--bs-table-accent-bg:var(--bs-table-striped-bg);color:var(--bs-table-striped-color)}.table-active{--bs-table-accent-bg:var(--bs-table-active-bg);color:var(--bs-table-active-color)}.table-hover>tbody>tr:hover>*{--bs-table-accent-bg:var(--bs-table-hover-bg);color:var(--bs-table-hover-color)}.table-primary{--bs-table-bg:#cfe2ff;--bs-table-striped-bg:#c5d7f2;--bs-table-striped-color:#000;--bs-table-active-bg:#bacbe6;--bs-table-active-color:#000;--bs-table-hover-bg:#bfd1ec;--bs-table-hover-color:#000;color:#000;border-color:#bacbe6}.table-secondary{--bs-table-bg:#e2e3e5;--bs-table-striped-bg:#d7d8da;--bs-table-striped-color:#000;--bs-table-active-bg:#cbccce;--bs-table-active-color:#000;--bs-table-hover-bg:#d1d2d4;--bs-table-hover-color:#000;color:#000;border-color:#cbccce}.table-success{--bs-table-bg:#d1e7dd;--bs-table-striped-bg:#c7dbd2;--bs-table-striped-color:#000;--bs-table-active-bg:#bcd0c7;--bs-table-active-color:#000;--bs-table-hover-bg:#c1d6cc;--bs-table-hover-color:#000;color:#000;border-color:#bcd0c7}.table-info{--bs-table-bg:#cff4fc;--bs-table-striped-bg:#c5e8ef;--bs-table-striped-color:#000;--bs-table-active-bg:#badce3;--bs-table-active-color:#000;--bs-table-hover-bg:#bfe2e9;--bs-table-hover-color:#000;color:#000;border-color:#badce3}.table-warning{--bs-table-bg:#fff3cd;--bs-table-striped-bg:#f2e7c3;--bs-table-striped-color:#000;--bs-table-active-bg:#e6dbb9;--bs-table-active-color:#000;--bs-table-hover-bg:#ece1be;--bs-table-hover-color:#000;color:#000;border-color:#e6dbb9}.table-danger{--bs-table-bg:#f8d7da;--bs-table-striped-bg:#eccccf;--bs-table-striped-color:#000;--bs-table-active-bg:#dfc2c4;--bs-table-active-color:#000;--bs-table-hover-bg:#e5c7ca;--bs-table-hover-color:#000;color:#000;border-color:#dfc2c4}.table-light{--bs-table-bg:#f8f9fa;--bs-table-striped-bg:#ecedee;--bs-table-striped-color:#000;--bs-table-active-bg:#dfe0e1;--bs-table-active-color:#000;--bs-table-hover-bg:#e5e6e7;--bs-table-hover-color:#000;color:#000;border-color:#dfe0e1}.table-dark{--bs-table-bg:#212529;--bs-table-striped-bg:#2c3034;--bs-table-striped-color:#fff;--bs-table-active-bg:#373b3e;--bs-table-active-color:#fff;--bs-table-hover-bg:#323539;--bs-table-hover-color:#fff;color:#fff;border-color:#373b3e}.table-responsive{overflow-x:auto;-webkit-overflow-scrolling:touch}@media (max-width:575.98px){.table-responsive-sm{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:767.98px){.table-responsive-md{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:991.98px){.table-responsive-lg{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:1199.98px){.table-responsive-xl{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:1399.98px){.table-responsive-xxl{overflow-x:auto;-webkit-overflow-scrolling:touch}}.form-label{margin-bottom:.5rem}.col-form-label{padding-top:calc(.375rem + 1px);padding-bottom:calc(.375rem + 1px);margin-bottom:0;font-size:inherit;line-height:1.5}.col-form-label-lg{padding-top:calc(.5rem + 1px);padding-bottom:calc(.5rem + 1px);font-size:1.25rem}.col-form-label-sm{padding-top:calc(.25rem + 1px);padding-bottom:calc(.25rem + 1px);font-size:.875rem}.form-text{margin-top:.25rem;font-size:.875em;color:#6c757d}.form-control{display:block;width:100%;padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#212529;background-color:#fff;background-clip:padding-box;border:1px solid #ced4da;-webkit-appearance:none;-moz-appearance:none;appearance:none;border-radius:.25rem;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control{transition:none}}.form-control[type=file]{overflow:hidden}.form-control[type=file]:not(:disabled):not([readonly]){cursor:pointer}.form-control:focus{color:#212529;background-color:#fff;border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.form-control::-webkit-date-and-time-value{height:1.5em}.form-control::-moz-placeholder{color:#6c757d;opacity:1}.form-control::placeholder{color:#6c757d;opacity:1}.form-control:disabled,.form-control[readonly]{background-color:#e9ecef;opacity:1}.form-control::-webkit-file-upload-button{padding:.375rem .75rem;margin:-.375rem -.75rem;-webkit-margin-end:.75rem;margin-inline-end:.75rem;color:#212529;background-color:#e9ecef;pointer-events:none;border-color:inherit;border-style:solid;border-width:0;border-inline-end-width:1px;border-radius:0;-webkit-transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}.form-control::file-selector-button{padding:.375rem .75rem;margin:-.375rem -.75rem;-webkit-margin-end:.75rem;margin-inline-end:.75rem;color:#212529;background-color:#e9ecef;pointer-events:none;border-color:inherit;border-style:solid;border-width:0;border-inline-end-width:1px;border-radius:0;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control::-webkit-file-upload-button{-webkit-transition:none;transition:none}.form-control::file-selector-button{transition:none}}.form-control:hover:not(:disabled):not([readonly])::-webkit-file-upload-button{background-color:#dde0e3}.form-control:hover:not(:disabled):not([readonly])::file-selector-button{background-color:#dde0e3}.form-control::-webkit-file-upload-button{padding:.375rem .75rem;margin:-.375rem -.75rem;-webkit-margin-end:.75rem;margin-inline-end:.75rem;color:#212529;background-color:#e9ecef;pointer-events:none;border-color:inherit;border-style:solid;border-width:0;border-inline-end-width:1px;border-radius:0;-webkit-transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control::-webkit-file-upload-button{-webkit-transition:none;transition:none}}.form-control:hover:not(:disabled):not([readonly])::-webkit-file-upload-button{background-color:#dde0e3}.form-control-plaintext{display:block;width:100%;padding:.375rem 0;margin-bottom:0;line-height:1.5;color:#212529;background-color:transparent;border:solid transparent;border-width:1px 0}.form-control-plaintext.form-control-lg,.form-control-plaintext.form-control-sm{padding-right:0;padding-left:0}.form-control-sm{min-height:calc(1.5em + .5rem + 2px);padding:.25rem .5rem;font-size:.875rem;border-radius:.2rem}.form-control-sm::-webkit-file-upload-button{padding:.25rem .5rem;margin:-.25rem -.5rem;-webkit-margin-end:.5rem;margin-inline-end:.5rem}.form-control-sm::file-selector-button{padding:.25rem .5rem;margin:-.25rem -.5rem;-webkit-margin-end:.5rem;margin-inline-end:.5rem}.form-control-sm::-webkit-file-upload-button{padding:.25rem .5rem;margin:-.25rem -.5rem;-webkit-margin-end:.5rem;margin-inline-end:.5rem}.form-control-lg{min-height:calc(1.5em + 1rem + 2px);padding:.5rem 1rem;font-size:1.25rem;border-radius:.3rem}.form-control-lg::-webkit-file-upload-button{padding:.5rem 1rem;margin:-.5rem -1rem;-webkit-margin-end:1rem;margin-inline-end:1rem}.form-control-lg::file-selector-button{padding:.5rem 1rem;margin:-.5rem -1rem;-webkit-margin-end:1rem;margin-inline-end:1rem}.form-control-lg::-webkit-file-upload-button{padding:.5rem 1rem;margin:-.5rem -1rem;-webkit-margin-end:1rem;margin-inline-end:1rem}textarea.form-control{min-height:calc(1.5em + .75rem + 2px)}textarea.form-control-sm{min-height:calc(1.5em + .5rem + 2px)}textarea.form-control-lg{min-height:calc(1.5em + 1rem + 2px)}.form-control-color{width:3rem;height:auto;padding:.375rem}.form-control-color:not(:disabled):not([readonly]){cursor:pointer}.form-control-color::-moz-color-swatch{height:1.5em;border-radius:.25rem}.form-control-color::-webkit-color-swatch{height:1.5em;border-radius:.25rem}.form-select{display:block;width:100%;padding:.375rem 2.25rem .375rem .75rem;-moz-padding-start:calc(0.75rem - 3px);font-size:1rem;font-weight:400;line-height:1.5;color:#212529;background-color:#fff;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right .75rem center;background-size:16px 12px;border:1px solid #ced4da;border-radius:.25rem;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out;-webkit-appearance:none;-moz-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.form-select{transition:none}}.form-select:focus{border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.form-select[multiple],.form-select[size]:not([size="1"]){padding-right:.75rem;background-image:none}.form-select:disabled{background-color:#e9ecef}.form-select:-moz-focusring{color:transparent;text-shadow:0 0 0 #212529}.form-select-sm{padding-top:.25rem;padding-bottom:.25rem;padding-left:.5rem;font-size:.875rem;border-radius:.2rem}.form-select-lg{padding-top:.5rem;padding-bottom:.5rem;padding-left:1rem;font-size:1.25rem;border-radius:.3rem}.form-check{display:block;min-height:1.5rem;padding-left:1.5em;margin-bottom:.125rem}.form-check .form-check-input{float:left;margin-left:-1.5em}.form-check-input{width:1em;height:1em;margin-top:.25em;vertical-align:top;background-color:#fff;background-repeat:no-repeat;background-position:center;background-size:contain;border:1px solid rgba(0,0,0,.25);-webkit-appearance:none;-moz-appearance:none;appearance:none;-webkit-print-color-adjust:exact;color-adjust:exact}.form-check-input[type=checkbox]{border-radius:.25em}.form-check-input[type=radio]{border-radius:50%}.form-check-input:active{filter:brightness(90%)}.form-check-input:focus{border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.form-check-input:checked{background-color:#0d6efd;border-color:#0d6efd}.form-check-input:checked[type=checkbox]{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10l3 3l6-6'/%3e%3c/svg%3e")}.form-check-input:checked[type=radio]{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='2' fill='%23fff'/%3e%3c/svg%3e")}.form-check-input[type=checkbox]:indeterminate{background-color:#0d6efd;border-color:#0d6efd;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10h8'/%3e%3c/svg%3e")}.form-check-input:disabled{pointer-events:none;filter:none;opacity:.5}.form-check-input:disabled~.form-check-label,.form-check-input[disabled]~.form-check-label{opacity:.5}.form-switch{padding-left:2.5em}.form-switch .form-check-input{width:2em;margin-left:-2.5em;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%280, 0, 0, 0.25%29'/%3e%3c/svg%3e");background-position:left center;border-radius:2em;transition:background-position .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-switch .form-check-input{transition:none}}.form-switch .form-check-input:focus{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%2386b7fe'/%3e%3c/svg%3e")}.form-switch .form-check-input:checked{background-position:right center;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e")}.form-check-inline{display:inline-block;margin-right:1rem}.btn-check{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.btn-check:disabled+.btn,.btn-check[disabled]+.btn{pointer-events:none;filter:none;opacity:.65}.form-range{width:100%;height:1.5rem;padding:0;background-color:transparent;-webkit-appearance:none;-moz-appearance:none;appearance:none}.form-range:focus{outline:0}.form-range:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .25rem rgba(13,110,253,.25)}.form-range:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .25rem rgba(13,110,253,.25)}.form-range::-moz-focus-outer{border:0}.form-range::-webkit-slider-thumb{width:1rem;height:1rem;margin-top:-.25rem;background-color:#0d6efd;border:0;border-radius:1rem;-webkit-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-webkit-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.form-range::-webkit-slider-thumb{-webkit-transition:none;transition:none}}.form-range::-webkit-slider-thumb:active{background-color:#b6d4fe}.form-range::-webkit-slider-runnable-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.form-range::-moz-range-thumb{width:1rem;height:1rem;background-color:#0d6efd;border:0;border-radius:1rem;-moz-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-moz-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.form-range::-moz-range-thumb{-moz-transition:none;transition:none}}.form-range::-moz-range-thumb:active{background-color:#b6d4fe}.form-range::-moz-range-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.form-range:disabled{pointer-events:none}.form-range:disabled::-webkit-slider-thumb{background-color:#adb5bd}.form-range:disabled::-moz-range-thumb{background-color:#adb5bd}.form-floating{position:relative}.form-floating>.form-control,.form-floating>.form-select{height:calc(3.5rem + 2px);line-height:1.25}.form-floating>label{position:absolute;top:0;left:0;height:100%;padding:1rem .75rem;pointer-events:none;border:1px solid transparent;transform-origin:0 0;transition:opacity .1s ease-in-out,transform .1s ease-in-out}@media (prefers-reduced-motion:reduce){.form-floating>label{transition:none}}.form-floating>.form-control{padding:1rem .75rem}.form-floating>.form-control::-moz-placeholder{color:transparent}.form-floating>.form-control::placeholder{color:transparent}.form-floating>.form-control:not(:-moz-placeholder-shown){padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control:focus,.form-floating>.form-control:not(:placeholder-shown){padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control:-webkit-autofill{padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-select{padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control:not(:-moz-placeholder-shown)~label{opacity:.65;transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.form-floating>.form-control:focus~label,.form-floating>.form-control:not(:placeholder-shown)~label,.form-floating>.form-select~label{opacity:.65;transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.form-floating>.form-control:-webkit-autofill~label{opacity:.65;transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.input-group{position:relative;display:flex;flex-wrap:wrap;align-items:stretch;width:100%}.input-group>.form-control,.input-group>.form-select{position:relative;flex:1 1 auto;width:1%;min-width:0}.input-group>.form-control:focus,.input-group>.form-select:focus{z-index:3}.input-group .btn{position:relative;z-index:2}.input-group .btn:focus{z-index:3}.input-group-text{display:flex;align-items:center;padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:center;white-space:nowrap;background-color:#e9ecef;border:1px solid #ced4da;border-radius:.25rem}.input-group-lg>.btn,.input-group-lg>.form-control,.input-group-lg>.form-select,.input-group-lg>.input-group-text{padding:.5rem 1rem;font-size:1.25rem;border-radius:.3rem}.input-group-sm>.btn,.input-group-sm>.form-control,.input-group-sm>.form-select,.input-group-sm>.input-group-text{padding:.25rem .5rem;font-size:.875rem;border-radius:.2rem}.input-group-lg>.form-select,.input-group-sm>.form-select{padding-right:3rem}.input-group:not(.has-validation)>.dropdown-toggle:nth-last-child(n+3),.input-group:not(.has-validation)>:not(:last-child):not(.dropdown-toggle):not(.dropdown-menu){border-top-right-radius:0;border-bottom-right-radius:0}.input-group.has-validation>.dropdown-toggle:nth-last-child(n+4),.input-group.has-validation>:nth-last-child(n+3):not(.dropdown-toggle):not(.dropdown-menu){border-top-right-radius:0;border-bottom-right-radius:0}.input-group>:not(:first-child):not(.dropdown-menu):not(.valid-tooltip):not(.valid-feedback):not(.invalid-tooltip):not(.invalid-feedback){margin-left:-1px;border-top-left-radius:0;border-bottom-left-radius:0}.valid-feedback{display:none;width:100%;margin-top:.25rem;font-size:.875em;color:#198754}.valid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;color:#fff;background-color:rgba(25,135,84,.9);border-radius:.25rem}.is-valid~.valid-feedback,.is-valid~.valid-tooltip,.was-validated :valid~.valid-feedback,.was-validated :valid~.valid-tooltip{display:block}.form-control.is-valid,.was-validated .form-control:valid{border-color:#198754;padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-valid:focus,.was-validated .form-control:valid:focus{border-color:#198754;box-shadow:0 0 0 .25rem rgba(25,135,84,.25)}.was-validated textarea.form-control:valid,textarea.form-control.is-valid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.form-select.is-valid,.was-validated .form-select:valid{border-color:#198754}.form-select.is-valid:not([multiple]):not([size]),.form-select.is-valid:not([multiple])[size="1"],.was-validated .form-select:valid:not([multiple]):not([size]),.was-validated .form-select:valid:not([multiple])[size="1"]{padding-right:4.125rem;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e"),url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");background-position:right .75rem center,center right 2.25rem;background-size:16px 12px,calc(.75em + .375rem) calc(.75em + .375rem)}.form-select.is-valid:focus,.was-validated .form-select:valid:focus{border-color:#198754;box-shadow:0 0 0 .25rem rgba(25,135,84,.25)}.form-check-input.is-valid,.was-validated .form-check-input:valid{border-color:#198754}.form-check-input.is-valid:checked,.was-validated .form-check-input:valid:checked{background-color:#198754}.form-check-input.is-valid:focus,.was-validated .form-check-input:valid:focus{box-shadow:0 0 0 .25rem rgba(25,135,84,.25)}.form-check-input.is-valid~.form-check-label,.was-validated .form-check-input:valid~.form-check-label{color:#198754}.form-check-inline .form-check-input~.valid-feedback{margin-left:.5em}.input-group .form-control.is-valid,.input-group .form-select.is-valid,.was-validated .input-group .form-control:valid,.was-validated .input-group .form-select:valid{z-index:1}.input-group .form-control.is-valid:focus,.input-group .form-select.is-valid:focus,.was-validated .input-group .form-control:valid:focus,.was-validated .input-group .form-select:valid:focus{z-index:3}.invalid-feedback{display:none;width:100%;margin-top:.25rem;font-size:.875em;color:#dc3545}.invalid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;color:#fff;background-color:rgba(220,53,69,.9);border-radius:.25rem}.is-invalid~.invalid-feedback,.is-invalid~.invalid-tooltip,.was-validated :invalid~.invalid-feedback,.was-validated :invalid~.invalid-tooltip{display:block}.form-control.is-invalid,.was-validated .form-control:invalid{border-color:#dc3545;padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-invalid:focus,.was-validated .form-control:invalid:focus{border-color:#dc3545;box-shadow:0 0 0 .25rem rgba(220,53,69,.25)}.was-validated textarea.form-control:invalid,textarea.form-control.is-invalid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.form-select.is-invalid,.was-validated .form-select:invalid{border-color:#dc3545}.form-select.is-invalid:not([multiple]):not([size]),.form-select.is-invalid:not([multiple])[size="1"],.was-validated .form-select:invalid:not([multiple]):not([size]),.was-validated .form-select:invalid:not([multiple])[size="1"]{padding-right:4.125rem;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e"),url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e");background-position:right .75rem center,center right 2.25rem;background-size:16px 12px,calc(.75em + .375rem) calc(.75em + .375rem)}.form-select.is-invalid:focus,.was-validated .form-select:invalid:focus{border-color:#dc3545;box-shadow:0 0 0 .25rem rgba(220,53,69,.25)}.form-check-input.is-invalid,.was-validated .form-check-input:invalid{border-color:#dc3545}.form-check-input.is-invalid:checked,.was-validated .form-check-input:invalid:checked{background-color:#dc3545}.form-check-input.is-invalid:focus,.was-validated .form-check-input:invalid:focus{box-shadow:0 0 0 .25rem rgba(220,53,69,.25)}.form-check-input.is-invalid~.form-check-label,.was-validated .form-check-input:invalid~.form-check-label{color:#dc3545}.form-check-inline .form-check-input~.invalid-feedback{margin-left:.5em}.input-group .form-control.is-invalid,.input-group .form-select.is-invalid,.was-validated .input-group .form-control:invalid,.was-validated .input-group .form-select:invalid{z-index:2}.input-group .form-control.is-invalid:focus,.input-group .form-select.is-invalid:focus,.was-validated .input-group .form-control:invalid:focus,.was-validated .input-group .form-select:invalid:focus{z-index:3}.btn{display:inline-block;font-weight:400;line-height:1.5;color:#212529;text-align:center;text-decoration:none;vertical-align:middle;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;user-select:none;background-color:transparent;border:1px solid transparent;padding:.375rem .75rem;font-size:1rem;border-radius:.25rem;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.btn{transition:none}}.btn:hover{color:#212529}.btn-check:focus+.btn,.btn:focus{outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.btn.disabled,.btn:disabled,fieldset:disabled .btn{pointer-events:none;opacity:.65}.btn-primary{color:#fff;background-color:#0d6efd;border-color:#0d6efd}.btn-primary:hover{color:#fff;background-color:#0b5ed7;border-color:#0a58ca}.btn-check:focus+.btn-primary,.btn-primary:focus{color:#fff;background-color:#0b5ed7;border-color:#0a58ca;box-shadow:0 0 0 .25rem rgba(49,132,253,.5)}.btn-check:active+.btn-primary,.btn-check:checked+.btn-primary,.btn-primary.active,.btn-primary:active,.show>.btn-primary.dropdown-toggle{color:#fff;background-color:#0a58ca;border-color:#0a53be}.btn-check:active+.btn-primary:focus,.btn-check:checked+.btn-primary:focus,.btn-primary.active:focus,.btn-primary:active:focus,.show>.btn-primary.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(49,132,253,.5)}.btn-primary.disabled,.btn-primary:disabled{color:#fff;background-color:#0d6efd;border-color:#0d6efd}.btn-secondary{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-secondary:hover{color:#fff;background-color:#5c636a;border-color:#565e64}.btn-check:focus+.btn-secondary,.btn-secondary:focus{color:#fff;background-color:#5c636a;border-color:#565e64;box-shadow:0 0 0 .25rem rgba(130,138,145,.5)}.btn-check:active+.btn-secondary,.btn-check:checked+.btn-secondary,.btn-secondary.active,.btn-secondary:active,.show>.btn-secondary.dropdown-toggle{color:#fff;background-color:#565e64;border-color:#51585e}.btn-check:active+.btn-secondary:focus,.btn-check:checked+.btn-secondary:focus,.btn-secondary.active:focus,.btn-secondary:active:focus,.show>.btn-secondary.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(130,138,145,.5)}.btn-secondary.disabled,.btn-secondary:disabled{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-success{color:#fff;background-color:#198754;border-color:#198754}.btn-success:hover{color:#fff;background-color:#157347;border-color:#146c43}.btn-check:focus+.btn-success,.btn-success:focus{color:#fff;background-color:#157347;border-color:#146c43;box-shadow:0 0 0 .25rem rgba(60,153,110,.5)}.btn-check:active+.btn-success,.btn-check:checked+.btn-success,.btn-success.active,.btn-success:active,.show>.btn-success.dropdown-toggle{color:#fff;background-color:#146c43;border-color:#13653f}.btn-check:active+.btn-success:focus,.btn-check:checked+.btn-success:focus,.btn-success.active:focus,.btn-success:active:focus,.show>.btn-success.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(60,153,110,.5)}.btn-success.disabled,.btn-success:disabled{color:#fff;background-color:#198754;border-color:#198754}.btn-info{color:#000;background-color:#0dcaf0;border-color:#0dcaf0}.btn-info:hover{color:#000;background-color:#31d2f2;border-color:#25cff2}.btn-check:focus+.btn-info,.btn-info:focus{color:#000;background-color:#31d2f2;border-color:#25cff2;box-shadow:0 0 0 .25rem rgba(11,172,204,.5)}.btn-check:active+.btn-info,.btn-check:checked+.btn-info,.btn-info.active,.btn-info:active,.show>.btn-info.dropdown-toggle{color:#000;background-color:#3dd5f3;border-color:#25cff2}.btn-check:active+.btn-info:focus,.btn-check:checked+.btn-info:focus,.btn-info.active:focus,.btn-info:active:focus,.show>.btn-info.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(11,172,204,.5)}.btn-info.disabled,.btn-info:disabled{color:#000;background-color:#0dcaf0;border-color:#0dcaf0}.btn-warning{color:#000;background-color:#ffc107;border-color:#ffc107}.btn-warning:hover{color:#000;background-color:#ffca2c;border-color:#ffc720}.btn-check:focus+.btn-warning,.btn-warning:focus{color:#000;background-color:#ffca2c;border-color:#ffc720;box-shadow:0 0 0 .25rem rgba(217,164,6,.5)}.btn-check:active+.btn-warning,.btn-check:checked+.btn-warning,.btn-warning.active,.btn-warning:active,.show>.btn-warning.dropdown-toggle{color:#000;background-color:#ffcd39;border-color:#ffc720}.btn-check:active+.btn-warning:focus,.btn-check:checked+.btn-warning:focus,.btn-warning.active:focus,.btn-warning:active:focus,.show>.btn-warning.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(217,164,6,.5)}.btn-warning.disabled,.btn-warning:disabled{color:#000;background-color:#ffc107;border-color:#ffc107}.btn-danger{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-danger:hover{color:#fff;background-color:#bb2d3b;border-color:#b02a37}.btn-check:focus+.btn-danger,.btn-danger:focus{color:#fff;background-color:#bb2d3b;border-color:#b02a37;box-shadow:0 0 0 .25rem rgba(225,83,97,.5)}.btn-check:active+.btn-danger,.btn-check:checked+.btn-danger,.btn-danger.active,.btn-danger:active,.show>.btn-danger.dropdown-toggle{color:#fff;background-color:#b02a37;border-color:#a52834}.btn-check:active+.btn-danger:focus,.btn-check:checked+.btn-danger:focus,.btn-danger.active:focus,.btn-danger:active:focus,.show>.btn-danger.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(225,83,97,.5)}.btn-danger.disabled,.btn-danger:disabled{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-light{color:#000;background-color:#f8f9fa;border-color:#f8f9fa}.btn-light:hover{color:#000;background-color:#f9fafb;border-color:#f9fafb}.btn-check:focus+.btn-light,.btn-light:focus{color:#000;background-color:#f9fafb;border-color:#f9fafb;box-shadow:0 0 0 .25rem rgba(211,212,213,.5)}.btn-check:active+.btn-light,.btn-check:checked+.btn-light,.btn-light.active,.btn-light:active,.show>.btn-light.dropdown-toggle{color:#000;background-color:#f9fafb;border-color:#f9fafb}.btn-check:active+.btn-light:focus,.btn-check:checked+.btn-light:focus,.btn-light.active:focus,.btn-light:active:focus,.show>.btn-light.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(211,212,213,.5)}.btn-light.disabled,.btn-light:disabled{color:#000;background-color:#f8f9fa;border-color:#f8f9fa}.btn-dark{color:#fff;background-color:#212529;border-color:#212529}.btn-dark:hover{color:#fff;background-color:#1c1f23;border-color:#1a1e21}.btn-check:focus+.btn-dark,.btn-dark:focus{color:#fff;background-color:#1c1f23;border-color:#1a1e21;box-shadow:0 0 0 .25rem rgba(66,70,73,.5)}.btn-check:active+.btn-dark,.btn-check:checked+.btn-dark,.btn-dark.active,.btn-dark:active,.show>.btn-dark.dropdown-toggle{color:#fff;background-color:#1a1e21;border-color:#191c1f}.btn-check:active+.btn-dark:focus,.btn-check:checked+.btn-dark:focus,.btn-dark.active:focus,.btn-dark:active:focus,.show>.btn-dark.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(66,70,73,.5)}.btn-dark.disabled,.btn-dark:disabled{color:#fff;background-color:#212529;border-color:#212529}.btn-outline-primary{color:#0d6efd;border-color:#0d6efd}.btn-outline-primary:hover{color:#fff;background-color:#0d6efd;border-color:#0d6efd}.btn-check:focus+.btn-outline-primary,.btn-outline-primary:focus{box-shadow:0 0 0 .25rem rgba(13,110,253,.5)}.btn-check:active+.btn-outline-primary,.btn-check:checked+.btn-outline-primary,.btn-outline-primary.active,.btn-outline-primary.dropdown-toggle.show,.btn-outline-primary:active{color:#fff;background-color:#0d6efd;border-color:#0d6efd}.btn-check:active+.btn-outline-primary:focus,.btn-check:checked+.btn-outline-primary:focus,.btn-outline-primary.active:focus,.btn-outline-primary.dropdown-toggle.show:focus,.btn-outline-primary:active:focus{box-shadow:0 0 0 .25rem rgba(13,110,253,.5)}.btn-outline-primary.disabled,.btn-outline-primary:disabled{color:#0d6efd;background-color:transparent}.btn-outline-secondary{color:#6c757d;border-color:#6c757d}.btn-outline-secondary:hover{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-check:focus+.btn-outline-secondary,.btn-outline-secondary:focus{box-shadow:0 0 0 .25rem rgba(108,117,125,.5)}.btn-check:active+.btn-outline-secondary,.btn-check:checked+.btn-outline-secondary,.btn-outline-secondary.active,.btn-outline-secondary.dropdown-toggle.show,.btn-outline-secondary:active{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-check:active+.btn-outline-secondary:focus,.btn-check:checked+.btn-outline-secondary:focus,.btn-outline-secondary.active:focus,.btn-outline-secondary.dropdown-toggle.show:focus,.btn-outline-secondary:active:focus{box-shadow:0 0 0 .25rem rgba(108,117,125,.5)}.btn-outline-secondary.disabled,.btn-outline-secondary:disabled{color:#6c757d;background-color:transparent}.btn-outline-success{color:#198754;border-color:#198754}.btn-outline-success:hover{color:#fff;background-color:#198754;border-color:#198754}.btn-check:focus+.btn-outline-success,.btn-outline-success:focus{box-shadow:0 0 0 .25rem rgba(25,135,84,.5)}.btn-check:active+.btn-outline-success,.btn-check:checked+.btn-outline-success,.btn-outline-success.active,.btn-outline-success.dropdown-toggle.show,.btn-outline-success:active{color:#fff;background-color:#198754;border-color:#198754}.btn-check:active+.btn-outline-success:focus,.btn-check:checked+.btn-outline-success:focus,.btn-outline-success.active:focus,.btn-outline-success.dropdown-toggle.show:focus,.btn-outline-success:active:focus{box-shadow:0 0 0 .25rem rgba(25,135,84,.5)}.btn-outline-success.disabled,.btn-outline-success:disabled{color:#198754;background-color:transparent}.btn-outline-info{color:#0dcaf0;border-color:#0dcaf0}.btn-outline-info:hover{color:#000;background-color:#0dcaf0;border-color:#0dcaf0}.btn-check:focus+.btn-outline-info,.btn-outline-info:focus{box-shadow:0 0 0 .25rem rgba(13,202,240,.5)}.btn-check:active+.btn-outline-info,.btn-check:checked+.btn-outline-info,.btn-outline-info.active,.btn-outline-info.dropdown-toggle.show,.btn-outline-info:active{color:#000;background-color:#0dcaf0;border-color:#0dcaf0}.btn-check:active+.btn-outline-info:focus,.btn-check:checked+.btn-outline-info:focus,.btn-outline-info.active:focus,.btn-outline-info.dropdown-toggle.show:focus,.btn-outline-info:active:focus{box-shadow:0 0 0 .25rem rgba(13,202,240,.5)}.btn-outline-info.disabled,.btn-outline-info:disabled{color:#0dcaf0;background-color:transparent}.btn-outline-warning{color:#ffc107;border-color:#ffc107}.btn-outline-warning:hover{color:#000;background-color:#ffc107;border-color:#ffc107}.btn-check:focus+.btn-outline-warning,.btn-outline-warning:focus{box-shadow:0 0 0 .25rem rgba(255,193,7,.5)}.btn-check:active+.btn-outline-warning,.btn-check:checked+.btn-outline-warning,.btn-outline-warning.active,.btn-outline-warning.dropdown-toggle.show,.btn-outline-warning:active{color:#000;background-color:#ffc107;border-color:#ffc107}.btn-check:active+.btn-outline-warning:focus,.btn-check:checked+.btn-outline-warning:focus,.btn-outline-warning.active:focus,.btn-outline-warning.dropdown-toggle.show:focus,.btn-outline-warning:active:focus{box-shadow:0 0 0 .25rem rgba(255,193,7,.5)}.btn-outline-warning.disabled,.btn-outline-warning:disabled{color:#ffc107;background-color:transparent}.btn-outline-danger{color:#dc3545;border-color:#dc3545}.btn-outline-danger:hover{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-check:focus+.btn-outline-danger,.btn-outline-danger:focus{box-shadow:0 0 0 .25rem rgba(220,53,69,.5)}.btn-check:active+.btn-outline-danger,.btn-check:checked+.btn-outline-danger,.btn-outline-danger.active,.btn-outline-danger.dropdown-toggle.show,.btn-outline-danger:active{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-check:active+.btn-outline-danger:focus,.btn-check:checked+.btn-outline-danger:focus,.btn-outline-danger.active:focus,.btn-outline-danger.dropdown-toggle.show:focus,.btn-outline-danger:active:focus{box-shadow:0 0 0 .25rem rgba(220,53,69,.5)}.btn-outline-danger.disabled,.btn-outline-danger:disabled{color:#dc3545;background-color:transparent}.btn-outline-light{color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light:hover{color:#000;background-color:#f8f9fa;border-color:#f8f9fa}.btn-check:focus+.btn-outline-light,.btn-outline-light:focus{box-shadow:0 0 0 .25rem rgba(248,249,250,.5)}.btn-check:active+.btn-outline-light,.btn-check:checked+.btn-outline-light,.btn-outline-light.active,.btn-outline-light.dropdown-toggle.show,.btn-outline-light:active{color:#000;background-color:#f8f9fa;border-color:#f8f9fa}.btn-check:active+.btn-outline-light:focus,.btn-check:checked+.btn-outline-light:focus,.btn-outline-light.active:focus,.btn-outline-light.dropdown-toggle.show:focus,.btn-outline-light:active:focus{box-shadow:0 0 0 .25rem rgba(248,249,250,.5)}.btn-outline-light.disabled,.btn-outline-light:disabled{color:#f8f9fa;background-color:transparent}.btn-outline-dark{color:#212529;border-color:#212529}.btn-outline-dark:hover{color:#fff;background-color:#212529;border-color:#212529}.btn-check:focus+.btn-outline-dark,.btn-outline-dark:focus{box-shadow:0 0 0 .25rem rgba(33,37,41,.5)}.btn-check:active+.btn-outline-dark,.btn-check:checked+.btn-outline-dark,.btn-outline-dark.active,.btn-outline-dark.dropdown-toggle.show,.btn-outline-dark:active{color:#fff;background-color:#212529;border-color:#212529}.btn-check:active+.btn-outline-dark:focus,.btn-check:checked+.btn-outline-dark:focus,.btn-outline-dark.active:focus,.btn-outline-dark.dropdown-toggle.show:focus,.btn-outline-dark:active:focus{box-shadow:0 0 0 .25rem rgba(33,37,41,.5)}.btn-outline-dark.disabled,.btn-outline-dark:disabled{color:#212529;background-color:transparent}.btn-link{font-weight:400;color:#0d6efd;text-decoration:underline}.btn-link:hover{color:#0a58ca}.btn-link.disabled,.btn-link:disabled{color:#6c757d}.btn-group-lg>.btn,.btn-lg{padding:.5rem 1rem;font-size:1.25rem;border-radius:.3rem}.btn-group-sm>.btn,.btn-sm{padding:.25rem .5rem;font-size:.875rem;border-radius:.2rem}.fade{transition:opacity .15s linear}@media (prefers-reduced-motion:reduce){.fade{transition:none}}.fade:not(.show){opacity:0}.collapse:not(.show){display:none}.collapsing{height:0;overflow:hidden;transition:height .35s ease}@media (prefers-reduced-motion:reduce){.collapsing{transition:none}}.collapsing.collapse-horizontal{width:0;height:auto;transition:width .35s ease}@media (prefers-reduced-motion:reduce){.collapsing.collapse-horizontal{transition:none}}.dropdown,.dropend,.dropstart,.dropup{position:relative}.dropdown-toggle{white-space:nowrap}.dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid;border-right:.3em solid transparent;border-bottom:0;border-left:.3em solid transparent}.dropdown-toggle:empty::after{margin-left:0}.dropdown-menu{position:absolute;z-index:1000;display:none;min-width:10rem;padding:.5rem 0;margin:0;font-size:1rem;color:#212529;text-align:left;list-style:none;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.15);border-radius:.25rem}.dropdown-menu[data-bs-popper]{top:100%;left:0;margin-top:.125rem}.dropdown-menu-start{--bs-position:start}.dropdown-menu-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-end{--bs-position:end}.dropdown-menu-end[data-bs-popper]{right:0;left:auto}@media (min-width:576px){.dropdown-menu-sm-start{--bs-position:start}.dropdown-menu-sm-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-sm-end{--bs-position:end}.dropdown-menu-sm-end[data-bs-popper]{right:0;left:auto}}@media (min-width:768px){.dropdown-menu-md-start{--bs-position:start}.dropdown-menu-md-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-md-end{--bs-position:end}.dropdown-menu-md-end[data-bs-popper]{right:0;left:auto}}@media (min-width:992px){.dropdown-menu-lg-start{--bs-position:start}.dropdown-menu-lg-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-lg-end{--bs-position:end}.dropdown-menu-lg-end[data-bs-popper]{right:0;left:auto}}@media (min-width:1200px){.dropdown-menu-xl-start{--bs-position:start}.dropdown-menu-xl-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-xl-end{--bs-position:end}.dropdown-menu-xl-end[data-bs-popper]{right:0;left:auto}}@media (min-width:1400px){.dropdown-menu-xxl-start{--bs-position:start}.dropdown-menu-xxl-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-xxl-end{--bs-position:end}.dropdown-menu-xxl-end[data-bs-popper]{right:0;left:auto}}.dropup .dropdown-menu[data-bs-popper]{top:auto;bottom:100%;margin-top:0;margin-bottom:.125rem}.dropup .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:0;border-right:.3em solid transparent;border-bottom:.3em solid;border-left:.3em solid transparent}.dropup .dropdown-toggle:empty::after{margin-left:0}.dropend .dropdown-menu[data-bs-popper]{top:0;right:auto;left:100%;margin-top:0;margin-left:.125rem}.dropend .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:0;border-bottom:.3em solid transparent;border-left:.3em solid}.dropend .dropdown-toggle:empty::after{margin-left:0}.dropend .dropdown-toggle::after{vertical-align:0}.dropstart .dropdown-menu[data-bs-popper]{top:0;right:100%;left:auto;margin-top:0;margin-right:.125rem}.dropstart .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:""}.dropstart .dropdown-toggle::after{display:none}.dropstart .dropdown-toggle::before{display:inline-block;margin-right:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:.3em solid;border-bottom:.3em solid transparent}.dropstart .dropdown-toggle:empty::after{margin-left:0}.dropstart .dropdown-toggle::before{vertical-align:0}.dropdown-divider{height:0;margin:.5rem 0;overflow:hidden;border-top:1px solid rgba(0,0,0,.15)}.dropdown-item{display:block;width:100%;padding:.25rem 1rem;clear:both;font-weight:400;color:#212529;text-align:inherit;text-decoration:none;white-space:nowrap;background-color:transparent;border:0}.dropdown-item:focus,.dropdown-item:hover{color:#1e2125;background-color:#e9ecef}.dropdown-item.active,.dropdown-item:active{color:#fff;text-decoration:none;background-color:#0d6efd}.dropdown-item.disabled,.dropdown-item:disabled{color:#adb5bd;pointer-events:none;background-color:transparent}.dropdown-menu.show{display:block}.dropdown-header{display:block;padding:.5rem 1rem;margin-bottom:0;font-size:.875rem;color:#6c757d;white-space:nowrap}.dropdown-item-text{display:block;padding:.25rem 1rem;color:#212529}.dropdown-menu-dark{color:#dee2e6;background-color:#343a40;border-color:rgba(0,0,0,.15)}.dropdown-menu-dark .dropdown-item{color:#dee2e6}.dropdown-menu-dark .dropdown-item:focus,.dropdown-menu-dark .dropdown-item:hover{color:#fff;background-color:rgba(255,255,255,.15)}.dropdown-menu-dark .dropdown-item.active,.dropdown-menu-dark .dropdown-item:active{color:#fff;background-color:#0d6efd}.dropdown-menu-dark .dropdown-item.disabled,.dropdown-menu-dark .dropdown-item:disabled{color:#adb5bd}.dropdown-menu-dark .dropdown-divider{border-color:rgba(0,0,0,.15)}.dropdown-menu-dark .dropdown-item-text{color:#dee2e6}.dropdown-menu-dark .dropdown-header{color:#adb5bd}.btn-group,.btn-group-vertical{position:relative;display:inline-flex;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;flex:1 1 auto}.btn-group-vertical>.btn-check:checked+.btn,.btn-group-vertical>.btn-check:focus+.btn,.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:hover,.btn-group>.btn-check:checked+.btn,.btn-group>.btn-check:focus+.btn,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus,.btn-group>.btn:hover{z-index:1}.btn-toolbar{display:flex;flex-wrap:wrap;justify-content:flex-start}.btn-toolbar .input-group{width:auto}.btn-group>.btn-group:not(:first-child),.btn-group>.btn:not(:first-child){margin-left:-1px}.btn-group>.btn-group:not(:last-child)>.btn,.btn-group>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:not(:first-child)>.btn,.btn-group>.btn:nth-child(n+3),.btn-group>:not(.btn-check)+.btn{border-top-left-radius:0;border-bottom-left-radius:0}.dropdown-toggle-split{padding-right:.5625rem;padding-left:.5625rem}.dropdown-toggle-split::after,.dropend .dropdown-toggle-split::after,.dropup .dropdown-toggle-split::after{margin-left:0}.dropstart .dropdown-toggle-split::before{margin-right:0}.btn-group-sm>.btn+.dropdown-toggle-split,.btn-sm+.dropdown-toggle-split{padding-right:.375rem;padding-left:.375rem}.btn-group-lg>.btn+.dropdown-toggle-split,.btn-lg+.dropdown-toggle-split{padding-right:.75rem;padding-left:.75rem}.btn-group-vertical{flex-direction:column;align-items:flex-start;justify-content:center}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group{width:100%}.btn-group-vertical>.btn-group:not(:first-child),.btn-group-vertical>.btn:not(:first-child){margin-top:-1px}.btn-group-vertical>.btn-group:not(:last-child)>.btn,.btn-group-vertical>.btn:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:not(:first-child)>.btn,.btn-group-vertical>.btn~.btn{border-top-left-radius:0;border-top-right-radius:0}.nav{display:flex;flex-wrap:wrap;padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:block;padding:.5rem 1rem;color:#0d6efd;text-decoration:none;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out}@media (prefers-reduced-motion:reduce){.nav-link{transition:none}}.nav-link:focus,.nav-link:hover{color:#0a58ca}.nav-link.disabled{color:#6c757d;pointer-events:none;cursor:default}.nav-tabs{border-bottom:1px solid #dee2e6}.nav-tabs .nav-link{margin-bottom:-1px;background:0 0;border:1px solid transparent;border-top-left-radius:.25rem;border-top-right-radius:.25rem}.nav-tabs .nav-link:focus,.nav-tabs .nav-link:hover{border-color:#e9ecef #e9ecef #dee2e6;isolation:isolate}.nav-tabs .nav-link.disabled{color:#6c757d;background-color:transparent;border-color:transparent}.nav-tabs .nav-item.show .nav-link,.nav-tabs .nav-link.active{color:#495057;background-color:#fff;border-color:#dee2e6 #dee2e6 #fff}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-left-radius:0;border-top-right-radius:0}.nav-pills .nav-link{background:0 0;border:0;border-radius:.25rem}.nav-pills .nav-link.active,.nav-pills .show>.nav-link{color:#fff;background-color:#0d6efd}.nav-fill .nav-item,.nav-fill>.nav-link{flex:1 1 auto;text-align:center}.nav-justified .nav-item,.nav-justified>.nav-link{flex-basis:0;flex-grow:1;text-align:center}.nav-fill .nav-item .nav-link,.nav-justified .nav-item .nav-link{width:100%}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.navbar{position:relative;display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;padding-top:.5rem;padding-bottom:.5rem}.navbar>.container,.navbar>.container-fluid,.navbar>.container-lg,.navbar>.container-md,.navbar>.container-sm,.navbar>.container-xl,.navbar>.container-xxl{display:flex;flex-wrap:inherit;align-items:center;justify-content:space-between}.navbar-brand{padding-top:.3125rem;padding-bottom:.3125rem;margin-right:1rem;font-size:1.25rem;text-decoration:none;white-space:nowrap}.navbar-nav{display:flex;flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-nav .nav-link{padding-right:0;padding-left:0}.navbar-nav .dropdown-menu{position:static}.navbar-text{padding-top:.5rem;padding-bottom:.5rem}.navbar-collapse{flex-basis:100%;flex-grow:1;align-items:center}.navbar-toggler{padding:.25rem .75rem;font-size:1.25rem;line-height:1;background-color:transparent;border:1px solid transparent;border-radius:.25rem;transition:box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.navbar-toggler{transition:none}}.navbar-toggler:hover{text-decoration:none}.navbar-toggler:focus{text-decoration:none;outline:0;box-shadow:0 0 0 .25rem}.navbar-toggler-icon{display:inline-block;width:1.5em;height:1.5em;vertical-align:middle;background-repeat:no-repeat;background-position:center;background-size:100%}.navbar-nav-scroll{max-height:var(--bs-scroll-height,75vh);overflow-y:auto}@media (min-width:576px){.navbar-expand-sm{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-sm .navbar-nav{flex-direction:row}.navbar-expand-sm .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-sm .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-sm .navbar-nav-scroll{overflow:visible}.navbar-expand-sm .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-sm .navbar-toggler{display:none}.navbar-expand-sm .offcanvas-header{display:none}.navbar-expand-sm .offcanvas{position:inherit;bottom:0;z-index:1000;flex-grow:1;visibility:visible!important;background-color:transparent;border-right:0;border-left:0;transition:none;transform:none}.navbar-expand-sm .offcanvas-bottom,.navbar-expand-sm .offcanvas-top{height:auto;border-top:0;border-bottom:0}.navbar-expand-sm .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:768px){.navbar-expand-md{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-md .navbar-nav{flex-direction:row}.navbar-expand-md .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-md .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-md .navbar-nav-scroll{overflow:visible}.navbar-expand-md .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-md .navbar-toggler{display:none}.navbar-expand-md .offcanvas-header{display:none}.navbar-expand-md .offcanvas{position:inherit;bottom:0;z-index:1000;flex-grow:1;visibility:visible!important;background-color:transparent;border-right:0;border-left:0;transition:none;transform:none}.navbar-expand-md .offcanvas-bottom,.navbar-expand-md .offcanvas-top{height:auto;border-top:0;border-bottom:0}.navbar-expand-md .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:992px){.navbar-expand-lg{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-lg .navbar-nav{flex-direction:row}.navbar-expand-lg .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-lg .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-lg .navbar-nav-scroll{overflow:visible}.navbar-expand-lg .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-lg .navbar-toggler{display:none}.navbar-expand-lg .offcanvas-header{display:none}.navbar-expand-lg .offcanvas{position:inherit;bottom:0;z-index:1000;flex-grow:1;visibility:visible!important;background-color:transparent;border-right:0;border-left:0;transition:none;transform:none}.navbar-expand-lg .offcanvas-bottom,.navbar-expand-lg .offcanvas-top{height:auto;border-top:0;border-bottom:0}.navbar-expand-lg .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:1200px){.navbar-expand-xl{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-xl .navbar-nav{flex-direction:row}.navbar-expand-xl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xl .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-xl .navbar-nav-scroll{overflow:visible}.navbar-expand-xl .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-xl .navbar-toggler{display:none}.navbar-expand-xl .offcanvas-header{display:none}.navbar-expand-xl .offcanvas{position:inherit;bottom:0;z-index:1000;flex-grow:1;visibility:visible!important;background-color:transparent;border-right:0;border-left:0;transition:none;transform:none}.navbar-expand-xl .offcanvas-bottom,.navbar-expand-xl .offcanvas-top{height:auto;border-top:0;border-bottom:0}.navbar-expand-xl .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:1400px){.navbar-expand-xxl{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-xxl .navbar-nav{flex-direction:row}.navbar-expand-xxl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xxl .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-xxl .navbar-nav-scroll{overflow:visible}.navbar-expand-xxl .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-xxl .navbar-toggler{display:none}.navbar-expand-xxl .offcanvas-header{display:none}.navbar-expand-xxl .offcanvas{position:inherit;bottom:0;z-index:1000;flex-grow:1;visibility:visible!important;background-color:transparent;border-right:0;border-left:0;transition:none;transform:none}.navbar-expand-xxl .offcanvas-bottom,.navbar-expand-xxl .offcanvas-top{height:auto;border-top:0;border-bottom:0}.navbar-expand-xxl .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}.navbar-expand{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand .navbar-nav{flex-direction:row}.navbar-expand .navbar-nav .dropdown-menu{position:absolute}.navbar-expand .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand .navbar-nav-scroll{overflow:visible}.navbar-expand .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand .navbar-toggler{display:none}.navbar-expand .offcanvas-header{display:none}.navbar-expand .offcanvas{position:inherit;bottom:0;z-index:1000;flex-grow:1;visibility:visible!important;background-color:transparent;border-right:0;border-left:0;transition:none;transform:none}.navbar-expand .offcanvas-bottom,.navbar-expand .offcanvas-top{height:auto;border-top:0;border-bottom:0}.navbar-expand .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}.navbar-light .navbar-brand{color:rgba(0,0,0,.9)}.navbar-light .navbar-brand:focus,.navbar-light .navbar-brand:hover{color:rgba(0,0,0,.9)}.navbar-light .navbar-nav .nav-link{color:rgba(0,0,0,.55)}.navbar-light .navbar-nav .nav-link:focus,.navbar-light .navbar-nav .nav-link:hover{color:rgba(0,0,0,.7)}.navbar-light .navbar-nav .nav-link.disabled{color:rgba(0,0,0,.3)}.navbar-light .navbar-nav .nav-link.active,.navbar-light .navbar-nav .show>.nav-link{color:rgba(0,0,0,.9)}.navbar-light .navbar-toggler{color:rgba(0,0,0,.55);border-color:rgba(0,0,0,.1)}.navbar-light .navbar-toggler-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%280, 0, 0, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-light .navbar-text{color:rgba(0,0,0,.55)}.navbar-light .navbar-text a,.navbar-light .navbar-text a:focus,.navbar-light .navbar-text a:hover{color:rgba(0,0,0,.9)}.navbar-dark .navbar-brand{color:#fff}.navbar-dark .navbar-brand:focus,.navbar-dark .navbar-brand:hover{color:#fff}.navbar-dark .navbar-nav .nav-link{color:rgba(255,255,255,.55)}.navbar-dark .navbar-nav .nav-link:focus,.navbar-dark .navbar-nav .nav-link:hover{color:rgba(255,255,255,.75)}.navbar-dark .navbar-nav .nav-link.disabled{color:rgba(255,255,255,.25)}.navbar-dark .navbar-nav .nav-link.active,.navbar-dark .navbar-nav .show>.nav-link{color:#fff}.navbar-dark .navbar-toggler{color:rgba(255,255,255,.55);border-color:rgba(255,255,255,.1)}.navbar-dark .navbar-toggler-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-dark .navbar-text{color:rgba(255,255,255,.55)}.navbar-dark .navbar-text a,.navbar-dark .navbar-text a:focus,.navbar-dark .navbar-text a:hover{color:#fff}.card{position:relative;display:flex;flex-direction:column;min-width:0;word-wrap:break-word;background-color:#fff;background-clip:border-box;border:1px solid rgba(0,0,0,.125);border-radius:.25rem}.card>hr{margin-right:0;margin-left:0}.card>.list-group{border-top:inherit;border-bottom:inherit}.card>.list-group:first-child{border-top-width:0;border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.card>.list-group:last-child{border-bottom-width:0;border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.card>.card-header+.list-group,.card>.list-group+.card-footer{border-top:0}.card-body{flex:1 1 auto;padding:1rem 1rem}.card-title{margin-bottom:.5rem}.card-subtitle{margin-top:-.25rem;margin-bottom:0}.card-text:last-child{margin-bottom:0}.card-link+.card-link{margin-left:1rem}.card-header{padding:.5rem 1rem;margin-bottom:0;background-color:rgba(0,0,0,.03);border-bottom:1px solid rgba(0,0,0,.125)}.card-header:first-child{border-radius:calc(.25rem - 1px) calc(.25rem - 1px) 0 0}.card-footer{padding:.5rem 1rem;background-color:rgba(0,0,0,.03);border-top:1px solid rgba(0,0,0,.125)}.card-footer:last-child{border-radius:0 0 calc(.25rem - 1px) calc(.25rem - 1px)}.card-header-tabs{margin-right:-.5rem;margin-bottom:-.5rem;margin-left:-.5rem;border-bottom:0}.card-header-pills{margin-right:-.5rem;margin-left:-.5rem}.card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:1rem;border-radius:calc(.25rem - 1px)}.card-img,.card-img-bottom,.card-img-top{width:100%}.card-img,.card-img-top{border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.card-img,.card-img-bottom{border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.card-group>.card{margin-bottom:.75rem}@media (min-width:576px){.card-group{display:flex;flex-flow:row wrap}.card-group>.card{flex:1 0 0%;margin-bottom:0}.card-group>.card+.card{margin-left:0;border-left:0}.card-group>.card:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.card-group>.card:not(:last-child) .card-header,.card-group>.card:not(:last-child) .card-img-top{border-top-right-radius:0}.card-group>.card:not(:last-child) .card-footer,.card-group>.card:not(:last-child) .card-img-bottom{border-bottom-right-radius:0}.card-group>.card:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.card-group>.card:not(:first-child) .card-header,.card-group>.card:not(:first-child) .card-img-top{border-top-left-radius:0}.card-group>.card:not(:first-child) .card-footer,.card-group>.card:not(:first-child) .card-img-bottom{border-bottom-left-radius:0}}.accordion-button{position:relative;display:flex;align-items:center;width:100%;padding:1rem 1.25rem;font-size:1rem;color:#212529;text-align:left;background-color:#fff;border:0;border-radius:0;overflow-anchor:none;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out,border-radius .15s ease}@media (prefers-reduced-motion:reduce){.accordion-button{transition:none}}.accordion-button:not(.collapsed){color:#0c63e4;background-color:#e7f1ff;box-shadow:inset 0 -1px 0 rgba(0,0,0,.125)}.accordion-button:not(.collapsed)::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%230c63e4'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");transform:rotate(-180deg)}.accordion-button::after{flex-shrink:0;width:1.25rem;height:1.25rem;margin-left:auto;content:"";background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23212529'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");background-repeat:no-repeat;background-size:1.25rem;transition:transform .2s ease-in-out}@media (prefers-reduced-motion:reduce){.accordion-button::after{transition:none}}.accordion-button:hover{z-index:2}.accordion-button:focus{z-index:3;border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.accordion-header{margin-bottom:0}.accordion-item{background-color:#fff;border:1px solid rgba(0,0,0,.125)}.accordion-item:first-of-type{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.accordion-item:first-of-type .accordion-button{border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.accordion-item:not(:first-of-type){border-top:0}.accordion-item:last-of-type{border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.accordion-item:last-of-type .accordion-button.collapsed{border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.accordion-item:last-of-type .accordion-collapse{border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.accordion-body{padding:1rem 1.25rem}.accordion-flush .accordion-collapse{border-width:0}.accordion-flush .accordion-item{border-right:0;border-left:0;border-radius:0}.accordion-flush .accordion-item:first-child{border-top:0}.accordion-flush .accordion-item:last-child{border-bottom:0}.accordion-flush .accordion-item .accordion-button{border-radius:0}.breadcrumb{display:flex;flex-wrap:wrap;padding:0 0;margin-bottom:1rem;list-style:none}.breadcrumb-item+.breadcrumb-item{padding-left:.5rem}.breadcrumb-item+.breadcrumb-item::before{float:left;padding-right:.5rem;color:#6c757d;content:var(--bs-breadcrumb-divider, "https://templates.hibootstrap.com/")}.breadcrumb-item.active{color:#6c757d}.pagination{display:flex;padding-left:0;list-style:none}.page-link{position:relative;display:block;color:#0d6efd;text-decoration:none;background-color:#fff;border:1px solid #dee2e6;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.page-link{transition:none}}.page-link:hover{z-index:2;color:#0a58ca;background-color:#e9ecef;border-color:#dee2e6}.page-link:focus{z-index:3;color:#0a58ca;background-color:#e9ecef;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.page-item:not(:first-child) .page-link{margin-left:-1px}.page-item.active .page-link{z-index:3;color:#fff;background-color:#0d6efd;border-color:#0d6efd}.page-item.disabled .page-link{color:#6c757d;pointer-events:none;background-color:#fff;border-color:#dee2e6}.page-link{padding:.375rem .75rem}.page-item:first-child .page-link{border-top-left-radius:.25rem;border-bottom-left-radius:.25rem}.page-item:last-child .page-link{border-top-right-radius:.25rem;border-bottom-right-radius:.25rem}.pagination-lg .page-link{padding:.75rem 1.5rem;font-size:1.25rem}.pagination-lg .page-item:first-child .page-link{border-top-left-radius:.3rem;border-bottom-left-radius:.3rem}.pagination-lg .page-item:last-child .page-link{border-top-right-radius:.3rem;border-bottom-right-radius:.3rem}.pagination-sm .page-link{padding:.25rem .5rem;font-size:.875rem}.pagination-sm .page-item:first-child .page-link{border-top-left-radius:.2rem;border-bottom-left-radius:.2rem}.pagination-sm .page-item:last-child .page-link{border-top-right-radius:.2rem;border-bottom-right-radius:.2rem}.badge{display:inline-block;padding:.35em .65em;font-size:.75em;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25rem}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.alert{position:relative;padding:1rem 1rem;margin-bottom:1rem;border:1px solid transparent;border-radius:.25rem}.alert-heading{color:inherit}.alert-link{font-weight:700}.alert-dismissible{padding-right:3rem}.alert-dismissible .btn-close{position:absolute;top:0;right:0;z-index:2;padding:1.25rem 1rem}.alert-primary{color:#084298;background-color:#cfe2ff;border-color:#b6d4fe}.alert-primary .alert-link{color:#06357a}.alert-secondary{color:#41464b;background-color:#e2e3e5;border-color:#d3d6d8}.alert-secondary .alert-link{color:#34383c}.alert-success{color:#0f5132;background-color:#d1e7dd;border-color:#badbcc}.alert-success .alert-link{color:#0c4128}.alert-info{color:#055160;background-color:#cff4fc;border-color:#b6effb}.alert-info .alert-link{color:#04414d}.alert-warning{color:#664d03;background-color:#fff3cd;border-color:#ffecb5}.alert-warning .alert-link{color:#523e02}.alert-danger{color:#842029;background-color:#f8d7da;border-color:#f5c2c7}.alert-danger .alert-link{color:#6a1a21}.alert-light{color:#636464;background-color:#fefefe;border-color:#fdfdfe}.alert-light .alert-link{color:#4f5050}.alert-dark{color:#141619;background-color:#d3d3d4;border-color:#bcbebf}.alert-dark .alert-link{color:#101214}@-webkit-keyframes progress-bar-stripes{0%{background-position-x:1rem}}@keyframes progress-bar-stripes{0%{background-position-x:1rem}}.progress{display:flex;height:1rem;overflow:hidden;font-size:.75rem;background-color:#e9ecef;border-radius:.25rem}.progress-bar{display:flex;flex-direction:column;justify-content:center;overflow:hidden;color:#fff;text-align:center;white-space:nowrap;background-color:#0d6efd;transition:width .6s ease}@media (prefers-reduced-motion:reduce){.progress-bar{transition:none}}.progress-bar-striped{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-size:1rem 1rem}.progress-bar-animated{-webkit-animation:1s linear infinite progress-bar-stripes;animation:1s linear infinite progress-bar-stripes}@media (prefers-reduced-motion:reduce){.progress-bar-animated{-webkit-animation:none;animation:none}}.list-group{display:flex;flex-direction:column;padding-left:0;margin-bottom:0;border-radius:.25rem}.list-group-numbered{list-style-type:none;counter-reset:section}.list-group-numbered>li::before{content:counters(section, ".") ". ";counter-increment:section}.list-group-item-action{width:100%;color:#495057;text-align:inherit}.list-group-item-action:focus,.list-group-item-action:hover{z-index:1;color:#495057;text-decoration:none;background-color:#f8f9fa}.list-group-item-action:active{color:#212529;background-color:#e9ecef}.list-group-item{position:relative;display:block;padding:.5rem 1rem;color:#212529;text-decoration:none;background-color:#fff;border:1px solid rgba(0,0,0,.125)}.list-group-item:first-child{border-top-left-radius:inherit;border-top-right-radius:inherit}.list-group-item:last-child{border-bottom-right-radius:inherit;border-bottom-left-radius:inherit}.list-group-item.disabled,.list-group-item:disabled{color:#6c757d;pointer-events:none;background-color:#fff}.list-group-item.active{z-index:2;color:#fff;background-color:#0d6efd;border-color:#0d6efd}.list-group-item+.list-group-item{border-top-width:0}.list-group-item+.list-group-item.active{margin-top:-1px;border-top-width:1px}.list-group-horizontal{flex-direction:row}.list-group-horizontal>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal>.list-group-item.active{margin-top:0}.list-group-horizontal>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}@media (min-width:576px){.list-group-horizontal-sm{flex-direction:row}.list-group-horizontal-sm>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-sm>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-sm>.list-group-item.active{margin-top:0}.list-group-horizontal-sm>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-sm>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:768px){.list-group-horizontal-md{flex-direction:row}.list-group-horizontal-md>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-md>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-md>.list-group-item.active{margin-top:0}.list-group-horizontal-md>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-md>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:992px){.list-group-horizontal-lg{flex-direction:row}.list-group-horizontal-lg>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-lg>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-lg>.list-group-item.active{margin-top:0}.list-group-horizontal-lg>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-lg>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:1200px){.list-group-horizontal-xl{flex-direction:row}.list-group-horizontal-xl>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-xl>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-xl>.list-group-item.active{margin-top:0}.list-group-horizontal-xl>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-xl>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:1400px){.list-group-horizontal-xxl{flex-direction:row}.list-group-horizontal-xxl>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-xxl>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-xxl>.list-group-item.active{margin-top:0}.list-group-horizontal-xxl>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-xxl>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}.list-group-flush{border-radius:0}.list-group-flush>.list-group-item{border-width:0 0 1px}.list-group-flush>.list-group-item:last-child{border-bottom-width:0}.list-group-item-primary{color:#084298;background-color:#cfe2ff}.list-group-item-primary.list-group-item-action:focus,.list-group-item-primary.list-group-item-action:hover{color:#084298;background-color:#bacbe6}.list-group-item-primary.list-group-item-action.active{color:#fff;background-color:#084298;border-color:#084298}.list-group-item-secondary{color:#41464b;background-color:#e2e3e5}.list-group-item-secondary.list-group-item-action:focus,.list-group-item-secondary.list-group-item-action:hover{color:#41464b;background-color:#cbccce}.list-group-item-secondary.list-group-item-action.active{color:#fff;background-color:#41464b;border-color:#41464b}.list-group-item-success{color:#0f5132;background-color:#d1e7dd}.list-group-item-success.list-group-item-action:focus,.list-group-item-success.list-group-item-action:hover{color:#0f5132;background-color:#bcd0c7}.list-group-item-success.list-group-item-action.active{color:#fff;background-color:#0f5132;border-color:#0f5132}.list-group-item-info{color:#055160;background-color:#cff4fc}.list-group-item-info.list-group-item-action:focus,.list-group-item-info.list-group-item-action:hover{color:#055160;background-color:#badce3}.list-group-item-info.list-group-item-action.active{color:#fff;background-color:#055160;border-color:#055160}.list-group-item-warning{color:#664d03;background-color:#fff3cd}.list-group-item-warning.list-group-item-action:focus,.list-group-item-warning.list-group-item-action:hover{color:#664d03;background-color:#e6dbb9}.list-group-item-warning.list-group-item-action.active{color:#fff;background-color:#664d03;border-color:#664d03}.list-group-item-danger{color:#842029;background-color:#f8d7da}.list-group-item-danger.list-group-item-action:focus,.list-group-item-danger.list-group-item-action:hover{color:#842029;background-color:#dfc2c4}.list-group-item-danger.list-group-item-action.active{color:#fff;background-color:#842029;border-color:#842029}.list-group-item-light{color:#636464;background-color:#fefefe}.list-group-item-light.list-group-item-action:focus,.list-group-item-light.list-group-item-action:hover{color:#636464;background-color:#e5e5e5}.list-group-item-light.list-group-item-action.active{color:#fff;background-color:#636464;border-color:#636464}.list-group-item-dark{color:#141619;background-color:#d3d3d4}.list-group-item-dark.list-group-item-action:focus,.list-group-item-dark.list-group-item-action:hover{color:#141619;background-color:#bebebf}.list-group-item-dark.list-group-item-action.active{color:#fff;background-color:#141619;border-color:#141619}.btn-close{box-sizing:content-box;width:1em;height:1em;padding:.25em .25em;color:#000;background:transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e") center/1em auto no-repeat;border:0;border-radius:.25rem;opacity:.5}.btn-close:hover{color:#000;text-decoration:none;opacity:.75}.btn-close:focus{outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25);opacity:1}.btn-close.disabled,.btn-close:disabled{pointer-events:none;-webkit-user-select:none;-moz-user-select:none;user-select:none;opacity:.25}.btn-close-white{filter:invert(1) grayscale(100%) brightness(200%)}.toast{width:350px;max-width:100%;font-size:.875rem;pointer-events:auto;background-color:rgba(255,255,255,.85);background-clip:padding-box;border:1px solid rgba(0,0,0,.1);box-shadow:0 .5rem 1rem rgba(0,0,0,.15);border-radius:.25rem}.toast.showing{opacity:0}.toast:not(.show){display:none}.toast-container{width:-webkit-max-content;width:-moz-max-content;width:max-content;max-width:100%;pointer-events:none}.toast-container>:not(:last-child){margin-bottom:.75rem}.toast-header{display:flex;align-items:center;padding:.5rem .75rem;color:#6c757d;background-color:rgba(255,255,255,.85);background-clip:padding-box;border-bottom:1px solid rgba(0,0,0,.05);border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.toast-header .btn-close{margin-right:-.375rem;margin-left:.75rem}.toast-body{padding:.75rem;word-wrap:break-word}.modal{position:fixed;top:0;left:0;z-index:1055;display:none;width:100%;height:100%;overflow-x:hidden;overflow-y:auto;outline:0}.modal-dialog{position:relative;width:auto;margin:.5rem;pointer-events:none}.modal.fade .modal-dialog{transition:transform .3s ease-out;transform:translate(0,-50px)}@media (prefers-reduced-motion:reduce){.modal.fade .modal-dialog{transition:none}}.modal.show .modal-dialog{transform:none}.modal.modal-static .modal-dialog{transform:scale(1.02)}.modal-dialog-scrollable{height:calc(100% - 1rem)}.modal-dialog-scrollable .modal-content{max-height:100%;overflow:hidden}.modal-dialog-scrollable .modal-body{overflow-y:auto}.modal-dialog-centered{display:flex;align-items:center;min-height:calc(100% - 1rem)}.modal-content{position:relative;display:flex;flex-direction:column;width:100%;pointer-events:auto;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem;outline:0}.modal-backdrop{position:fixed;top:0;left:0;z-index:1050;width:100vw;height:100vh;background-color:#000}.modal-backdrop.fade{opacity:0}.modal-backdrop.show{opacity:.5}.modal-header{display:flex;flex-shrink:0;align-items:center;justify-content:space-between;padding:1rem 1rem;border-bottom:1px solid #dee2e6;border-top-left-radius:calc(.3rem - 1px);border-top-right-radius:calc(.3rem - 1px)}.modal-header .btn-close{padding:.5rem .5rem;margin:-.5rem -.5rem -.5rem auto}.modal-title{margin-bottom:0;line-height:1.5}.modal-body{position:relative;flex:1 1 auto;padding:1rem}.modal-footer{display:flex;flex-wrap:wrap;flex-shrink:0;align-items:center;justify-content:flex-end;padding:.75rem;border-top:1px solid #dee2e6;border-bottom-right-radius:calc(.3rem - 1px);border-bottom-left-radius:calc(.3rem - 1px)}.modal-footer>*{margin:.25rem}@media (min-width:576px){.modal-dialog{max-width:500px;margin:1.75rem auto}.modal-dialog-scrollable{height:calc(100% - 3.5rem)}.modal-dialog-centered{min-height:calc(100% - 3.5rem)}.modal-sm{max-width:300px}}@media (min-width:992px){.modal-lg,.modal-xl{max-width:800px}}@media (min-width:1200px){.modal-xl{max-width:1140px}}.modal-fullscreen{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen .modal-header{border-radius:0}.modal-fullscreen .modal-body{overflow-y:auto}.modal-fullscreen .modal-footer{border-radius:0}@media (max-width:575.98px){.modal-fullscreen-sm-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-sm-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-sm-down .modal-header{border-radius:0}.modal-fullscreen-sm-down .modal-body{overflow-y:auto}.modal-fullscreen-sm-down .modal-footer{border-radius:0}}@media (max-width:767.98px){.modal-fullscreen-md-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-md-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-md-down .modal-header{border-radius:0}.modal-fullscreen-md-down .modal-body{overflow-y:auto}.modal-fullscreen-md-down .modal-footer{border-radius:0}}@media (max-width:991.98px){.modal-fullscreen-lg-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-lg-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-lg-down .modal-header{border-radius:0}.modal-fullscreen-lg-down .modal-body{overflow-y:auto}.modal-fullscreen-lg-down .modal-footer{border-radius:0}}@media (max-width:1199.98px){.modal-fullscreen-xl-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-xl-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-xl-down .modal-header{border-radius:0}.modal-fullscreen-xl-down .modal-body{overflow-y:auto}.modal-fullscreen-xl-down .modal-footer{border-radius:0}}@media (max-width:1399.98px){.modal-fullscreen-xxl-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-xxl-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-xxl-down .modal-header{border-radius:0}.modal-fullscreen-xxl-down .modal-body{overflow-y:auto}.modal-fullscreen-xxl-down .modal-footer{border-radius:0}}.tooltip{position:absolute;z-index:1080;display:block;margin:0;font-family:var(--bs-font-sans-serif);font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;opacity:0}.tooltip.show{opacity:.9}.tooltip .tooltip-arrow{position:absolute;display:block;width:.8rem;height:.4rem}.tooltip .tooltip-arrow::before{position:absolute;content:"";border-color:transparent;border-style:solid}.bs-tooltip-auto[data-popper-placement^=top],.bs-tooltip-top{padding:.4rem 0}.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow,.bs-tooltip-top .tooltip-arrow{bottom:0}.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow::before,.bs-tooltip-top .tooltip-arrow::before{top:-1px;border-width:.4rem .4rem 0;border-top-color:#000}.bs-tooltip-auto[data-popper-placement^=right],.bs-tooltip-end{padding:0 .4rem}.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow,.bs-tooltip-end .tooltip-arrow{left:0;width:.4rem;height:.8rem}.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow::before,.bs-tooltip-end .tooltip-arrow::before{right:-1px;border-width:.4rem .4rem .4rem 0;border-right-color:#000}.bs-tooltip-auto[data-popper-placement^=bottom],.bs-tooltip-bottom{padding:.4rem 0}.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow,.bs-tooltip-bottom .tooltip-arrow{top:0}.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow::before,.bs-tooltip-bottom .tooltip-arrow::before{bottom:-1px;border-width:0 .4rem .4rem;border-bottom-color:#000}.bs-tooltip-auto[data-popper-placement^=left],.bs-tooltip-start{padding:0 .4rem}.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow,.bs-tooltip-start .tooltip-arrow{right:0;width:.4rem;height:.8rem}.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow::before,.bs-tooltip-start .tooltip-arrow::before{left:-1px;border-width:.4rem 0 .4rem .4rem;border-left-color:#000}.tooltip-inner{max-width:200px;padding:.25rem .5rem;color:#fff;text-align:center;background-color:#000;border-radius:.25rem}.popover{position:absolute;top:0;left:0;z-index:1070;display:block;max-width:276px;font-family:var(--bs-font-sans-serif);font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem}.popover .popover-arrow{position:absolute;display:block;width:1rem;height:.5rem}.popover .popover-arrow::after,.popover .popover-arrow::before{position:absolute;display:block;content:"";border-color:transparent;border-style:solid}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow,.bs-popover-top>.popover-arrow{bottom:calc(-.5rem - 1px)}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::before,.bs-popover-top>.popover-arrow::before{bottom:0;border-width:.5rem .5rem 0;border-top-color:rgba(0,0,0,.25)}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::after,.bs-popover-top>.popover-arrow::after{bottom:1px;border-width:.5rem .5rem 0;border-top-color:#fff}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow,.bs-popover-end>.popover-arrow{left:calc(-.5rem - 1px);width:.5rem;height:1rem}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::before,.bs-popover-end>.popover-arrow::before{left:0;border-width:.5rem .5rem .5rem 0;border-right-color:rgba(0,0,0,.25)}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::after,.bs-popover-end>.popover-arrow::after{left:1px;border-width:.5rem .5rem .5rem 0;border-right-color:#fff}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow,.bs-popover-bottom>.popover-arrow{top:calc(-.5rem - 1px)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::before,.bs-popover-bottom>.popover-arrow::before{top:0;border-width:0 .5rem .5rem .5rem;border-bottom-color:rgba(0,0,0,.25)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::after,.bs-popover-bottom>.popover-arrow::after{top:1px;border-width:0 .5rem .5rem .5rem;border-bottom-color:#fff}.bs-popover-auto[data-popper-placement^=bottom] .popover-header::before,.bs-popover-bottom .popover-header::before{position:absolute;top:0;left:50%;display:block;width:1rem;margin-left:-.5rem;content:"";border-bottom:1px solid #f0f0f0}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow,.bs-popover-start>.popover-arrow{right:calc(-.5rem - 1px);width:.5rem;height:1rem}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::before,.bs-popover-start>.popover-arrow::before{right:0;border-width:.5rem 0 .5rem .5rem;border-left-color:rgba(0,0,0,.25)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::after,.bs-popover-start>.popover-arrow::after{right:1px;border-width:.5rem 0 .5rem .5rem;border-left-color:#fff}.popover-header{padding:.5rem 1rem;margin-bottom:0;font-size:1rem;background-color:#f0f0f0;border-bottom:1px solid rgba(0,0,0,.2);border-top-left-radius:calc(.3rem - 1px);border-top-right-radius:calc(.3rem - 1px)}.popover-header:empty{display:none}.popover-body{padding:1rem 1rem;color:#212529}.carousel{position:relative}.carousel.pointer-event{touch-action:pan-y}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner::after{display:block;clear:both;content:""}.carousel-item{position:relative;display:none;float:left;width:100%;margin-right:-100%;-webkit-backface-visibility:hidden;backface-visibility:hidden;transition:transform .6s ease-in-out}@media (prefers-reduced-motion:reduce){.carousel-item{transition:none}}.carousel-item-next,.carousel-item-prev,.carousel-item.active{display:block}.active.carousel-item-end,.carousel-item-next:not(.carousel-item-start){transform:translateX(100%)}.active.carousel-item-start,.carousel-item-prev:not(.carousel-item-end){transform:translateX(-100%)}.carousel-fade .carousel-item{opacity:0;transition-property:opacity;transform:none}.carousel-fade .carousel-item-next.carousel-item-start,.carousel-fade .carousel-item-prev.carousel-item-end,.carousel-fade .carousel-item.active{z-index:1;opacity:1}.carousel-fade .active.carousel-item-end,.carousel-fade .active.carousel-item-start{z-index:0;opacity:0;transition:opacity 0s .6s}@media (prefers-reduced-motion:reduce){.carousel-fade .active.carousel-item-end,.carousel-fade .active.carousel-item-start{transition:none}}.carousel-control-next,.carousel-control-prev{position:absolute;top:0;bottom:0;z-index:1;display:flex;align-items:center;justify-content:center;width:15%;padding:0;color:#fff;text-align:center;background:0 0;border:0;opacity:.5;transition:opacity .15s ease}@media (prefers-reduced-motion:reduce){.carousel-control-next,.carousel-control-prev{transition:none}}.carousel-control-next:focus,.carousel-control-next:hover,.carousel-control-prev:focus,.carousel-control-prev:hover{color:#fff;text-decoration:none;outline:0;opacity:.9}.carousel-control-prev{left:0}.carousel-control-next{right:0}.carousel-control-next-icon,.carousel-control-prev-icon{display:inline-block;width:2rem;height:2rem;background-repeat:no-repeat;background-position:50%;background-size:100% 100%}.carousel-control-prev-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z'/%3e%3c/svg%3e")}.carousel-control-next-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e")}.carousel-indicators{position:absolute;right:0;bottom:0;left:0;z-index:2;display:flex;justify-content:center;padding:0;margin-right:15%;margin-bottom:1rem;margin-left:15%;list-style:none}.carousel-indicators [data-bs-target]{box-sizing:content-box;flex:0 1 auto;width:30px;height:3px;padding:0;margin-right:3px;margin-left:3px;text-indent:-999px;cursor:pointer;background-color:#fff;background-clip:padding-box;border:0;border-top:10px solid transparent;border-bottom:10px solid transparent;opacity:.5;transition:opacity .6s ease}@media (prefers-reduced-motion:reduce){.carousel-indicators [data-bs-target]{transition:none}}.carousel-indicators .active{opacity:1}.carousel-caption{position:absolute;right:15%;bottom:1.25rem;left:15%;padding-top:1.25rem;padding-bottom:1.25rem;color:#fff;text-align:center}.carousel-dark .carousel-control-next-icon,.carousel-dark .carousel-control-prev-icon{filter:invert(1) grayscale(100)}.carousel-dark .carousel-indicators [data-bs-target]{background-color:#000}.carousel-dark .carousel-caption{color:#000}@-webkit-keyframes spinner-border{to{transform:rotate(360deg)}}@keyframes spinner-border{to{transform:rotate(360deg)}}.spinner-border{display:inline-block;width:2rem;height:2rem;vertical-align:-.125em;border:.25em solid currentColor;border-right-color:transparent;border-radius:50%;-webkit-animation:.75s linear infinite spinner-border;animation:.75s linear infinite spinner-border}.spinner-border-sm{width:1rem;height:1rem;border-width:.2em}@-webkit-keyframes spinner-grow{0%{transform:scale(0)}50%{opacity:1;transform:none}}@keyframes spinner-grow{0%{transform:scale(0)}50%{opacity:1;transform:none}}.spinner-grow{display:inline-block;width:2rem;height:2rem;vertical-align:-.125em;background-color:currentColor;border-radius:50%;opacity:0;-webkit-animation:.75s linear infinite spinner-grow;animation:.75s linear infinite spinner-grow}.spinner-grow-sm{width:1rem;height:1rem}@media (prefers-reduced-motion:reduce){.spinner-border,.spinner-grow{-webkit-animation-duration:1.5s;animation-duration:1.5s}}.offcanvas{position:fixed;bottom:0;z-index:1045;display:flex;flex-direction:column;max-width:100%;visibility:hidden;background-color:#fff;background-clip:padding-box;outline:0;transition:transform .3s ease-in-out}@media (prefers-reduced-motion:reduce){.offcanvas{transition:none}}.offcanvas-backdrop{position:fixed;top:0;left:0;z-index:1040;width:100vw;height:100vh;background-color:#000}.offcanvas-backdrop.fade{opacity:0}.offcanvas-backdrop.show{opacity:.5}.offcanvas-header{display:flex;align-items:center;justify-content:space-between;padding:1rem 1rem}.offcanvas-header .btn-close{padding:.5rem .5rem;margin-top:-.5rem;margin-right:-.5rem;margin-bottom:-.5rem}.offcanvas-title{margin-bottom:0;line-height:1.5}.offcanvas-body{flex-grow:1;padding:1rem 1rem;overflow-y:auto}.offcanvas-start{top:0;left:0;width:400px;border-right:1px solid rgba(0,0,0,.2);transform:translateX(-100%)}.offcanvas-end{top:0;right:0;width:400px;border-left:1px solid rgba(0,0,0,.2);transform:translateX(100%)}.offcanvas-top{top:0;right:0;left:0;height:30vh;max-height:100%;border-bottom:1px solid rgba(0,0,0,.2);transform:translateY(-100%)}.offcanvas-bottom{right:0;left:0;height:30vh;max-height:100%;border-top:1px solid rgba(0,0,0,.2);transform:translateY(100%)}.offcanvas.show{transform:none}.placeholder{display:inline-block;min-height:1em;vertical-align:middle;cursor:wait;background-color:currentColor;opacity:.5}.placeholder.btn::before{display:inline-block;content:""}.placeholder-xs{min-height:.6em}.placeholder-sm{min-height:.8em}.placeholder-lg{min-height:1.2em}.placeholder-glow .placeholder{-webkit-animation:placeholder-glow 2s ease-in-out infinite;animation:placeholder-glow 2s ease-in-out infinite}@-webkit-keyframes placeholder-glow{50%{opacity:.2}}@keyframes placeholder-glow{50%{opacity:.2}}.placeholder-wave{-webkit-mask-image:linear-gradient(130deg,#000 55%,rgba(0,0,0,0.8) 75%,#000 95%);mask-image:linear-gradient(130deg,#000 55%,rgba(0,0,0,0.8) 75%,#000 95%);-webkit-mask-size:200% 100%;mask-size:200% 100%;-webkit-animation:placeholder-wave 2s linear infinite;animation:placeholder-wave 2s linear infinite}@-webkit-keyframes placeholder-wave{100%{-webkit-mask-position:-200% 0%;mask-position:-200% 0%}}@keyframes placeholder-wave{100%{-webkit-mask-position:-200% 0%;mask-position:-200% 0%}}.clearfix::after{display:block;clear:both;content:""}.link-primary{color:#0d6efd}.link-primary:focus,.link-primary:hover{color:#0a58ca}.link-secondary{color:#6c757d}.link-secondary:focus,.link-secondary:hover{color:#565e64}.link-success{color:#198754}.link-success:focus,.link-success:hover{color:#146c43}.link-info{color:#0dcaf0}.link-info:focus,.link-info:hover{color:#3dd5f3}.link-warning{color:#ffc107}.link-warning:focus,.link-warning:hover{color:#ffcd39}.link-danger{color:#dc3545}.link-danger:focus,.link-danger:hover{color:#b02a37}.link-light{color:#f8f9fa}.link-light:focus,.link-light:hover{color:#f9fafb}.link-dark{color:#212529}.link-dark:focus,.link-dark:hover{color:#1a1e21}.ratio{position:relative;width:100%}.ratio::before{display:block;padding-top:var(--bs-aspect-ratio);content:""}.ratio>*{position:absolute;top:0;left:0;width:100%;height:100%}.ratio-1x1{--bs-aspect-ratio:100%}.ratio-4x3{--bs-aspect-ratio:75%}.ratio-16x9{--bs-aspect-ratio:56.25%}.ratio-21x9{--bs-aspect-ratio:42.8571428571%}.fixed-top{position:fixed;top:0;right:0;left:0;z-index:1030}.fixed-bottom{position:fixed;right:0;bottom:0;left:0;z-index:1030}.sticky-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}@media (min-width:576px){.sticky-sm-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}@media (min-width:768px){.sticky-md-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}@media (min-width:992px){.sticky-lg-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}@media (min-width:1200px){.sticky-xl-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}@media (min-width:1400px){.sticky-xxl-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}.hstack{display:flex;flex-direction:row;align-items:center;align-self:stretch}.vstack{display:flex;flex:1 1 auto;flex-direction:column;align-self:stretch}.visually-hidden,.visually-hidden-focusable:not(:focus):not(:focus-within){position:absolute!important;width:1px!important;height:1px!important;padding:0!important;margin:-1px!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important;border:0!important}.stretched-link::after{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;content:""}.text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.vr{display:inline-block;align-self:stretch;width:1px;min-height:1em;background-color:currentColor;opacity:.25}.align-baseline{vertical-align:baseline!important}.align-top{vertical-align:top!important}.align-middle{vertical-align:middle!important}.align-bottom{vertical-align:bottom!important}.align-text-bottom{vertical-align:text-bottom!important}.align-text-top{vertical-align:text-top!important}.float-start{float:left!important}.float-end{float:right!important}.float-none{float:none!important}.opacity-0{opacity:0!important}.opacity-25{opacity:.25!important}.opacity-50{opacity:.5!important}.opacity-75{opacity:.75!important}.opacity-100{opacity:1!important}.overflow-auto{overflow:auto!important}.overflow-hidden{overflow:hidden!important}.overflow-visible{overflow:visible!important}.overflow-scroll{overflow:scroll!important}.d-inline{display:inline!important}.d-inline-block{display:inline-block!important}.d-block{display:block!important}.d-grid{display:grid!important}.d-table{display:table!important}.d-table-row{display:table-row!important}.d-table-cell{display:table-cell!important}.d-flex{display:flex!important}.d-inline-flex{display:inline-flex!important}.d-none{display:none!important}.shadow{box-shadow:0 .5rem 1rem rgba(0,0,0,.15)!important}.shadow-sm{box-shadow:0 .125rem .25rem rgba(0,0,0,.075)!important}.shadow-lg{box-shadow:0 1rem 3rem rgba(0,0,0,.175)!important}.shadow-none{box-shadow:none!important}.position-static{position:static!important}.position-relative{position:relative!important}.position-absolute{position:absolute!important}.position-fixed{position:fixed!important}.position-sticky{position:-webkit-sticky!important;position:sticky!important}.top-0{top:0!important}.top-50{top:50%!important}.top-100{top:100%!important}.bottom-0{bottom:0!important}.bottom-50{bottom:50%!important}.bottom-100{bottom:100%!important}.start-0{left:0!important}.start-50{left:50%!important}.start-100{left:100%!important}.end-0{right:0!important}.end-50{right:50%!important}.end-100{right:100%!important}.translate-middle{transform:translate(-50%,-50%)!important}.translate-middle-x{transform:translateX(-50%)!important}.translate-middle-y{transform:translateY(-50%)!important}.border{border:1px solid #dee2e6!important}.border-0{border:0!important}.border-top{border-top:1px solid #dee2e6!important}.border-top-0{border-top:0!important}.border-end{border-right:1px solid #dee2e6!important}.border-end-0{border-right:0!important}.border-bottom{border-bottom:1px solid #dee2e6!important}.border-bottom-0{border-bottom:0!important}.border-start{border-left:1px solid #dee2e6!important}.border-start-0{border-left:0!important}.border-primary{border-color:#0d6efd!important}.border-secondary{border-color:#6c757d!important}.border-success{border-color:#198754!important}.border-info{border-color:#0dcaf0!important}.border-warning{border-color:#ffc107!important}.border-danger{border-color:#dc3545!important}.border-light{border-color:#f8f9fa!important}.border-dark{border-color:#212529!important}.border-white{border-color:#fff!important}.border-1{border-width:1px!important}.border-2{border-width:2px!important}.border-3{border-width:3px!important}.border-4{border-width:4px!important}.border-5{border-width:5px!important}.w-25{width:25%!important}.w-50{width:50%!important}.w-75{width:75%!important}.w-100{width:100%!important}.w-auto{width:auto!important}.mw-100{max-width:100%!important}.vw-100{width:100vw!important}.min-vw-100{min-width:100vw!important}.h-25{height:25%!important}.h-50{height:50%!important}.h-75{height:75%!important}.h-100{height:100%!important}.h-auto{height:auto!important}.mh-100{max-height:100%!important}.vh-100{height:100vh!important}.min-vh-100{min-height:100vh!important}.flex-fill{flex:1 1 auto!important}.flex-row{flex-direction:row!important}.flex-column{flex-direction:column!important}.flex-row-reverse{flex-direction:row-reverse!important}.flex-column-reverse{flex-direction:column-reverse!important}.flex-grow-0{flex-grow:0!important}.flex-grow-1{flex-grow:1!important}.flex-shrink-0{flex-shrink:0!important}.flex-shrink-1{flex-shrink:1!important}.flex-wrap{flex-wrap:wrap!important}.flex-nowrap{flex-wrap:nowrap!important}.flex-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-0{gap:0!important}.gap-1{gap:.25rem!important}.gap-2{gap:.5rem!important}.gap-3{gap:1rem!important}.gap-4{gap:1.5rem!important}.gap-5{gap:3rem!important}.justify-content-start{justify-content:flex-start!important}.justify-content-end{justify-content:flex-end!important}.justify-content-center{justify-content:center!important}.justify-content-between{justify-content:space-between!important}.justify-content-around{justify-content:space-around!important}.justify-content-evenly{justify-content:space-evenly!important}.align-items-start{align-items:flex-start!important}.align-items-end{align-items:flex-end!important}.align-items-center{align-items:center!important}.align-items-baseline{align-items:baseline!important}.align-items-stretch{align-items:stretch!important}.align-content-start{align-content:flex-start!important}.align-content-end{align-content:flex-end!important}.align-content-center{align-content:center!important}.align-content-between{align-content:space-between!important}.align-content-around{align-content:space-around!important}.align-content-stretch{align-content:stretch!important}.align-self-auto{align-self:auto!important}.align-self-start{align-self:flex-start!important}.align-self-end{align-self:flex-end!important}.align-self-center{align-self:center!important}.align-self-baseline{align-self:baseline!important}.align-self-stretch{align-self:stretch!important}.order-first{order:-1!important}.order-0{order:0!important}.order-1{order:1!important}.order-2{order:2!important}.order-3{order:3!important}.order-4{order:4!important}.order-5{order:5!important}.order-last{order:6!important}.m-0{margin:0!important}.m-1{margin:.25rem!important}.m-2{margin:.5rem!important}.m-3{margin:1rem!important}.m-4{margin:1.5rem!important}.m-5{margin:3rem!important}.m-auto{margin:auto!important}.mx-0{margin-right:0!important;margin-left:0!important}.mx-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-3{margin-right:1rem!important;margin-left:1rem!important}.mx-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-5{margin-right:3rem!important;margin-left:3rem!important}.mx-auto{margin-right:auto!important;margin-left:auto!important}.my-0{margin-top:0!important;margin-bottom:0!important}.my-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-0{margin-top:0!important}.mt-1{margin-top:.25rem!important}.mt-2{margin-top:.5rem!important}.mt-3{margin-top:1rem!important}.mt-4{margin-top:1.5rem!important}.mt-5{margin-top:3rem!important}.mt-auto{margin-top:auto!important}.me-0{margin-right:0!important}.me-1{margin-right:.25rem!important}.me-2{margin-right:.5rem!important}.me-3{margin-right:1rem!important}.me-4{margin-right:1.5rem!important}.me-5{margin-right:3rem!important}.me-auto{margin-right:auto!important}.mb-0{margin-bottom:0!important}.mb-1{margin-bottom:.25rem!important}.mb-2{margin-bottom:.5rem!important}.mb-3{margin-bottom:1rem!important}.mb-4{margin-bottom:1.5rem!important}.mb-5{margin-bottom:3rem!important}.mb-auto{margin-bottom:auto!important}.ms-0{margin-left:0!important}.ms-1{margin-left:.25rem!important}.ms-2{margin-left:.5rem!important}.ms-3{margin-left:1rem!important}.ms-4{margin-left:1.5rem!important}.ms-5{margin-left:3rem!important}.ms-auto{margin-left:auto!important}.p-0{padding:0!important}.p-1{padding:.25rem!important}.p-2{padding:.5rem!important}.p-3{padding:1rem!important}.p-4{padding:1.5rem!important}.p-5{padding:3rem!important}.px-0{padding-right:0!important;padding-left:0!important}.px-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-3{padding-right:1rem!important;padding-left:1rem!important}.px-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-5{padding-right:3rem!important;padding-left:3rem!important}.py-0{padding-top:0!important;padding-bottom:0!important}.py-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-0{padding-top:0!important}.pt-1{padding-top:.25rem!important}.pt-2{padding-top:.5rem!important}.pt-3{padding-top:1rem!important}.pt-4{padding-top:1.5rem!important}.pt-5{padding-top:3rem!important}.pe-0{padding-right:0!important}.pe-1{padding-right:.25rem!important}.pe-2{padding-right:.5rem!important}.pe-3{padding-right:1rem!important}.pe-4{padding-right:1.5rem!important}.pe-5{padding-right:3rem!important}.pb-0{padding-bottom:0!important}.pb-1{padding-bottom:.25rem!important}.pb-2{padding-bottom:.5rem!important}.pb-3{padding-bottom:1rem!important}.pb-4{padding-bottom:1.5rem!important}.pb-5{padding-bottom:3rem!important}.ps-0{padding-left:0!important}.ps-1{padding-left:.25rem!important}.ps-2{padding-left:.5rem!important}.ps-3{padding-left:1rem!important}.ps-4{padding-left:1.5rem!important}.ps-5{padding-left:3rem!important}.font-monospace{font-family:var(--bs-font-monospace)!important}.fs-1{font-size:calc(1.375rem + 1.5vw)!important}.fs-2{font-size:calc(1.325rem + .9vw)!important}.fs-3{font-size:calc(1.3rem + .6vw)!important}.fs-4{font-size:calc(1.275rem + .3vw)!important}.fs-5{font-size:1.25rem!important}.fs-6{font-size:1rem!important}.fst-italic{font-style:italic!important}.fst-normal{font-style:normal!important}.fw-light{font-weight:300!important}.fw-lighter{font-weight:lighter!important}.fw-normal{font-weight:400!important}.fw-bold{font-weight:700!important}.fw-bolder{font-weight:bolder!important}.lh-1{line-height:1!important}.lh-sm{line-height:1.25!important}.lh-base{line-height:1.5!important}.lh-lg{line-height:2!important}.text-start{text-align:left!important}.text-end{text-align:right!important}.text-center{text-align:center!important}.text-decoration-none{text-decoration:none!important}.text-decoration-underline{text-decoration:underline!important}.text-decoration-line-through{text-decoration:line-through!important}.text-lowercase{text-transform:lowercase!important}.text-uppercase{text-transform:uppercase!important}.text-capitalize{text-transform:capitalize!important}.text-wrap{white-space:normal!important}.text-nowrap{white-space:nowrap!important}.text-break{word-wrap:break-word!important;word-break:break-word!important}.text-primary{--bs-text-opacity:1;color:rgba(var(--bs-primary-rgb),var(--bs-text-opacity))!important}.text-secondary{--bs-text-opacity:1;color:rgba(var(--bs-secondary-rgb),var(--bs-text-opacity))!important}.text-success{--bs-text-opacity:1;color:rgba(var(--bs-success-rgb),var(--bs-text-opacity))!important}.text-info{--bs-text-opacity:1;color:rgba(var(--bs-info-rgb),var(--bs-text-opacity))!important}.text-warning{--bs-text-opacity:1;color:rgba(var(--bs-warning-rgb),var(--bs-text-opacity))!important}.text-danger{--bs-text-opacity:1;color:rgba(var(--bs-danger-rgb),var(--bs-text-opacity))!important}.text-light{--bs-text-opacity:1;color:rgba(var(--bs-light-rgb),var(--bs-text-opacity))!important}.text-dark{--bs-text-opacity:1;color:rgba(var(--bs-dark-rgb),var(--bs-text-opacity))!important}.text-black{--bs-text-opacity:1;color:rgba(var(--bs-black-rgb),var(--bs-text-opacity))!important}.text-white{--bs-text-opacity:1;color:rgba(var(--bs-white-rgb),var(--bs-text-opacity))!important}.text-body{--bs-text-opacity:1;color:rgba(var(--bs-body-color-rgb),var(--bs-text-opacity))!important}.text-muted{--bs-text-opacity:1;color:#6c757d!important}.text-black-50{--bs-text-opacity:1;color:rgba(0,0,0,.5)!important}.text-white-50{--bs-text-opacity:1;color:rgba(255,255,255,.5)!important}.text-reset{--bs-text-opacity:1;color:inherit!important}.text-opacity-25{--bs-text-opacity:0.25}.text-opacity-50{--bs-text-opacity:0.5}.text-opacity-75{--bs-text-opacity:0.75}.text-opacity-100{--bs-text-opacity:1}.bg-primary{--bs-bg-opacity:1;background-color:rgba(var(--bs-primary-rgb),var(--bs-bg-opacity))!important}.bg-secondary{--bs-bg-opacity:1;background-color:rgba(var(--bs-secondary-rgb),var(--bs-bg-opacity))!important}.bg-success{--bs-bg-opacity:1;background-color:rgba(var(--bs-success-rgb),var(--bs-bg-opacity))!important}.bg-info{--bs-bg-opacity:1;background-color:rgba(var(--bs-info-rgb),var(--bs-bg-opacity))!important}.bg-warning{--bs-bg-opacity:1;background-color:rgba(var(--bs-warning-rgb),var(--bs-bg-opacity))!important}.bg-danger{--bs-bg-opacity:1;background-color:rgba(var(--bs-danger-rgb),var(--bs-bg-opacity))!important}.bg-light{--bs-bg-opacity:1;background-color:rgba(var(--bs-light-rgb),var(--bs-bg-opacity))!important}.bg-dark{--bs-bg-opacity:1;background-color:rgba(var(--bs-dark-rgb),var(--bs-bg-opacity))!important}.bg-black{--bs-bg-opacity:1;background-color:rgba(var(--bs-black-rgb),var(--bs-bg-opacity))!important}.bg-white{--bs-bg-opacity:1;background-color:rgba(var(--bs-white-rgb),var(--bs-bg-opacity))!important}.bg-body{--bs-bg-opacity:1;background-color:rgba(var(--bs-body-bg-rgb),var(--bs-bg-opacity))!important}.bg-transparent{--bs-bg-opacity:1;background-color:transparent!important}.bg-opacity-10{--bs-bg-opacity:0.1}.bg-opacity-25{--bs-bg-opacity:0.25}.bg-opacity-50{--bs-bg-opacity:0.5}.bg-opacity-75{--bs-bg-opacity:0.75}.bg-opacity-100{--bs-bg-opacity:1}.bg-gradient{background-image:var(--bs-gradient)!important}.user-select-all{-webkit-user-select:all!important;-moz-user-select:all!important;user-select:all!important}.user-select-auto{-webkit-user-select:auto!important;-moz-user-select:auto!important;user-select:auto!important}.user-select-none{-webkit-user-select:none!important;-moz-user-select:none!important;user-select:none!important}.pe-none{pointer-events:none!important}.pe-auto{pointer-events:auto!important}.rounded{border-radius:.25rem!important}.rounded-0{border-radius:0!important}.rounded-1{border-radius:.2rem!important}.rounded-2{border-radius:.25rem!important}.rounded-3{border-radius:.3rem!important}.rounded-circle{border-radius:50%!important}.rounded-pill{border-radius:50rem!important}.rounded-top{border-top-left-radius:.25rem!important;border-top-right-radius:.25rem!important}.rounded-end{border-top-right-radius:.25rem!important;border-bottom-right-radius:.25rem!important}.rounded-bottom{border-bottom-right-radius:.25rem!important;border-bottom-left-radius:.25rem!important}.rounded-start{border-bottom-left-radius:.25rem!important;border-top-left-radius:.25rem!important}.visible{visibility:visible!important}.invisible{visibility:hidden!important}@media (min-width:576px){.float-sm-start{float:left!important}.float-sm-end{float:right!important}.float-sm-none{float:none!important}.d-sm-inline{display:inline!important}.d-sm-inline-block{display:inline-block!important}.d-sm-block{display:block!important}.d-sm-grid{display:grid!important}.d-sm-table{display:table!important}.d-sm-table-row{display:table-row!important}.d-sm-table-cell{display:table-cell!important}.d-sm-flex{display:flex!important}.d-sm-inline-flex{display:inline-flex!important}.d-sm-none{display:none!important}.flex-sm-fill{flex:1 1 auto!important}.flex-sm-row{flex-direction:row!important}.flex-sm-column{flex-direction:column!important}.flex-sm-row-reverse{flex-direction:row-reverse!important}.flex-sm-column-reverse{flex-direction:column-reverse!important}.flex-sm-grow-0{flex-grow:0!important}.flex-sm-grow-1{flex-grow:1!important}.flex-sm-shrink-0{flex-shrink:0!important}.flex-sm-shrink-1{flex-shrink:1!important}.flex-sm-wrap{flex-wrap:wrap!important}.flex-sm-nowrap{flex-wrap:nowrap!important}.flex-sm-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-sm-0{gap:0!important}.gap-sm-1{gap:.25rem!important}.gap-sm-2{gap:.5rem!important}.gap-sm-3{gap:1rem!important}.gap-sm-4{gap:1.5rem!important}.gap-sm-5{gap:3rem!important}.justify-content-sm-start{justify-content:flex-start!important}.justify-content-sm-end{justify-content:flex-end!important}.justify-content-sm-center{justify-content:center!important}.justify-content-sm-between{justify-content:space-between!important}.justify-content-sm-around{justify-content:space-around!important}.justify-content-sm-evenly{justify-content:space-evenly!important}.align-items-sm-start{align-items:flex-start!important}.align-items-sm-end{align-items:flex-end!important}.align-items-sm-center{align-items:center!important}.align-items-sm-baseline{align-items:baseline!important}.align-items-sm-stretch{align-items:stretch!important}.align-content-sm-start{align-content:flex-start!important}.align-content-sm-end{align-content:flex-end!important}.align-content-sm-center{align-content:center!important}.align-content-sm-between{align-content:space-between!important}.align-content-sm-around{align-content:space-around!important}.align-content-sm-stretch{align-content:stretch!important}.align-self-sm-auto{align-self:auto!important}.align-self-sm-start{align-self:flex-start!important}.align-self-sm-end{align-self:flex-end!important}.align-self-sm-center{align-self:center!important}.align-self-sm-baseline{align-self:baseline!important}.align-self-sm-stretch{align-self:stretch!important}.order-sm-first{order:-1!important}.order-sm-0{order:0!important}.order-sm-1{order:1!important}.order-sm-2{order:2!important}.order-sm-3{order:3!important}.order-sm-4{order:4!important}.order-sm-5{order:5!important}.order-sm-last{order:6!important}.m-sm-0{margin:0!important}.m-sm-1{margin:.25rem!important}.m-sm-2{margin:.5rem!important}.m-sm-3{margin:1rem!important}.m-sm-4{margin:1.5rem!important}.m-sm-5{margin:3rem!important}.m-sm-auto{margin:auto!important}.mx-sm-0{margin-right:0!important;margin-left:0!important}.mx-sm-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-sm-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-sm-3{margin-right:1rem!important;margin-left:1rem!important}.mx-sm-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-sm-5{margin-right:3rem!important;margin-left:3rem!important}.mx-sm-auto{margin-right:auto!important;margin-left:auto!important}.my-sm-0{margin-top:0!important;margin-bottom:0!important}.my-sm-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-sm-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-sm-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-sm-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-sm-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-sm-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-sm-0{margin-top:0!important}.mt-sm-1{margin-top:.25rem!important}.mt-sm-2{margin-top:.5rem!important}.mt-sm-3{margin-top:1rem!important}.mt-sm-4{margin-top:1.5rem!important}.mt-sm-5{margin-top:3rem!important}.mt-sm-auto{margin-top:auto!important}.me-sm-0{margin-right:0!important}.me-sm-1{margin-right:.25rem!important}.me-sm-2{margin-right:.5rem!important}.me-sm-3{margin-right:1rem!important}.me-sm-4{margin-right:1.5rem!important}.me-sm-5{margin-right:3rem!important}.me-sm-auto{margin-right:auto!important}.mb-sm-0{margin-bottom:0!important}.mb-sm-1{margin-bottom:.25rem!important}.mb-sm-2{margin-bottom:.5rem!important}.mb-sm-3{margin-bottom:1rem!important}.mb-sm-4{margin-bottom:1.5rem!important}.mb-sm-5{margin-bottom:3rem!important}.mb-sm-auto{margin-bottom:auto!important}.ms-sm-0{margin-left:0!important}.ms-sm-1{margin-left:.25rem!important}.ms-sm-2{margin-left:.5rem!important}.ms-sm-3{margin-left:1rem!important}.ms-sm-4{margin-left:1.5rem!important}.ms-sm-5{margin-left:3rem!important}.ms-sm-auto{margin-left:auto!important}.p-sm-0{padding:0!important}.p-sm-1{padding:.25rem!important}.p-sm-2{padding:.5rem!important}.p-sm-3{padding:1rem!important}.p-sm-4{padding:1.5rem!important}.p-sm-5{padding:3rem!important}.px-sm-0{padding-right:0!important;padding-left:0!important}.px-sm-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-sm-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-sm-3{padding-right:1rem!important;padding-left:1rem!important}.px-sm-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-sm-5{padding-right:3rem!important;padding-left:3rem!important}.py-sm-0{padding-top:0!important;padding-bottom:0!important}.py-sm-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-sm-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-sm-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-sm-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-sm-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-sm-0{padding-top:0!important}.pt-sm-1{padding-top:.25rem!important}.pt-sm-2{padding-top:.5rem!important}.pt-sm-3{padding-top:1rem!important}.pt-sm-4{padding-top:1.5rem!important}.pt-sm-5{padding-top:3rem!important}.pe-sm-0{padding-right:0!important}.pe-sm-1{padding-right:.25rem!important}.pe-sm-2{padding-right:.5rem!important}.pe-sm-3{padding-right:1rem!important}.pe-sm-4{padding-right:1.5rem!important}.pe-sm-5{padding-right:3rem!important}.pb-sm-0{padding-bottom:0!important}.pb-sm-1{padding-bottom:.25rem!important}.pb-sm-2{padding-bottom:.5rem!important}.pb-sm-3{padding-bottom:1rem!important}.pb-sm-4{padding-bottom:1.5rem!important}.pb-sm-5{padding-bottom:3rem!important}.ps-sm-0{padding-left:0!important}.ps-sm-1{padding-left:.25rem!important}.ps-sm-2{padding-left:.5rem!important}.ps-sm-3{padding-left:1rem!important}.ps-sm-4{padding-left:1.5rem!important}.ps-sm-5{padding-left:3rem!important}.text-sm-start{text-align:left!important}.text-sm-end{text-align:right!important}.text-sm-center{text-align:center!important}}@media (min-width:768px){.float-md-start{float:left!important}.float-md-end{float:right!important}.float-md-none{float:none!important}.d-md-inline{display:inline!important}.d-md-inline-block{display:inline-block!important}.d-md-block{display:block!important}.d-md-grid{display:grid!important}.d-md-table{display:table!important}.d-md-table-row{display:table-row!important}.d-md-table-cell{display:table-cell!important}.d-md-flex{display:flex!important}.d-md-inline-flex{display:inline-flex!important}.d-md-none{display:none!important}.flex-md-fill{flex:1 1 auto!important}.flex-md-row{flex-direction:row!important}.flex-md-column{flex-direction:column!important}.flex-md-row-reverse{flex-direction:row-reverse!important}.flex-md-column-reverse{flex-direction:column-reverse!important}.flex-md-grow-0{flex-grow:0!important}.flex-md-grow-1{flex-grow:1!important}.flex-md-shrink-0{flex-shrink:0!important}.flex-md-shrink-1{flex-shrink:1!important}.flex-md-wrap{flex-wrap:wrap!important}.flex-md-nowrap{flex-wrap:nowrap!important}.flex-md-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-md-0{gap:0!important}.gap-md-1{gap:.25rem!important}.gap-md-2{gap:.5rem!important}.gap-md-3{gap:1rem!important}.gap-md-4{gap:1.5rem!important}.gap-md-5{gap:3rem!important}.justify-content-md-start{justify-content:flex-start!important}.justify-content-md-end{justify-content:flex-end!important}.justify-content-md-center{justify-content:center!important}.justify-content-md-between{justify-content:space-between!important}.justify-content-md-around{justify-content:space-around!important}.justify-content-md-evenly{justify-content:space-evenly!important}.align-items-md-start{align-items:flex-start!important}.align-items-md-end{align-items:flex-end!important}.align-items-md-center{align-items:center!important}.align-items-md-baseline{align-items:baseline!important}.align-items-md-stretch{align-items:stretch!important}.align-content-md-start{align-content:flex-start!important}.align-content-md-end{align-content:flex-end!important}.align-content-md-center{align-content:center!important}.align-content-md-between{align-content:space-between!important}.align-content-md-around{align-content:space-around!important}.align-content-md-stretch{align-content:stretch!important}.align-self-md-auto{align-self:auto!important}.align-self-md-start{align-self:flex-start!important}.align-self-md-end{align-self:flex-end!important}.align-self-md-center{align-self:center!important}.align-self-md-baseline{align-self:baseline!important}.align-self-md-stretch{align-self:stretch!important}.order-md-first{order:-1!important}.order-md-0{order:0!important}.order-md-1{order:1!important}.order-md-2{order:2!important}.order-md-3{order:3!important}.order-md-4{order:4!important}.order-md-5{order:5!important}.order-md-last{order:6!important}.m-md-0{margin:0!important}.m-md-1{margin:.25rem!important}.m-md-2{margin:.5rem!important}.m-md-3{margin:1rem!important}.m-md-4{margin:1.5rem!important}.m-md-5{margin:3rem!important}.m-md-auto{margin:auto!important}.mx-md-0{margin-right:0!important;margin-left:0!important}.mx-md-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-md-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-md-3{margin-right:1rem!important;margin-left:1rem!important}.mx-md-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-md-5{margin-right:3rem!important;margin-left:3rem!important}.mx-md-auto{margin-right:auto!important;margin-left:auto!important}.my-md-0{margin-top:0!important;margin-bottom:0!important}.my-md-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-md-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-md-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-md-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-md-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-md-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-md-0{margin-top:0!important}.mt-md-1{margin-top:.25rem!important}.mt-md-2{margin-top:.5rem!important}.mt-md-3{margin-top:1rem!important}.mt-md-4{margin-top:1.5rem!important}.mt-md-5{margin-top:3rem!important}.mt-md-auto{margin-top:auto!important}.me-md-0{margin-right:0!important}.me-md-1{margin-right:.25rem!important}.me-md-2{margin-right:.5rem!important}.me-md-3{margin-right:1rem!important}.me-md-4{margin-right:1.5rem!important}.me-md-5{margin-right:3rem!important}.me-md-auto{margin-right:auto!important}.mb-md-0{margin-bottom:0!important}.mb-md-1{margin-bottom:.25rem!important}.mb-md-2{margin-bottom:.5rem!important}.mb-md-3{margin-bottom:1rem!important}.mb-md-4{margin-bottom:1.5rem!important}.mb-md-5{margin-bottom:3rem!important}.mb-md-auto{margin-bottom:auto!important}.ms-md-0{margin-left:0!important}.ms-md-1{margin-left:.25rem!important}.ms-md-2{margin-left:.5rem!important}.ms-md-3{margin-left:1rem!important}.ms-md-4{margin-left:1.5rem!important}.ms-md-5{margin-left:3rem!important}.ms-md-auto{margin-left:auto!important}.p-md-0{padding:0!important}.p-md-1{padding:.25rem!important}.p-md-2{padding:.5rem!important}.p-md-3{padding:1rem!important}.p-md-4{padding:1.5rem!important}.p-md-5{padding:3rem!important}.px-md-0{padding-right:0!important;padding-left:0!important}.px-md-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-md-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-md-3{padding-right:1rem!important;padding-left:1rem!important}.px-md-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-md-5{padding-right:3rem!important;padding-left:3rem!important}.py-md-0{padding-top:0!important;padding-bottom:0!important}.py-md-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-md-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-md-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-md-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-md-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-md-0{padding-top:0!important}.pt-md-1{padding-top:.25rem!important}.pt-md-2{padding-top:.5rem!important}.pt-md-3{padding-top:1rem!important}.pt-md-4{padding-top:1.5rem!important}.pt-md-5{padding-top:3rem!important}.pe-md-0{padding-right:0!important}.pe-md-1{padding-right:.25rem!important}.pe-md-2{padding-right:.5rem!important}.pe-md-3{padding-right:1rem!important}.pe-md-4{padding-right:1.5rem!important}.pe-md-5{padding-right:3rem!important}.pb-md-0{padding-bottom:0!important}.pb-md-1{padding-bottom:.25rem!important}.pb-md-2{padding-bottom:.5rem!important}.pb-md-3{padding-bottom:1rem!important}.pb-md-4{padding-bottom:1.5rem!important}.pb-md-5{padding-bottom:3rem!important}.ps-md-0{padding-left:0!important}.ps-md-1{padding-left:.25rem!important}.ps-md-2{padding-left:.5rem!important}.ps-md-3{padding-left:1rem!important}.ps-md-4{padding-left:1.5rem!important}.ps-md-5{padding-left:3rem!important}.text-md-start{text-align:left!important}.text-md-end{text-align:right!important}.text-md-center{text-align:center!important}}@media (min-width:992px){.float-lg-start{float:left!important}.float-lg-end{float:right!important}.float-lg-none{float:none!important}.d-lg-inline{display:inline!important}.d-lg-inline-block{display:inline-block!important}.d-lg-block{display:block!important}.d-lg-grid{display:grid!important}.d-lg-table{display:table!important}.d-lg-table-row{display:table-row!important}.d-lg-table-cell{display:table-cell!important}.d-lg-flex{display:flex!important}.d-lg-inline-flex{display:inline-flex!important}.d-lg-none{display:none!important}.flex-lg-fill{flex:1 1 auto!important}.flex-lg-row{flex-direction:row!important}.flex-lg-column{flex-direction:column!important}.flex-lg-row-reverse{flex-direction:row-reverse!important}.flex-lg-column-reverse{flex-direction:column-reverse!important}.flex-lg-grow-0{flex-grow:0!important}.flex-lg-grow-1{flex-grow:1!important}.flex-lg-shrink-0{flex-shrink:0!important}.flex-lg-shrink-1{flex-shrink:1!important}.flex-lg-wrap{flex-wrap:wrap!important}.flex-lg-nowrap{flex-wrap:nowrap!important}.flex-lg-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-lg-0{gap:0!important}.gap-lg-1{gap:.25rem!important}.gap-lg-2{gap:.5rem!important}.gap-lg-3{gap:1rem!important}.gap-lg-4{gap:1.5rem!important}.gap-lg-5{gap:3rem!important}.justify-content-lg-start{justify-content:flex-start!important}.justify-content-lg-end{justify-content:flex-end!important}.justify-content-lg-center{justify-content:center!important}.justify-content-lg-between{justify-content:space-between!important}.justify-content-lg-around{justify-content:space-around!important}.justify-content-lg-evenly{justify-content:space-evenly!important}.align-items-lg-start{align-items:flex-start!important}.align-items-lg-end{align-items:flex-end!important}.align-items-lg-center{align-items:center!important}.align-items-lg-baseline{align-items:baseline!important}.align-items-lg-stretch{align-items:stretch!important}.align-content-lg-start{align-content:flex-start!important}.align-content-lg-end{align-content:flex-end!important}.align-content-lg-center{align-content:center!important}.align-content-lg-between{align-content:space-between!important}.align-content-lg-around{align-content:space-around!important}.align-content-lg-stretch{align-content:stretch!important}.align-self-lg-auto{align-self:auto!important}.align-self-lg-start{align-self:flex-start!important}.align-self-lg-end{align-self:flex-end!important}.align-self-lg-center{align-self:center!important}.align-self-lg-baseline{align-self:baseline!important}.align-self-lg-stretch{align-self:stretch!important}.order-lg-first{order:-1!important}.order-lg-0{order:0!important}.order-lg-1{order:1!important}.order-lg-2{order:2!important}.order-lg-3{order:3!important}.order-lg-4{order:4!important}.order-lg-5{order:5!important}.order-lg-last{order:6!important}.m-lg-0{margin:0!important}.m-lg-1{margin:.25rem!important}.m-lg-2{margin:.5rem!important}.m-lg-3{margin:1rem!important}.m-lg-4{margin:1.5rem!important}.m-lg-5{margin:3rem!important}.m-lg-auto{margin:auto!important}.mx-lg-0{margin-right:0!important;margin-left:0!important}.mx-lg-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-lg-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-lg-3{margin-right:1rem!important;margin-left:1rem!important}.mx-lg-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-lg-5{margin-right:3rem!important;margin-left:3rem!important}.mx-lg-auto{margin-right:auto!important;margin-left:auto!important}.my-lg-0{margin-top:0!important;margin-bottom:0!important}.my-lg-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-lg-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-lg-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-lg-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-lg-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-lg-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-lg-0{margin-top:0!important}.mt-lg-1{margin-top:.25rem!important}.mt-lg-2{margin-top:.5rem!important}.mt-lg-3{margin-top:1rem!important}.mt-lg-4{margin-top:1.5rem!important}.mt-lg-5{margin-top:3rem!important}.mt-lg-auto{margin-top:auto!important}.me-lg-0{margin-right:0!important}.me-lg-1{margin-right:.25rem!important}.me-lg-2{margin-right:.5rem!important}.me-lg-3{margin-right:1rem!important}.me-lg-4{margin-right:1.5rem!important}.me-lg-5{margin-right:3rem!important}.me-lg-auto{margin-right:auto!important}.mb-lg-0{margin-bottom:0!important}.mb-lg-1{margin-bottom:.25rem!important}.mb-lg-2{margin-bottom:.5rem!important}.mb-lg-3{margin-bottom:1rem!important}.mb-lg-4{margin-bottom:1.5rem!important}.mb-lg-5{margin-bottom:3rem!important}.mb-lg-auto{margin-bottom:auto!important}.ms-lg-0{margin-left:0!important}.ms-lg-1{margin-left:.25rem!important}.ms-lg-2{margin-left:.5rem!important}.ms-lg-3{margin-left:1rem!important}.ms-lg-4{margin-left:1.5rem!important}.ms-lg-5{margin-left:3rem!important}.ms-lg-auto{margin-left:auto!important}.p-lg-0{padding:0!important}.p-lg-1{padding:.25rem!important}.p-lg-2{padding:.5rem!important}.p-lg-3{padding:1rem!important}.p-lg-4{padding:1.5rem!important}.p-lg-5{padding:3rem!important}.px-lg-0{padding-right:0!important;padding-left:0!important}.px-lg-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-lg-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-lg-3{padding-right:1rem!important;padding-left:1rem!important}.px-lg-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-lg-5{padding-right:3rem!important;padding-left:3rem!important}.py-lg-0{padding-top:0!important;padding-bottom:0!important}.py-lg-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-lg-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-lg-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-lg-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-lg-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-lg-0{padding-top:0!important}.pt-lg-1{padding-top:.25rem!important}.pt-lg-2{padding-top:.5rem!important}.pt-lg-3{padding-top:1rem!important}.pt-lg-4{padding-top:1.5rem!important}.pt-lg-5{padding-top:3rem!important}.pe-lg-0{padding-right:0!important}.pe-lg-1{padding-right:.25rem!important}.pe-lg-2{padding-right:.5rem!important}.pe-lg-3{padding-right:1rem!important}.pe-lg-4{padding-right:1.5rem!important}.pe-lg-5{padding-right:3rem!important}.pb-lg-0{padding-bottom:0!important}.pb-lg-1{padding-bottom:.25rem!important}.pb-lg-2{padding-bottom:.5rem!important}.pb-lg-3{padding-bottom:1rem!important}.pb-lg-4{padding-bottom:1.5rem!important}.pb-lg-5{padding-bottom:3rem!important}.ps-lg-0{padding-left:0!important}.ps-lg-1{padding-left:.25rem!important}.ps-lg-2{padding-left:.5rem!important}.ps-lg-3{padding-left:1rem!important}.ps-lg-4{padding-left:1.5rem!important}.ps-lg-5{padding-left:3rem!important}.text-lg-start{text-align:left!important}.text-lg-end{text-align:right!important}.text-lg-center{text-align:center!important}}@media (min-width:1200px){.float-xl-start{float:left!important}.float-xl-end{float:right!important}.float-xl-none{float:none!important}.d-xl-inline{display:inline!important}.d-xl-inline-block{display:inline-block!important}.d-xl-block{display:block!important}.d-xl-grid{display:grid!important}.d-xl-table{display:table!important}.d-xl-table-row{display:table-row!important}.d-xl-table-cell{display:table-cell!important}.d-xl-flex{display:flex!important}.d-xl-inline-flex{display:inline-flex!important}.d-xl-none{display:none!important}.flex-xl-fill{flex:1 1 auto!important}.flex-xl-row{flex-direction:row!important}.flex-xl-column{flex-direction:column!important}.flex-xl-row-reverse{flex-direction:row-reverse!important}.flex-xl-column-reverse{flex-direction:column-reverse!important}.flex-xl-grow-0{flex-grow:0!important}.flex-xl-grow-1{flex-grow:1!important}.flex-xl-shrink-0{flex-shrink:0!important}.flex-xl-shrink-1{flex-shrink:1!important}.flex-xl-wrap{flex-wrap:wrap!important}.flex-xl-nowrap{flex-wrap:nowrap!important}.flex-xl-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-xl-0{gap:0!important}.gap-xl-1{gap:.25rem!important}.gap-xl-2{gap:.5rem!important}.gap-xl-3{gap:1rem!important}.gap-xl-4{gap:1.5rem!important}.gap-xl-5{gap:3rem!important}.justify-content-xl-start{justify-content:flex-start!important}.justify-content-xl-end{justify-content:flex-end!important}.justify-content-xl-center{justify-content:center!important}.justify-content-xl-between{justify-content:space-between!important}.justify-content-xl-around{justify-content:space-around!important}.justify-content-xl-evenly{justify-content:space-evenly!important}.align-items-xl-start{align-items:flex-start!important}.align-items-xl-end{align-items:flex-end!important}.align-items-xl-center{align-items:center!important}.align-items-xl-baseline{align-items:baseline!important}.align-items-xl-stretch{align-items:stretch!important}.align-content-xl-start{align-content:flex-start!important}.align-content-xl-end{align-content:flex-end!important}.align-content-xl-center{align-content:center!important}.align-content-xl-between{align-content:space-between!important}.align-content-xl-around{align-content:space-around!important}.align-content-xl-stretch{align-content:stretch!important}.align-self-xl-auto{align-self:auto!important}.align-self-xl-start{align-self:flex-start!important}.align-self-xl-end{align-self:flex-end!important}.align-self-xl-center{align-self:center!important}.align-self-xl-baseline{align-self:baseline!important}.align-self-xl-stretch{align-self:stretch!important}.order-xl-first{order:-1!important}.order-xl-0{order:0!important}.order-xl-1{order:1!important}.order-xl-2{order:2!important}.order-xl-3{order:3!important}.order-xl-4{order:4!important}.order-xl-5{order:5!important}.order-xl-last{order:6!important}.m-xl-0{margin:0!important}.m-xl-1{margin:.25rem!important}.m-xl-2{margin:.5rem!important}.m-xl-3{margin:1rem!important}.m-xl-4{margin:1.5rem!important}.m-xl-5{margin:3rem!important}.m-xl-auto{margin:auto!important}.mx-xl-0{margin-right:0!important;margin-left:0!important}.mx-xl-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-xl-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-xl-3{margin-right:1rem!important;margin-left:1rem!important}.mx-xl-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-xl-5{margin-right:3rem!important;margin-left:3rem!important}.mx-xl-auto{margin-right:auto!important;margin-left:auto!important}.my-xl-0{margin-top:0!important;margin-bottom:0!important}.my-xl-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-xl-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-xl-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-xl-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-xl-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-xl-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-xl-0{margin-top:0!important}.mt-xl-1{margin-top:.25rem!important}.mt-xl-2{margin-top:.5rem!important}.mt-xl-3{margin-top:1rem!important}.mt-xl-4{margin-top:1.5rem!important}.mt-xl-5{margin-top:3rem!important}.mt-xl-auto{margin-top:auto!important}.me-xl-0{margin-right:0!important}.me-xl-1{margin-right:.25rem!important}.me-xl-2{margin-right:.5rem!important}.me-xl-3{margin-right:1rem!important}.me-xl-4{margin-right:1.5rem!important}.me-xl-5{margin-right:3rem!important}.me-xl-auto{margin-right:auto!important}.mb-xl-0{margin-bottom:0!important}.mb-xl-1{margin-bottom:.25rem!important}.mb-xl-2{margin-bottom:.5rem!important}.mb-xl-3{margin-bottom:1rem!important}.mb-xl-4{margin-bottom:1.5rem!important}.mb-xl-5{margin-bottom:3rem!important}.mb-xl-auto{margin-bottom:auto!important}.ms-xl-0{margin-left:0!important}.ms-xl-1{margin-left:.25rem!important}.ms-xl-2{margin-left:.5rem!important}.ms-xl-3{margin-left:1rem!important}.ms-xl-4{margin-left:1.5rem!important}.ms-xl-5{margin-left:3rem!important}.ms-xl-auto{margin-left:auto!important}.p-xl-0{padding:0!important}.p-xl-1{padding:.25rem!important}.p-xl-2{padding:.5rem!important}.p-xl-3{padding:1rem!important}.p-xl-4{padding:1.5rem!important}.p-xl-5{padding:3rem!important}.px-xl-0{padding-right:0!important;padding-left:0!important}.px-xl-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-xl-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-xl-3{padding-right:1rem!important;padding-left:1rem!important}.px-xl-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-xl-5{padding-right:3rem!important;padding-left:3rem!important}.py-xl-0{padding-top:0!important;padding-bottom:0!important}.py-xl-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-xl-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-xl-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-xl-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-xl-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-xl-0{padding-top:0!important}.pt-xl-1{padding-top:.25rem!important}.pt-xl-2{padding-top:.5rem!important}.pt-xl-3{padding-top:1rem!important}.pt-xl-4{padding-top:1.5rem!important}.pt-xl-5{padding-top:3rem!important}.pe-xl-0{padding-right:0!important}.pe-xl-1{padding-right:.25rem!important}.pe-xl-2{padding-right:.5rem!important}.pe-xl-3{padding-right:1rem!important}.pe-xl-4{padding-right:1.5rem!important}.pe-xl-5{padding-right:3rem!important}.pb-xl-0{padding-bottom:0!important}.pb-xl-1{padding-bottom:.25rem!important}.pb-xl-2{padding-bottom:.5rem!important}.pb-xl-3{padding-bottom:1rem!important}.pb-xl-4{padding-bottom:1.5rem!important}.pb-xl-5{padding-bottom:3rem!important}.ps-xl-0{padding-left:0!important}.ps-xl-1{padding-left:.25rem!important}.ps-xl-2{padding-left:.5rem!important}.ps-xl-3{padding-left:1rem!important}.ps-xl-4{padding-left:1.5rem!important}.ps-xl-5{padding-left:3rem!important}.text-xl-start{text-align:left!important}.text-xl-end{text-align:right!important}.text-xl-center{text-align:center!important}}@media (min-width:1400px){.float-xxl-start{float:left!important}.float-xxl-end{float:right!important}.float-xxl-none{float:none!important}.d-xxl-inline{display:inline!important}.d-xxl-inline-block{display:inline-block!important}.d-xxl-block{display:block!important}.d-xxl-grid{display:grid!important}.d-xxl-table{display:table!important}.d-xxl-table-row{display:table-row!important}.d-xxl-table-cell{display:table-cell!important}.d-xxl-flex{display:flex!important}.d-xxl-inline-flex{display:inline-flex!important}.d-xxl-none{display:none!important}.flex-xxl-fill{flex:1 1 auto!important}.flex-xxl-row{flex-direction:row!important}.flex-xxl-column{flex-direction:column!important}.flex-xxl-row-reverse{flex-direction:row-reverse!important}.flex-xxl-column-reverse{flex-direction:column-reverse!important}.flex-xxl-grow-0{flex-grow:0!important}.flex-xxl-grow-1{flex-grow:1!important}.flex-xxl-shrink-0{flex-shrink:0!important}.flex-xxl-shrink-1{flex-shrink:1!important}.flex-xxl-wrap{flex-wrap:wrap!important}.flex-xxl-nowrap{flex-wrap:nowrap!important}.flex-xxl-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-xxl-0{gap:0!important}.gap-xxl-1{gap:.25rem!important}.gap-xxl-2{gap:.5rem!important}.gap-xxl-3{gap:1rem!important}.gap-xxl-4{gap:1.5rem!important}.gap-xxl-5{gap:3rem!important}.justify-content-xxl-start{justify-content:flex-start!important}.justify-content-xxl-end{justify-content:flex-end!important}.justify-content-xxl-center{justify-content:center!important}.justify-content-xxl-between{justify-content:space-between!important}.justify-content-xxl-around{justify-content:space-around!important}.justify-content-xxl-evenly{justify-content:space-evenly!important}.align-items-xxl-start{align-items:flex-start!important}.align-items-xxl-end{align-items:flex-end!important}.align-items-xxl-center{align-items:center!important}.align-items-xxl-baseline{align-items:baseline!important}.align-items-xxl-stretch{align-items:stretch!important}.align-content-xxl-start{align-content:flex-start!important}.align-content-xxl-end{align-content:flex-end!important}.align-content-xxl-center{align-content:center!important}.align-content-xxl-between{align-content:space-between!important}.align-content-xxl-around{align-content:space-around!important}.align-content-xxl-stretch{align-content:stretch!important}.align-self-xxl-auto{align-self:auto!important}.align-self-xxl-start{align-self:flex-start!important}.align-self-xxl-end{align-self:flex-end!important}.align-self-xxl-center{align-self:center!important}.align-self-xxl-baseline{align-self:baseline!important}.align-self-xxl-stretch{align-self:stretch!important}.order-xxl-first{order:-1!important}.order-xxl-0{order:0!important}.order-xxl-1{order:1!important}.order-xxl-2{order:2!important}.order-xxl-3{order:3!important}.order-xxl-4{order:4!important}.order-xxl-5{order:5!important}.order-xxl-last{order:6!important}.m-xxl-0{margin:0!important}.m-xxl-1{margin:.25rem!important}.m-xxl-2{margin:.5rem!important}.m-xxl-3{margin:1rem!important}.m-xxl-4{margin:1.5rem!important}.m-xxl-5{margin:3rem!important}.m-xxl-auto{margin:auto!important}.mx-xxl-0{margin-right:0!important;margin-left:0!important}.mx-xxl-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-xxl-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-xxl-3{margin-right:1rem!important;margin-left:1rem!important}.mx-xxl-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-xxl-5{margin-right:3rem!important;margin-left:3rem!important}.mx-xxl-auto{margin-right:auto!important;margin-left:auto!important}.my-xxl-0{margin-top:0!important;margin-bottom:0!important}.my-xxl-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-xxl-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-xxl-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-xxl-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-xxl-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-xxl-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-xxl-0{margin-top:0!important}.mt-xxl-1{margin-top:.25rem!important}.mt-xxl-2{margin-top:.5rem!important}.mt-xxl-3{margin-top:1rem!important}.mt-xxl-4{margin-top:1.5rem!important}.mt-xxl-5{margin-top:3rem!important}.mt-xxl-auto{margin-top:auto!important}.me-xxl-0{margin-right:0!important}.me-xxl-1{margin-right:.25rem!important}.me-xxl-2{margin-right:.5rem!important}.me-xxl-3{margin-right:1rem!important}.me-xxl-4{margin-right:1.5rem!important}.me-xxl-5{margin-right:3rem!important}.me-xxl-auto{margin-right:auto!important}.mb-xxl-0{margin-bottom:0!important}.mb-xxl-1{margin-bottom:.25rem!important}.mb-xxl-2{margin-bottom:.5rem!important}.mb-xxl-3{margin-bottom:1rem!important}.mb-xxl-4{margin-bottom:1.5rem!important}.mb-xxl-5{margin-bottom:3rem!important}.mb-xxl-auto{margin-bottom:auto!important}.ms-xxl-0{margin-left:0!important}.ms-xxl-1{margin-left:.25rem!important}.ms-xxl-2{margin-left:.5rem!important}.ms-xxl-3{margin-left:1rem!important}.ms-xxl-4{margin-left:1.5rem!important}.ms-xxl-5{margin-left:3rem!important}.ms-xxl-auto{margin-left:auto!important}.p-xxl-0{padding:0!important}.p-xxl-1{padding:.25rem!important}.p-xxl-2{padding:.5rem!important}.p-xxl-3{padding:1rem!important}.p-xxl-4{padding:1.5rem!important}.p-xxl-5{padding:3rem!important}.px-xxl-0{padding-right:0!important;padding-left:0!important}.px-xxl-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-xxl-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-xxl-3{padding-right:1rem!important;padding-left:1rem!important}.px-xxl-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-xxl-5{padding-right:3rem!important;padding-left:3rem!important}.py-xxl-0{padding-top:0!important;padding-bottom:0!important}.py-xxl-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-xxl-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-xxl-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-xxl-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-xxl-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-xxl-0{padding-top:0!important}.pt-xxl-1{padding-top:.25rem!important}.pt-xxl-2{padding-top:.5rem!important}.pt-xxl-3{padding-top:1rem!important}.pt-xxl-4{padding-top:1.5rem!important}.pt-xxl-5{padding-top:3rem!important}.pe-xxl-0{padding-right:0!important}.pe-xxl-1{padding-right:.25rem!important}.pe-xxl-2{padding-right:.5rem!important}.pe-xxl-3{padding-right:1rem!important}.pe-xxl-4{padding-right:1.5rem!important}.pe-xxl-5{padding-right:3rem!important}.pb-xxl-0{padding-bottom:0!important}.pb-xxl-1{padding-bottom:.25rem!important}.pb-xxl-2{padding-bottom:.5rem!important}.pb-xxl-3{padding-bottom:1rem!important}.pb-xxl-4{padding-bottom:1.5rem!important}.pb-xxl-5{padding-bottom:3rem!important}.ps-xxl-0{padding-left:0!important}.ps-xxl-1{padding-left:.25rem!important}.ps-xxl-2{padding-left:.5rem!important}.ps-xxl-3{padding-left:1rem!important}.ps-xxl-4{padding-left:1.5rem!important}.ps-xxl-5{padding-left:3rem!important}.text-xxl-start{text-align:left!important}.text-xxl-end{text-align:right!important}.text-xxl-center{text-align:center!important}}@media (min-width:1200px){.fs-1{font-size:2.5rem!important}.fs-2{font-size:2rem!important}.fs-3{font-size:1.75rem!important}.fs-4{font-size:1.5rem!important}}@media print{.d-print-inline{display:inline!important}.d-print-inline-block{display:inline-block!important}.d-print-block{display:block!important}.d-print-grid{display:grid!important}.d-print-table{display:table!important}.d-print-table-row{display:table-row!important}.d-print-table-cell{display:table-cell!important}.d-print-flex{display:flex!important}.d-print-inline-flex{display:inline-flex!important}.d-print-none{display:none!important}} diff --git a/themes/30coffe/assets/css/boxicons.min.css b/themes/30coffe/assets/css/boxicons.min.css new file mode 100644 index 00000000..d0428ef9 --- /dev/null +++ b/themes/30coffe/assets/css/boxicons.min.css @@ -0,0 +1 @@ +@font-face{font-family:boxicons;font-weight:400;font-style:normal;src:url(../fonts/boxicons.eot);src:url(../fonts/boxicons.eot) format('embedded-opentype'),url(../fonts/boxicons.woff2) format('woff2'),url(../fonts/boxicons.woff) format('woff'),url(../fonts/boxicons.ttf) format('truetype'),url(../fonts/boxiconsd41d.svg?#boxicons) format('svg')}.bx{font-family:boxicons!important;font-weight:400;font-style:normal;font-variant:normal;line-height:1;text-rendering:auto;display:inline-block;text-transform:none;speak:none;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.bx-ul{margin-left:2em;padding-left:0;list-style:none}.bx-ul>li{position:relative}.bx-ul .bx{font-size:inherit;line-height:inherit;position:absolute;left:-2em;width:2em;text-align:center}@-webkit-keyframes spin{0%{-webkit-transform:rotate(0);transform:rotate(0)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes spin{0%{-webkit-transform:rotate(0);transform:rotate(0)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@-webkit-keyframes burst{0%{-webkit-transform:scale(1);transform:scale(1);opacity:1}90%{-webkit-transform:scale(1.5);transform:scale(1.5);opacity:0}}@keyframes burst{0%{-webkit-transform:scale(1);transform:scale(1);opacity:1}90%{-webkit-transform:scale(1.5);transform:scale(1.5);opacity:0}}@-webkit-keyframes flashing{0%{opacity:1}45%{opacity:0}90%{opacity:1}}@keyframes flashing{0%{opacity:1}45%{opacity:0}90%{opacity:1}}@-webkit-keyframes fade-left{0%{-webkit-transform:translateX(0);transform:translateX(0);opacity:1}75%{-webkit-transform:translateX(-20px);transform:translateX(-20px);opacity:0}}@keyframes fade-left{0%{-webkit-transform:translateX(0);transform:translateX(0);opacity:1}75%{-webkit-transform:translateX(-20px);transform:translateX(-20px);opacity:0}}@-webkit-keyframes fade-right{0%{-webkit-transform:translateX(0);transform:translateX(0);opacity:1}75%{-webkit-transform:translateX(20px);transform:translateX(20px);opacity:0}}@keyframes fade-right{0%{-webkit-transform:translateX(0);transform:translateX(0);opacity:1}75%{-webkit-transform:translateX(20px);transform:translateX(20px);opacity:0}}@-webkit-keyframes fade-up{0%{-webkit-transform:translateY(0);transform:translateY(0);opacity:1}75%{-webkit-transform:translateY(-20px);transform:translateY(-20px);opacity:0}}@keyframes fade-up{0%{-webkit-transform:translateY(0);transform:translateY(0);opacity:1}75%{-webkit-transform:translateY(-20px);transform:translateY(-20px);opacity:0}}@-webkit-keyframes fade-down{0%{-webkit-transform:translateY(0);transform:translateY(0);opacity:1}75%{-webkit-transform:translateY(20px);transform:translateY(20px);opacity:0}}@keyframes fade-down{0%{-webkit-transform:translateY(0);transform:translateY(0);opacity:1}75%{-webkit-transform:translateY(20px);transform:translateY(20px);opacity:0}}@-webkit-keyframes tada{from{-webkit-transform:scale3d(1,1,1);transform:scale3d(1,1,1)}10%,20%{-webkit-transform:scale3d(.95,.95,.95) rotate3d(0,0,1,-10deg);transform:scale3d(.95,.95,.95) rotate3d(0,0,1,-10deg)}30%,50%,70%,90%{-webkit-transform:scale3d(1,1,1) rotate3d(0,0,1,10deg);transform:scale3d(1,1,1) rotate3d(0,0,1,10deg)}40%,60%,80%{-webkit-transform:scale3d(1,1,1) rotate3d(0,0,1,-10deg);transform:scale3d(1,1,1) rotate3d(0,0,1,-10deg)}to{-webkit-transform:scale3d(1,1,1);transform:scale3d(1,1,1)}}@keyframes tada{from{-webkit-transform:scale3d(1,1,1);transform:scale3d(1,1,1)}10%,20%{-webkit-transform:scale3d(.95,.95,.95) rotate3d(0,0,1,-10deg);transform:scale3d(.95,.95,.95) rotate3d(0,0,1,-10deg)}30%,50%,70%,90%{-webkit-transform:scale3d(1,1,1) rotate3d(0,0,1,10deg);transform:scale3d(1,1,1) rotate3d(0,0,1,10deg)}40%,60%,80%{-webkit-transform:rotate3d(0,0,1,-10deg);transform:rotate3d(0,0,1,-10deg)}to{-webkit-transform:scale3d(1,1,1);transform:scale3d(1,1,1)}}.bx-spin{-webkit-animation:spin 2s linear infinite;animation:spin 2s linear infinite}.bx-spin-hover:hover{-webkit-animation:spin 2s linear infinite;animation:spin 2s linear infinite}.bx-tada{-webkit-animation:tada 1.5s ease infinite;animation:tada 1.5s ease infinite}.bx-tada-hover:hover{-webkit-animation:tada 1.5s ease infinite;animation:tada 1.5s ease infinite}.bx-flashing{-webkit-animation:flashing 1.5s infinite linear;animation:flashing 1.5s infinite linear}.bx-flashing-hover:hover{-webkit-animation:flashing 1.5s infinite linear;animation:flashing 1.5s infinite linear}.bx-burst{-webkit-animation:burst 1.5s infinite linear;animation:burst 1.5s infinite linear}.bx-burst-hover:hover{-webkit-animation:burst 1.5s infinite linear;animation:burst 1.5s infinite linear}.bx-fade-up{-webkit-animation:fade-up 1.5s infinite linear;animation:fade-up 1.5s infinite linear}.bx-fade-up-hover:hover{-webkit-animation:fade-up 1.5s infinite linear;animation:fade-up 1.5s infinite linear}.bx-fade-down{-webkit-animation:fade-down 1.5s infinite linear;animation:fade-down 1.5s infinite linear}.bx-fade-down-hover:hover{-webkit-animation:fade-down 1.5s infinite linear;animation:fade-down 1.5s infinite linear}.bx-fade-left{-webkit-animation:fade-left 1.5s infinite linear;animation:fade-left 1.5s infinite linear}.bx-fade-left-hover:hover{-webkit-animation:fade-left 1.5s infinite linear;animation:fade-left 1.5s infinite linear}.bx-fade-right{-webkit-animation:fade-right 1.5s infinite linear;animation:fade-right 1.5s infinite linear}.bx-fade-right-hover:hover{-webkit-animation:fade-right 1.5s infinite linear;animation:fade-right 1.5s infinite linear}.bx-xs{font-size:1rem!important}.bx-sm{font-size:1.55rem!important}.bx-md{font-size:2.25rem!important}.bx-lg{font-size:3rem!important}.bx-fw{font-size:1.2857142857em;line-height:.8em;width:1.2857142857em;height:.8em;margin-top:-.2em!important;vertical-align:middle}.bx-pull-left{float:left;margin-right:.3em!important}.bx-pull-right{float:right;margin-left:.3em!important}.bx-rotate-90{transform:rotate(90deg)}.bx-rotate-180{transform:rotate(180deg)}.bx-rotate-270{transform:rotate(270deg)}.bx-flip-horizontal{transform:scaleX(-1)}.bx-flip-vertical{transform:scaleY(-1)}.bx-border{padding:.25em;border:.07em solid rgba(0,0,0,.1);border-radius:.25em}.bx-border-circle{padding:.25em;border:.07em solid rgba(0,0,0,.1);border-radius:50%}.bxs-color:before{content:"\ef39"}.bx-reflect-horizontal:before{content:"\ef3a"}.bx-reflect-vertical:before{content:"\ef3b"}.bx-color:before{content:"\ef3c"}.bxl-mongodb:before{content:"\ef3d"}.bxl-postgresql:before{content:"\ef3e"}.bxl-deezer:before{content:"\ef3f"}.bxs-hard-hat:before{content:"\ef2a"}.bxs-home-alt-2:before{content:"\ef2b"}.bxs-cheese:before{content:"\ef2c"}.bx-home-alt-2:before{content:"\ef2d"}.bx-hard-hat:before{content:"\ef2e"}.bx-cheese:before{content:"\ef2f"}.bx-cart-add:before{content:"\ef30"}.bx-cart-download:before{content:"\ef31"}.bx-no-signal:before{content:"\ef32"}.bx-signal-1:before{content:"\ef33"}.bx-signal-2:before{content:"\ef34"}.bx-signal-3:before{content:"\ef35"}.bx-signal-4:before{content:"\ef36"}.bx-signal-5:before{content:"\ef37"}.bxl-xing:before{content:"\ef38"}.bxl-meta:before{content:"\ef27"}.bx-lemon:before{content:"\ef28"}.bxs-lemon:before{content:"\ef29"}.bx-cricket-ball:before{content:"\ef0c"}.bx-baguette:before{content:"\ef0d"}.bx-bowl-hot:before{content:"\ef0e"}.bx-bowl-rice:before{content:"\ef0f"}.bx-cable-car:before{content:"\ef10"}.bx-candles:before{content:"\ef11"}.bx-circle-half:before{content:"\ef12"}.bx-circle-quarter:before{content:"\ef13"}.bx-circle-three-quarter:before{content:"\ef14"}.bx-cross:before{content:"\ef15"}.bx-fork:before{content:"\ef16"}.bx-knife:before{content:"\ef17"}.bx-money-withdraw:before{content:"\ef18"}.bx-popsicle:before{content:"\ef19"}.bx-scatter-chart:before{content:"\ef1a"}.bxs-baguette:before{content:"\ef1b"}.bxs-bowl-hot:before{content:"\ef1c"}.bxs-bowl-rice:before{content:"\ef1d"}.bxs-cable-car:before{content:"\ef1e"}.bxs-circle-half:before{content:"\ef1f"}.bxs-circle-quarter:before{content:"\ef20"}.bxs-circle-three-quarter:before{content:"\ef21"}.bxs-cricket-ball:before{content:"\ef22"}.bxs-invader:before{content:"\ef23"}.bx-male-female:before{content:"\ef24"}.bxs-popsicle:before{content:"\ef25"}.bxs-tree-alt:before{content:"\ef26"}.bxl-venmo:before{content:"\e900"}.bxl-upwork:before{content:"\e901"}.bxl-netlify:before{content:"\e902"}.bxl-java:before{content:"\e903"}.bxl-heroku:before{content:"\e904"}.bxl-go-lang:before{content:"\e905"}.bxl-gmail:before{content:"\e906"}.bxl-flask:before{content:"\e907"}.bxl-99designs:before{content:"\e908"}.bxl-500px:before{content:"\e909"}.bxl-adobe:before{content:"\e90a"}.bxl-airbnb:before{content:"\e90b"}.bxl-algolia:before{content:"\e90c"}.bxl-amazon:before{content:"\e90d"}.bxl-android:before{content:"\e90e"}.bxl-angular:before{content:"\e90f"}.bxl-apple:before{content:"\e910"}.bxl-audible:before{content:"\e911"}.bxl-aws:before{content:"\e912"}.bxl-baidu:before{content:"\e913"}.bxl-behance:before{content:"\e914"}.bxl-bing:before{content:"\e915"}.bxl-bitcoin:before{content:"\e916"}.bxl-blender:before{content:"\e917"}.bxl-blogger:before{content:"\e918"}.bxl-bootstrap:before{content:"\e919"}.bxl-chrome:before{content:"\e91a"}.bxl-codepen:before{content:"\e91b"}.bxl-c-plus-plus:before{content:"\e91c"}.bxl-creative-commons:before{content:"\e91d"}.bxl-css3:before{content:"\e91e"}.bxl-dailymotion:before{content:"\e91f"}.bxl-deviantart:before{content:"\e920"}.bxl-dev-to:before{content:"\e921"}.bxl-digg:before{content:"\e922"}.bxl-digitalocean:before{content:"\e923"}.bxl-discord:before{content:"\e924"}.bxl-discord-alt:before{content:"\e925"}.bxl-discourse:before{content:"\e926"}.bxl-django:before{content:"\e927"}.bxl-docker:before{content:"\e928"}.bxl-dribbble:before{content:"\e929"}.bxl-dropbox:before{content:"\e92a"}.bxl-drupal:before{content:"\e92b"}.bxl-ebay:before{content:"\e92c"}.bxl-edge:before{content:"\e92d"}.bxl-etsy:before{content:"\e92e"}.bxl-facebook:before{content:"\e92f"}.bxl-facebook-circle:before{content:"\e930"}.bxl-facebook-square:before{content:"\e931"}.bxl-figma:before{content:"\e932"}.bxl-firebase:before{content:"\e933"}.bxl-firefox:before{content:"\e934"}.bxl-flickr:before{content:"\e935"}.bxl-flickr-square:before{content:"\e936"}.bxl-flutter:before{content:"\e937"}.bxl-foursquare:before{content:"\e938"}.bxl-git:before{content:"\e939"}.bxl-github:before{content:"\e93a"}.bxl-gitlab:before{content:"\e93b"}.bxl-google:before{content:"\e93c"}.bxl-google-cloud:before{content:"\e93d"}.bxl-google-plus:before{content:"\e93e"}.bxl-google-plus-circle:before{content:"\e93f"}.bxl-html5:before{content:"\e940"}.bxl-imdb:before{content:"\e941"}.bxl-instagram:before{content:"\e942"}.bxl-instagram-alt:before{content:"\e943"}.bxl-internet-explorer:before{content:"\e944"}.bxl-invision:before{content:"\e945"}.bxl-javascript:before{content:"\e946"}.bxl-joomla:before{content:"\e947"}.bxl-jquery:before{content:"\e948"}.bxl-jsfiddle:before{content:"\e949"}.bxl-kickstarter:before{content:"\e94a"}.bxl-kubernetes:before{content:"\e94b"}.bxl-less:before{content:"\e94c"}.bxl-linkedin:before{content:"\e94d"}.bxl-linkedin-square:before{content:"\e94e"}.bxl-magento:before{content:"\e94f"}.bxl-mailchimp:before{content:"\e950"}.bxl-markdown:before{content:"\e951"}.bxl-mastercard:before{content:"\e952"}.bxl-mastodon:before{content:"\e953"}.bxl-medium:before{content:"\e954"}.bxl-medium-old:before{content:"\e955"}.bxl-medium-square:before{content:"\e956"}.bxl-messenger:before{content:"\e957"}.bxl-microsoft:before{content:"\e958"}.bxl-microsoft-teams:before{content:"\e959"}.bxl-nodejs:before{content:"\e95a"}.bxl-ok-ru:before{content:"\e95b"}.bxl-opera:before{content:"\e95c"}.bxl-patreon:before{content:"\e95d"}.bxl-paypal:before{content:"\e95e"}.bxl-periscope:before{content:"\e95f"}.bxl-php:before{content:"\e960"}.bxl-pinterest:before{content:"\e961"}.bxl-pinterest-alt:before{content:"\e962"}.bxl-play-store:before{content:"\e963"}.bxl-pocket:before{content:"\e964"}.bxl-product-hunt:before{content:"\e965"}.bxl-python:before{content:"\e966"}.bxl-quora:before{content:"\e967"}.bxl-react:before{content:"\e968"}.bxl-redbubble:before{content:"\e969"}.bxl-reddit:before{content:"\e96a"}.bxl-redux:before{content:"\e96b"}.bxl-sass:before{content:"\e96c"}.bxl-shopify:before{content:"\e96d"}.bxl-sketch:before{content:"\e96e"}.bxl-skype:before{content:"\e96f"}.bxl-slack:before{content:"\e970"}.bxl-slack-old:before{content:"\e971"}.bxl-snapchat:before{content:"\e972"}.bxl-soundcloud:before{content:"\e973"}.bxl-spotify:before{content:"\e974"}.bxl-spring-boot:before{content:"\e975"}.bxl-squarespace:before{content:"\e976"}.bxl-stack-overflow:before{content:"\e977"}.bxl-steam:before{content:"\e978"}.bxl-stripe:before{content:"\e979"}.bxl-tailwind-css:before{content:"\e97a"}.bxl-telegram:before{content:"\e97b"}.bxl-tiktok:before{content:"\e97c"}.bxl-trello:before{content:"\e97d"}.bxl-trip-advisor:before{content:"\e97e"}.bxl-tumblr:before{content:"\e97f"}.bxl-tux:before{content:"\e980"}.bxl-twitch:before{content:"\e981"}.bxl-twitter:before{content:"\e982"}.bxl-unity:before{content:"\e983"}.bxl-unsplash:before{content:"\e984"}.bxl-vimeo:before{content:"\e985"}.bxl-visa:before{content:"\e986"}.bxl-visual-studio:before{content:"\e987"}.bxl-vk:before{content:"\e988"}.bxl-vuejs:before{content:"\e989"}.bxl-whatsapp:before{content:"\e98a"}.bxl-whatsapp-square:before{content:"\e98b"}.bxl-wikipedia:before{content:"\e98c"}.bxl-windows:before{content:"\e98d"}.bxl-wix:before{content:"\e98e"}.bxl-wordpress:before{content:"\e98f"}.bxl-yahoo:before{content:"\e990"}.bxl-yelp:before{content:"\e991"}.bxl-youtube:before{content:"\e992"}.bxl-zoom:before{content:"\e993"}.bx-collapse-alt:before{content:"\e994"}.bx-collapse-horizontal:before{content:"\e995"}.bx-collapse-vertical:before{content:"\e996"}.bx-expand-horizontal:before{content:"\e997"}.bx-expand-vertical:before{content:"\e998"}.bx-injection:before{content:"\e999"}.bx-leaf:before{content:"\e99a"}.bx-math:before{content:"\e99b"}.bx-party:before{content:"\e99c"}.bx-abacus:before{content:"\e99d"}.bx-accessibility:before{content:"\e99e"}.bx-add-to-queue:before{content:"\e99f"}.bx-adjust:before{content:"\e9a0"}.bx-alarm:before{content:"\e9a1"}.bx-alarm-add:before{content:"\e9a2"}.bx-alarm-exclamation:before{content:"\e9a3"}.bx-alarm-off:before{content:"\e9a4"}.bx-alarm-snooze:before{content:"\e9a5"}.bx-album:before{content:"\e9a6"}.bx-align-justify:before{content:"\e9a7"}.bx-align-left:before{content:"\e9a8"}.bx-align-middle:before{content:"\e9a9"}.bx-align-right:before{content:"\e9aa"}.bx-analyse:before{content:"\e9ab"}.bx-anchor:before{content:"\e9ac"}.bx-angry:before{content:"\e9ad"}.bx-aperture:before{content:"\e9ae"}.bx-arch:before{content:"\e9af"}.bx-archive:before{content:"\e9b0"}.bx-archive-in:before{content:"\e9b1"}.bx-archive-out:before{content:"\e9b2"}.bx-area:before{content:"\e9b3"}.bx-arrow-back:before{content:"\e9b4"}.bx-arrow-from-bottom:before{content:"\e9b5"}.bx-arrow-from-left:before{content:"\e9b6"}.bx-arrow-from-right:before{content:"\e9b7"}.bx-arrow-from-top:before{content:"\e9b8"}.bx-arrow-to-bottom:before{content:"\e9b9"}.bx-arrow-to-left:before{content:"\e9ba"}.bx-arrow-to-right:before{content:"\e9bb"}.bx-arrow-to-top:before{content:"\e9bc"}.bx-at:before{content:"\e9bd"}.bx-atom:before{content:"\e9be"}.bx-award:before{content:"\e9bf"}.bx-badge:before{content:"\e9c0"}.bx-badge-check:before{content:"\e9c1"}.bx-ball:before{content:"\e9c2"}.bx-band-aid:before{content:"\e9c3"}.bx-bar-chart:before{content:"\e9c4"}.bx-bar-chart-alt:before{content:"\e9c5"}.bx-bar-chart-alt-2:before{content:"\e9c6"}.bx-bar-chart-square:before{content:"\e9c7"}.bx-barcode:before{content:"\e9c8"}.bx-barcode-reader:before{content:"\e9c9"}.bx-baseball:before{content:"\e9ca"}.bx-basket:before{content:"\e9cb"}.bx-basketball:before{content:"\e9cc"}.bx-bath:before{content:"\e9cd"}.bx-battery:before{content:"\e9ce"}.bx-bed:before{content:"\e9cf"}.bx-been-here:before{content:"\e9d0"}.bx-beer:before{content:"\e9d1"}.bx-bell:before{content:"\e9d2"}.bx-bell-minus:before{content:"\e9d3"}.bx-bell-off:before{content:"\e9d4"}.bx-bell-plus:before{content:"\e9d5"}.bx-bible:before{content:"\e9d6"}.bx-bitcoin:before{content:"\e9d7"}.bx-blanket:before{content:"\e9d8"}.bx-block:before{content:"\e9d9"}.bx-bluetooth:before{content:"\e9da"}.bx-body:before{content:"\e9db"}.bx-bold:before{content:"\e9dc"}.bx-bolt-circle:before{content:"\e9dd"}.bx-bomb:before{content:"\e9de"}.bx-bone:before{content:"\e9df"}.bx-bong:before{content:"\e9e0"}.bx-book:before{content:"\e9e1"}.bx-book-add:before{content:"\e9e2"}.bx-book-alt:before{content:"\e9e3"}.bx-book-bookmark:before{content:"\e9e4"}.bx-book-content:before{content:"\e9e5"}.bx-book-heart:before{content:"\e9e6"}.bx-bookmark:before{content:"\e9e7"}.bx-bookmark-alt:before{content:"\e9e8"}.bx-bookmark-alt-minus:before{content:"\e9e9"}.bx-bookmark-alt-plus:before{content:"\e9ea"}.bx-bookmark-heart:before{content:"\e9eb"}.bx-bookmark-minus:before{content:"\e9ec"}.bx-bookmark-plus:before{content:"\e9ed"}.bx-bookmarks:before{content:"\e9ee"}.bx-book-open:before{content:"\e9ef"}.bx-book-reader:before{content:"\e9f0"}.bx-border-all:before{content:"\e9f1"}.bx-border-bottom:before{content:"\e9f2"}.bx-border-inner:before{content:"\e9f3"}.bx-border-left:before{content:"\e9f4"}.bx-border-none:before{content:"\e9f5"}.bx-border-outer:before{content:"\e9f6"}.bx-border-radius:before{content:"\e9f7"}.bx-border-right:before{content:"\e9f8"}.bx-border-top:before{content:"\e9f9"}.bx-bot:before{content:"\e9fa"}.bx-bowling-ball:before{content:"\e9fb"}.bx-box:before{content:"\e9fc"}.bx-bracket:before{content:"\e9fd"}.bx-braille:before{content:"\e9fe"}.bx-brain:before{content:"\e9ff"}.bx-briefcase:before{content:"\ea00"}.bx-briefcase-alt:before{content:"\ea01"}.bx-briefcase-alt-2:before{content:"\ea02"}.bx-brightness:before{content:"\ea03"}.bx-brightness-half:before{content:"\ea04"}.bx-broadcast:before{content:"\ea05"}.bx-brush:before{content:"\ea06"}.bx-brush-alt:before{content:"\ea07"}.bx-bug:before{content:"\ea08"}.bx-bug-alt:before{content:"\ea09"}.bx-building:before{content:"\ea0a"}.bx-building-house:before{content:"\ea0b"}.bx-buildings:before{content:"\ea0c"}.bx-bulb:before{content:"\ea0d"}.bx-bullseye:before{content:"\ea0e"}.bx-buoy:before{content:"\ea0f"}.bx-bus:before{content:"\ea10"}.bx-bus-school:before{content:"\ea11"}.bx-cabinet:before{content:"\ea12"}.bx-cake:before{content:"\ea13"}.bx-calculator:before{content:"\ea14"}.bx-calendar:before{content:"\ea15"}.bx-calendar-alt:before{content:"\ea16"}.bx-calendar-check:before{content:"\ea17"}.bx-calendar-edit:before{content:"\ea18"}.bx-calendar-event:before{content:"\ea19"}.bx-calendar-exclamation:before{content:"\ea1a"}.bx-calendar-heart:before{content:"\ea1b"}.bx-calendar-minus:before{content:"\ea1c"}.bx-calendar-plus:before{content:"\ea1d"}.bx-calendar-star:before{content:"\ea1e"}.bx-calendar-week:before{content:"\ea1f"}.bx-calendar-x:before{content:"\ea20"}.bx-camera:before{content:"\ea21"}.bx-camera-home:before{content:"\ea22"}.bx-camera-movie:before{content:"\ea23"}.bx-camera-off:before{content:"\ea24"}.bx-capsule:before{content:"\ea25"}.bx-captions:before{content:"\ea26"}.bx-car:before{content:"\ea27"}.bx-card:before{content:"\ea28"}.bx-caret-down:before{content:"\ea29"}.bx-caret-down-circle:before{content:"\ea2a"}.bx-caret-down-square:before{content:"\ea2b"}.bx-caret-left:before{content:"\ea2c"}.bx-caret-left-circle:before{content:"\ea2d"}.bx-caret-left-square:before{content:"\ea2e"}.bx-caret-right:before{content:"\ea2f"}.bx-caret-right-circle:before{content:"\ea30"}.bx-caret-right-square:before{content:"\ea31"}.bx-caret-up:before{content:"\ea32"}.bx-caret-up-circle:before{content:"\ea33"}.bx-caret-up-square:before{content:"\ea34"}.bx-carousel:before{content:"\ea35"}.bx-cart:before{content:"\ea36"}.bx-cart-alt:before{content:"\ea37"}.bx-cast:before{content:"\ea38"}.bx-category:before{content:"\ea39"}.bx-category-alt:before{content:"\ea3a"}.bx-cctv:before{content:"\ea3b"}.bx-certification:before{content:"\ea3c"}.bx-chair:before{content:"\ea3d"}.bx-chalkboard:before{content:"\ea3e"}.bx-chart:before{content:"\ea3f"}.bx-chat:before{content:"\ea40"}.bx-check:before{content:"\ea41"}.bx-checkbox:before{content:"\ea42"}.bx-checkbox-checked:before{content:"\ea43"}.bx-checkbox-minus:before{content:"\ea44"}.bx-checkbox-square:before{content:"\ea45"}.bx-check-circle:before{content:"\ea46"}.bx-check-double:before{content:"\ea47"}.bx-check-shield:before{content:"\ea48"}.bx-check-square:before{content:"\ea49"}.bx-chevron-down:before{content:"\ea4a"}.bx-chevron-down-circle:before{content:"\ea4b"}.bx-chevron-down-square:before{content:"\ea4c"}.bx-chevron-left:before{content:"\ea4d"}.bx-chevron-left-circle:before{content:"\ea4e"}.bx-chevron-left-square:before{content:"\ea4f"}.bx-chevron-right:before{content:"\ea50"}.bx-chevron-right-circle:before{content:"\ea51"}.bx-chevron-right-square:before{content:"\ea52"}.bx-chevrons-down:before{content:"\ea53"}.bx-chevrons-left:before{content:"\ea54"}.bx-chevrons-right:before{content:"\ea55"}.bx-chevrons-up:before{content:"\ea56"}.bx-chevron-up:before{content:"\ea57"}.bx-chevron-up-circle:before{content:"\ea58"}.bx-chevron-up-square:before{content:"\ea59"}.bx-chip:before{content:"\ea5a"}.bx-church:before{content:"\ea5b"}.bx-circle:before{content:"\ea5c"}.bx-clinic:before{content:"\ea5d"}.bx-clipboard:before{content:"\ea5e"}.bx-closet:before{content:"\ea5f"}.bx-cloud:before{content:"\ea60"}.bx-cloud-download:before{content:"\ea61"}.bx-cloud-drizzle:before{content:"\ea62"}.bx-cloud-lightning:before{content:"\ea63"}.bx-cloud-light-rain:before{content:"\ea64"}.bx-cloud-rain:before{content:"\ea65"}.bx-cloud-snow:before{content:"\ea66"}.bx-cloud-upload:before{content:"\ea67"}.bx-code:before{content:"\ea68"}.bx-code-alt:before{content:"\ea69"}.bx-code-block:before{content:"\ea6a"}.bx-code-curly:before{content:"\ea6b"}.bx-coffee:before{content:"\ea6c"}.bx-coffee-togo:before{content:"\ea6d"}.bx-cog:before{content:"\ea6e"}.bx-coin:before{content:"\ea6f"}.bx-coin-stack:before{content:"\ea70"}.bx-collapse:before{content:"\ea71"}.bx-collection:before{content:"\ea72"}.bx-color-fill:before{content:"\ea73"}.bx-columns:before{content:"\ea74"}.bx-command:before{content:"\ea75"}.bx-comment:before{content:"\ea76"}.bx-comment-add:before{content:"\ea77"}.bx-comment-check:before{content:"\ea78"}.bx-comment-detail:before{content:"\ea79"}.bx-comment-dots:before{content:"\ea7a"}.bx-comment-edit:before{content:"\ea7b"}.bx-comment-error:before{content:"\ea7c"}.bx-comment-minus:before{content:"\ea7d"}.bx-comment-x:before{content:"\ea7e"}.bx-compass:before{content:"\ea7f"}.bx-confused:before{content:"\ea80"}.bx-conversation:before{content:"\ea81"}.bx-cookie:before{content:"\ea82"}.bx-cool:before{content:"\ea83"}.bx-copy:before{content:"\ea84"}.bx-copy-alt:before{content:"\ea85"}.bx-copyright:before{content:"\ea86"}.bx-credit-card:before{content:"\ea87"}.bx-credit-card-alt:before{content:"\ea88"}.bx-credit-card-front:before{content:"\ea89"}.bx-crop:before{content:"\ea8a"}.bx-crosshair:before{content:"\ea8b"}.bx-crown:before{content:"\ea8c"}.bx-cube:before{content:"\ea8d"}.bx-cube-alt:before{content:"\ea8e"}.bx-cuboid:before{content:"\ea8f"}.bx-current-location:before{content:"\ea90"}.bx-customize:before{content:"\ea91"}.bx-cut:before{content:"\ea92"}.bx-cycling:before{content:"\ea93"}.bx-cylinder:before{content:"\ea94"}.bx-data:before{content:"\ea95"}.bx-desktop:before{content:"\ea96"}.bx-detail:before{content:"\ea97"}.bx-devices:before{content:"\ea98"}.bx-dialpad:before{content:"\ea99"}.bx-dialpad-alt:before{content:"\ea9a"}.bx-diamond:before{content:"\ea9b"}.bx-dice-1:before{content:"\ea9c"}.bx-dice-2:before{content:"\ea9d"}.bx-dice-3:before{content:"\ea9e"}.bx-dice-4:before{content:"\ea9f"}.bx-dice-5:before{content:"\eaa0"}.bx-dice-6:before{content:"\eaa1"}.bx-directions:before{content:"\eaa2"}.bx-disc:before{content:"\eaa3"}.bx-dish:before{content:"\eaa4"}.bx-dislike:before{content:"\eaa5"}.bx-dizzy:before{content:"\eaa6"}.bx-dna:before{content:"\eaa7"}.bx-dock-bottom:before{content:"\eaa8"}.bx-dock-left:before{content:"\eaa9"}.bx-dock-right:before{content:"\eaaa"}.bx-dock-top:before{content:"\eaab"}.bx-dollar:before{content:"\eaac"}.bx-dollar-circle:before{content:"\eaad"}.bx-donate-blood:before{content:"\eaae"}.bx-donate-heart:before{content:"\eaaf"}.bx-door-open:before{content:"\eab0"}.bx-dots-horizontal:before{content:"\eab1"}.bx-dots-horizontal-rounded:before{content:"\eab2"}.bx-dots-vertical:before{content:"\eab3"}.bx-dots-vertical-rounded:before{content:"\eab4"}.bx-doughnut-chart:before{content:"\eab5"}.bx-down-arrow:before{content:"\eab6"}.bx-down-arrow-alt:before{content:"\eab7"}.bx-down-arrow-circle:before{content:"\eab8"}.bx-download:before{content:"\eab9"}.bx-downvote:before{content:"\eaba"}.bx-drink:before{content:"\eabb"}.bx-droplet:before{content:"\eabc"}.bx-dumbbell:before{content:"\eabd"}.bx-duplicate:before{content:"\eabe"}.bx-edit:before{content:"\eabf"}.bx-edit-alt:before{content:"\eac0"}.bx-envelope:before{content:"\eac1"}.bx-envelope-open:before{content:"\eac2"}.bx-equalizer:before{content:"\eac3"}.bx-eraser:before{content:"\eac4"}.bx-error:before{content:"\eac5"}.bx-error-alt:before{content:"\eac6"}.bx-error-circle:before{content:"\eac7"}.bx-euro:before{content:"\eac8"}.bx-exclude:before{content:"\eac9"}.bx-exit:before{content:"\eaca"}.bx-exit-fullscreen:before{content:"\eacb"}.bx-expand:before{content:"\eacc"}.bx-expand-alt:before{content:"\eacd"}.bx-export:before{content:"\eace"}.bx-extension:before{content:"\eacf"}.bx-face:before{content:"\ead0"}.bx-fast-forward:before{content:"\ead1"}.bx-fast-forward-circle:before{content:"\ead2"}.bx-female:before{content:"\ead3"}.bx-female-sign:before{content:"\ead4"}.bx-file:before{content:"\ead5"}.bx-file-blank:before{content:"\ead6"}.bx-file-find:before{content:"\ead7"}.bx-film:before{content:"\ead8"}.bx-filter:before{content:"\ead9"}.bx-filter-alt:before{content:"\eada"}.bx-fingerprint:before{content:"\eadb"}.bx-first-aid:before{content:"\eadc"}.bx-first-page:before{content:"\eadd"}.bx-flag:before{content:"\eade"}.bx-folder:before{content:"\eadf"}.bx-folder-minus:before{content:"\eae0"}.bx-folder-open:before{content:"\eae1"}.bx-folder-plus:before{content:"\eae2"}.bx-font:before{content:"\eae3"}.bx-font-color:before{content:"\eae4"}.bx-font-family:before{content:"\eae5"}.bx-font-size:before{content:"\eae6"}.bx-food-menu:before{content:"\eae7"}.bx-food-tag:before{content:"\eae8"}.bx-football:before{content:"\eae9"}.bx-fridge:before{content:"\eaea"}.bx-fullscreen:before{content:"\eaeb"}.bx-game:before{content:"\eaec"}.bx-gas-pump:before{content:"\eaed"}.bx-ghost:before{content:"\eaee"}.bx-gift:before{content:"\eaef"}.bx-git-branch:before{content:"\eaf0"}.bx-git-commit:before{content:"\eaf1"}.bx-git-compare:before{content:"\eaf2"}.bx-git-merge:before{content:"\eaf3"}.bx-git-pull-request:before{content:"\eaf4"}.bx-git-repo-forked:before{content:"\eaf5"}.bx-glasses:before{content:"\eaf6"}.bx-glasses-alt:before{content:"\eaf7"}.bx-globe:before{content:"\eaf8"}.bx-globe-alt:before{content:"\eaf9"}.bx-grid:before{content:"\eafa"}.bx-grid-alt:before{content:"\eafb"}.bx-grid-horizontal:before{content:"\eafc"}.bx-grid-small:before{content:"\eafd"}.bx-grid-vertical:before{content:"\eafe"}.bx-group:before{content:"\eaff"}.bx-handicap:before{content:"\eb00"}.bx-happy:before{content:"\eb01"}.bx-happy-alt:before{content:"\eb02"}.bx-happy-beaming:before{content:"\eb03"}.bx-happy-heart-eyes:before{content:"\eb04"}.bx-hash:before{content:"\eb05"}.bx-hdd:before{content:"\eb06"}.bx-heading:before{content:"\eb07"}.bx-headphone:before{content:"\eb08"}.bx-health:before{content:"\eb09"}.bx-heart:before{content:"\eb0a"}.bx-heart-circle:before{content:"\eb0b"}.bx-heart-square:before{content:"\eb0c"}.bx-help-circle:before{content:"\eb0d"}.bx-hide:before{content:"\eb0e"}.bx-highlight:before{content:"\eb0f"}.bx-history:before{content:"\eb10"}.bx-hive:before{content:"\eb11"}.bx-home:before{content:"\eb12"}.bx-home-alt:before{content:"\eb13"}.bx-home-circle:before{content:"\eb14"}.bx-home-heart:before{content:"\eb15"}.bx-home-smile:before{content:"\eb16"}.bx-horizontal-center:before{content:"\eb17"}.bx-hotel:before{content:"\eb18"}.bx-hourglass:before{content:"\eb19"}.bx-id-card:before{content:"\eb1a"}.bx-image:before{content:"\eb1b"}.bx-image-add:before{content:"\eb1c"}.bx-image-alt:before{content:"\eb1d"}.bx-images:before{content:"\eb1e"}.bx-import:before{content:"\eb1f"}.bx-infinite:before{content:"\eb20"}.bx-info-circle:before{content:"\eb21"}.bx-info-square:before{content:"\eb22"}.bx-intersect:before{content:"\eb23"}.bx-italic:before{content:"\eb24"}.bx-joystick:before{content:"\eb25"}.bx-joystick-alt:before{content:"\eb26"}.bx-joystick-button:before{content:"\eb27"}.bx-key:before{content:"\eb28"}.bx-label:before{content:"\eb29"}.bx-landscape:before{content:"\eb2a"}.bx-laptop:before{content:"\eb2b"}.bx-last-page:before{content:"\eb2c"}.bx-laugh:before{content:"\eb2d"}.bx-layer:before{content:"\eb2e"}.bx-layer-minus:before{content:"\eb2f"}.bx-layer-plus:before{content:"\eb30"}.bx-layout:before{content:"\eb31"}.bx-left-arrow:before{content:"\eb32"}.bx-left-arrow-alt:before{content:"\eb33"}.bx-left-arrow-circle:before{content:"\eb34"}.bx-left-down-arrow-circle:before{content:"\eb35"}.bx-left-indent:before{content:"\eb36"}.bx-left-top-arrow-circle:before{content:"\eb37"}.bx-library:before{content:"\eb38"}.bx-like:before{content:"\eb39"}.bx-line-chart:before{content:"\eb3a"}.bx-line-chart-down:before{content:"\eb3b"}.bx-link:before{content:"\eb3c"}.bx-link-alt:before{content:"\eb3d"}.bx-link-external:before{content:"\eb3e"}.bx-lira:before{content:"\eb3f"}.bx-list-check:before{content:"\eb40"}.bx-list-minus:before{content:"\eb41"}.bx-list-ol:before{content:"\eb42"}.bx-list-plus:before{content:"\eb43"}.bx-list-ul:before{content:"\eb44"}.bx-loader:before{content:"\eb45"}.bx-loader-alt:before{content:"\eb46"}.bx-loader-circle:before{content:"\eb47"}.bx-location-plus:before{content:"\eb48"}.bx-lock:before{content:"\eb49"}.bx-lock-alt:before{content:"\eb4a"}.bx-lock-open:before{content:"\eb4b"}.bx-lock-open-alt:before{content:"\eb4c"}.bx-log-in:before{content:"\eb4d"}.bx-log-in-circle:before{content:"\eb4e"}.bx-log-out:before{content:"\eb4f"}.bx-log-out-circle:before{content:"\eb50"}.bx-low-vision:before{content:"\eb51"}.bx-magnet:before{content:"\eb52"}.bx-mail-send:before{content:"\eb53"}.bx-male:before{content:"\eb54"}.bx-male-sign:before{content:"\eb55"}.bx-map:before{content:"\eb56"}.bx-map-alt:before{content:"\eb57"}.bx-map-pin:before{content:"\eb58"}.bx-mask:before{content:"\eb59"}.bx-medal:before{content:"\eb5a"}.bx-meh:before{content:"\eb5b"}.bx-meh-alt:before{content:"\eb5c"}.bx-meh-blank:before{content:"\eb5d"}.bx-memory-card:before{content:"\eb5e"}.bx-menu:before{content:"\eb5f"}.bx-menu-alt-left:before{content:"\eb60"}.bx-menu-alt-right:before{content:"\eb61"}.bx-merge:before{content:"\eb62"}.bx-message:before{content:"\eb63"}.bx-message-add:before{content:"\eb64"}.bx-message-alt:before{content:"\eb65"}.bx-message-alt-add:before{content:"\eb66"}.bx-message-alt-check:before{content:"\eb67"}.bx-message-alt-detail:before{content:"\eb68"}.bx-message-alt-dots:before{content:"\eb69"}.bx-message-alt-edit:before{content:"\eb6a"}.bx-message-alt-error:before{content:"\eb6b"}.bx-message-alt-minus:before{content:"\eb6c"}.bx-message-alt-x:before{content:"\eb6d"}.bx-message-check:before{content:"\eb6e"}.bx-message-detail:before{content:"\eb6f"}.bx-message-dots:before{content:"\eb70"}.bx-message-edit:before{content:"\eb71"}.bx-message-error:before{content:"\eb72"}.bx-message-minus:before{content:"\eb73"}.bx-message-rounded:before{content:"\eb74"}.bx-message-rounded-add:before{content:"\eb75"}.bx-message-rounded-check:before{content:"\eb76"}.bx-message-rounded-detail:before{content:"\eb77"}.bx-message-rounded-dots:before{content:"\eb78"}.bx-message-rounded-edit:before{content:"\eb79"}.bx-message-rounded-error:before{content:"\eb7a"}.bx-message-rounded-minus:before{content:"\eb7b"}.bx-message-rounded-x:before{content:"\eb7c"}.bx-message-square:before{content:"\eb7d"}.bx-message-square-add:before{content:"\eb7e"}.bx-message-square-check:before{content:"\eb7f"}.bx-message-square-detail:before{content:"\eb80"}.bx-message-square-dots:before{content:"\eb81"}.bx-message-square-edit:before{content:"\eb82"}.bx-message-square-error:before{content:"\eb83"}.bx-message-square-minus:before{content:"\eb84"}.bx-message-square-x:before{content:"\eb85"}.bx-message-x:before{content:"\eb86"}.bx-meteor:before{content:"\eb87"}.bx-microchip:before{content:"\eb88"}.bx-microphone:before{content:"\eb89"}.bx-microphone-off:before{content:"\eb8a"}.bx-minus:before{content:"\eb8b"}.bx-minus-back:before{content:"\eb8c"}.bx-minus-circle:before{content:"\eb8d"}.bx-minus-front:before{content:"\eb8e"}.bx-mobile:before{content:"\eb8f"}.bx-mobile-alt:before{content:"\eb90"}.bx-mobile-landscape:before{content:"\eb91"}.bx-mobile-vibration:before{content:"\eb92"}.bx-money:before{content:"\eb93"}.bx-moon:before{content:"\eb94"}.bx-mouse:before{content:"\eb95"}.bx-mouse-alt:before{content:"\eb96"}.bx-move:before{content:"\eb97"}.bx-move-horizontal:before{content:"\eb98"}.bx-move-vertical:before{content:"\eb99"}.bx-movie:before{content:"\eb9a"}.bx-movie-play:before{content:"\eb9b"}.bx-music:before{content:"\eb9c"}.bx-navigation:before{content:"\eb9d"}.bx-network-chart:before{content:"\eb9e"}.bx-news:before{content:"\eb9f"}.bx-no-entry:before{content:"\eba0"}.bx-note:before{content:"\eba1"}.bx-notepad:before{content:"\eba2"}.bx-notification:before{content:"\eba3"}.bx-notification-off:before{content:"\eba4"}.bx-outline:before{content:"\eba5"}.bx-package:before{content:"\eba6"}.bx-paint:before{content:"\eba7"}.bx-paint-roll:before{content:"\eba8"}.bx-palette:before{content:"\eba9"}.bx-paperclip:before{content:"\ebaa"}.bx-paper-plane:before{content:"\ebab"}.bx-paragraph:before{content:"\ebac"}.bx-paste:before{content:"\ebad"}.bx-pause:before{content:"\ebae"}.bx-pause-circle:before{content:"\ebaf"}.bx-pen:before{content:"\ebb0"}.bx-pencil:before{content:"\ebb1"}.bx-phone:before{content:"\ebb2"}.bx-phone-call:before{content:"\ebb3"}.bx-phone-incoming:before{content:"\ebb4"}.bx-phone-off:before{content:"\ebb5"}.bx-phone-outgoing:before{content:"\ebb6"}.bx-photo-album:before{content:"\ebb7"}.bx-pie-chart:before{content:"\ebb8"}.bx-pie-chart-alt:before{content:"\ebb9"}.bx-pie-chart-alt-2:before{content:"\ebba"}.bx-pin:before{content:"\ebbb"}.bx-planet:before{content:"\ebbc"}.bx-play:before{content:"\ebbd"}.bx-play-circle:before{content:"\ebbe"}.bx-plug:before{content:"\ebbf"}.bx-plus:before{content:"\ebc0"}.bx-plus-circle:before{content:"\ebc1"}.bx-plus-medical:before{content:"\ebc2"}.bx-podcast:before{content:"\ebc3"}.bx-pointer:before{content:"\ebc4"}.bx-poll:before{content:"\ebc5"}.bx-polygon:before{content:"\ebc6"}.bx-pound:before{content:"\ebc7"}.bx-power-off:before{content:"\ebc8"}.bx-printer:before{content:"\ebc9"}.bx-pulse:before{content:"\ebca"}.bx-purchase-tag:before{content:"\ebcb"}.bx-purchase-tag-alt:before{content:"\ebcc"}.bx-pyramid:before{content:"\ebcd"}.bx-qr:before{content:"\ebce"}.bx-qr-scan:before{content:"\ebcf"}.bx-question-mark:before{content:"\ebd0"}.bx-radar:before{content:"\ebd1"}.bx-radio:before{content:"\ebd2"}.bx-radio-circle:before{content:"\ebd3"}.bx-radio-circle-marked:before{content:"\ebd4"}.bx-receipt:before{content:"\ebd5"}.bx-rectangle:before{content:"\ebd6"}.bx-recycle:before{content:"\ebd7"}.bx-redo:before{content:"\ebd8"}.bx-refresh:before{content:"\ebd9"}.bx-registered:before{content:"\ebda"}.bx-rename:before{content:"\ebdb"}.bx-repeat:before{content:"\ebdc"}.bx-reply:before{content:"\ebdd"}.bx-reply-all:before{content:"\ebde"}.bx-repost:before{content:"\ebdf"}.bx-reset:before{content:"\ebe0"}.bx-restaurant:before{content:"\ebe1"}.bx-revision:before{content:"\ebe2"}.bx-rewind:before{content:"\ebe3"}.bx-rewind-circle:before{content:"\ebe4"}.bx-right-arrow:before{content:"\ebe5"}.bx-right-arrow-alt:before{content:"\ebe6"}.bx-right-arrow-circle:before{content:"\ebe7"}.bx-right-down-arrow-circle:before{content:"\ebe8"}.bx-right-indent:before{content:"\ebe9"}.bx-right-top-arrow-circle:before{content:"\ebea"}.bx-rocket:before{content:"\ebeb"}.bx-rotate-left:before{content:"\ebec"}.bx-rotate-right:before{content:"\ebed"}.bx-rss:before{content:"\ebee"}.bx-ruble:before{content:"\ebef"}.bx-ruler:before{content:"\ebf0"}.bx-run:before{content:"\ebf1"}.bx-rupee:before{content:"\ebf2"}.bx-sad:before{content:"\ebf3"}.bx-save:before{content:"\ebf4"}.bx-scan:before{content:"\ebf5"}.bx-screenshot:before{content:"\ebf6"}.bx-search:before{content:"\ebf7"}.bx-search-alt:before{content:"\ebf8"}.bx-search-alt-2:before{content:"\ebf9"}.bx-selection:before{content:"\ebfa"}.bx-select-multiple:before{content:"\ebfb"}.bx-send:before{content:"\ebfc"}.bx-server:before{content:"\ebfd"}.bx-shape-circle:before{content:"\ebfe"}.bx-shape-polygon:before{content:"\ebff"}.bx-shape-square:before{content:"\ec00"}.bx-shape-triangle:before{content:"\ec01"}.bx-share:before{content:"\ec02"}.bx-share-alt:before{content:"\ec03"}.bx-shekel:before{content:"\ec04"}.bx-shield:before{content:"\ec05"}.bx-shield-alt:before{content:"\ec06"}.bx-shield-alt-2:before{content:"\ec07"}.bx-shield-quarter:before{content:"\ec08"}.bx-shield-x:before{content:"\ec09"}.bx-shocked:before{content:"\ec0a"}.bx-shopping-bag:before{content:"\ec0b"}.bx-show:before{content:"\ec0c"}.bx-show-alt:before{content:"\ec0d"}.bx-shuffle:before{content:"\ec0e"}.bx-sidebar:before{content:"\ec0f"}.bx-sitemap:before{content:"\ec10"}.bx-skip-next:before{content:"\ec11"}.bx-skip-next-circle:before{content:"\ec12"}.bx-skip-previous:before{content:"\ec13"}.bx-skip-previous-circle:before{content:"\ec14"}.bx-sleepy:before{content:"\ec15"}.bx-slider:before{content:"\ec16"}.bx-slider-alt:before{content:"\ec17"}.bx-slideshow:before{content:"\ec18"}.bx-smile:before{content:"\ec19"}.bx-sort:before{content:"\ec1a"}.bx-sort-alt-2:before{content:"\ec1b"}.bx-sort-a-z:before{content:"\ec1c"}.bx-sort-down:before{content:"\ec1d"}.bx-sort-up:before{content:"\ec1e"}.bx-sort-z-a:before{content:"\ec1f"}.bx-spa:before{content:"\ec20"}.bx-space-bar:before{content:"\ec21"}.bx-speaker:before{content:"\ec22"}.bx-spray-can:before{content:"\ec23"}.bx-spreadsheet:before{content:"\ec24"}.bx-square:before{content:"\ec25"}.bx-square-rounded:before{content:"\ec26"}.bx-star:before{content:"\ec27"}.bx-station:before{content:"\ec28"}.bx-stats:before{content:"\ec29"}.bx-sticker:before{content:"\ec2a"}.bx-stop:before{content:"\ec2b"}.bx-stop-circle:before{content:"\ec2c"}.bx-stopwatch:before{content:"\ec2d"}.bx-store:before{content:"\ec2e"}.bx-store-alt:before{content:"\ec2f"}.bx-street-view:before{content:"\ec30"}.bx-strikethrough:before{content:"\ec31"}.bx-subdirectory-left:before{content:"\ec32"}.bx-subdirectory-right:before{content:"\ec33"}.bx-sun:before{content:"\ec34"}.bx-support:before{content:"\ec35"}.bx-swim:before{content:"\ec36"}.bx-sync:before{content:"\ec37"}.bx-tab:before{content:"\ec38"}.bx-table:before{content:"\ec39"}.bx-tachometer:before{content:"\ec3a"}.bx-tag:before{content:"\ec3b"}.bx-tag-alt:before{content:"\ec3c"}.bx-target-lock:before{content:"\ec3d"}.bx-task:before{content:"\ec3e"}.bx-task-x:before{content:"\ec3f"}.bx-taxi:before{content:"\ec40"}.bx-tennis-ball:before{content:"\ec41"}.bx-terminal:before{content:"\ec42"}.bx-test-tube:before{content:"\ec43"}.bx-text:before{content:"\ec44"}.bx-time:before{content:"\ec45"}.bx-time-five:before{content:"\ec46"}.bx-timer:before{content:"\ec47"}.bx-tired:before{content:"\ec48"}.bx-toggle-left:before{content:"\ec49"}.bx-toggle-right:before{content:"\ec4a"}.bx-tone:before{content:"\ec4b"}.bx-traffic-cone:before{content:"\ec4c"}.bx-train:before{content:"\ec4d"}.bx-transfer:before{content:"\ec4e"}.bx-transfer-alt:before{content:"\ec4f"}.bx-trash:before{content:"\ec50"}.bx-trash-alt:before{content:"\ec51"}.bx-trending-down:before{content:"\ec52"}.bx-trending-up:before{content:"\ec53"}.bx-trim:before{content:"\ec54"}.bx-trip:before{content:"\ec55"}.bx-trophy:before{content:"\ec56"}.bx-tv:before{content:"\ec57"}.bx-underline:before{content:"\ec58"}.bx-undo:before{content:"\ec59"}.bx-unite:before{content:"\ec5a"}.bx-unlink:before{content:"\ec5b"}.bx-up-arrow:before{content:"\ec5c"}.bx-up-arrow-alt:before{content:"\ec5d"}.bx-up-arrow-circle:before{content:"\ec5e"}.bx-upload:before{content:"\ec5f"}.bx-upside-down:before{content:"\ec60"}.bx-upvote:before{content:"\ec61"}.bx-usb:before{content:"\ec62"}.bx-user:before{content:"\ec63"}.bx-user-check:before{content:"\ec64"}.bx-user-circle:before{content:"\ec65"}.bx-user-minus:before{content:"\ec66"}.bx-user-pin:before{content:"\ec67"}.bx-user-plus:before{content:"\ec68"}.bx-user-voice:before{content:"\ec69"}.bx-user-x:before{content:"\ec6a"}.bx-vector:before{content:"\ec6b"}.bx-vertical-center:before{content:"\ec6c"}.bx-vial:before{content:"\ec6d"}.bx-video:before{content:"\ec6e"}.bx-video-off:before{content:"\ec6f"}.bx-video-plus:before{content:"\ec70"}.bx-video-recording:before{content:"\ec71"}.bx-voicemail:before{content:"\ec72"}.bx-volume:before{content:"\ec73"}.bx-volume-full:before{content:"\ec74"}.bx-volume-low:before{content:"\ec75"}.bx-volume-mute:before{content:"\ec76"}.bx-walk:before{content:"\ec77"}.bx-wallet:before{content:"\ec78"}.bx-wallet-alt:before{content:"\ec79"}.bx-water:before{content:"\ec7a"}.bx-webcam:before{content:"\ec7b"}.bx-wifi:before{content:"\ec7c"}.bx-wifi-0:before{content:"\ec7d"}.bx-wifi-1:before{content:"\ec7e"}.bx-wifi-2:before{content:"\ec7f"}.bx-wifi-off:before{content:"\ec80"}.bx-wind:before{content:"\ec81"}.bx-window:before{content:"\ec82"}.bx-window-alt:before{content:"\ec83"}.bx-window-close:before{content:"\ec84"}.bx-window-open:before{content:"\ec85"}.bx-windows:before{content:"\ec86"}.bx-wine:before{content:"\ec87"}.bx-wink-smile:before{content:"\ec88"}.bx-wink-tongue:before{content:"\ec89"}.bx-won:before{content:"\ec8a"}.bx-world:before{content:"\ec8b"}.bx-wrench:before{content:"\ec8c"}.bx-x:before{content:"\ec8d"}.bx-x-circle:before{content:"\ec8e"}.bx-yen:before{content:"\ec8f"}.bx-zoom-in:before{content:"\ec90"}.bx-zoom-out:before{content:"\ec91"}.bxs-party:before{content:"\ec92"}.bxs-hot:before{content:"\ec93"}.bxs-droplet:before{content:"\ec94"}.bxs-cat:before{content:"\ec95"}.bxs-dog:before{content:"\ec96"}.bxs-injection:before{content:"\ec97"}.bxs-leaf:before{content:"\ec98"}.bxs-add-to-queue:before{content:"\ec99"}.bxs-adjust:before{content:"\ec9a"}.bxs-adjust-alt:before{content:"\ec9b"}.bxs-alarm:before{content:"\ec9c"}.bxs-alarm-add:before{content:"\ec9d"}.bxs-alarm-exclamation:before{content:"\ec9e"}.bxs-alarm-off:before{content:"\ec9f"}.bxs-alarm-snooze:before{content:"\eca0"}.bxs-album:before{content:"\eca1"}.bxs-ambulance:before{content:"\eca2"}.bxs-analyse:before{content:"\eca3"}.bxs-angry:before{content:"\eca4"}.bxs-arch:before{content:"\eca5"}.bxs-archive:before{content:"\eca6"}.bxs-archive-in:before{content:"\eca7"}.bxs-archive-out:before{content:"\eca8"}.bxs-area:before{content:"\eca9"}.bxs-arrow-from-bottom:before{content:"\ecaa"}.bxs-arrow-from-left:before{content:"\ecab"}.bxs-arrow-from-right:before{content:"\ecac"}.bxs-arrow-from-top:before{content:"\ecad"}.bxs-arrow-to-bottom:before{content:"\ecae"}.bxs-arrow-to-left:before{content:"\ecaf"}.bxs-arrow-to-right:before{content:"\ecb0"}.bxs-arrow-to-top:before{content:"\ecb1"}.bxs-award:before{content:"\ecb2"}.bxs-baby-carriage:before{content:"\ecb3"}.bxs-backpack:before{content:"\ecb4"}.bxs-badge:before{content:"\ecb5"}.bxs-badge-check:before{content:"\ecb6"}.bxs-badge-dollar:before{content:"\ecb7"}.bxs-ball:before{content:"\ecb8"}.bxs-band-aid:before{content:"\ecb9"}.bxs-bank:before{content:"\ecba"}.bxs-bar-chart-alt-2:before{content:"\ecbb"}.bxs-bar-chart-square:before{content:"\ecbc"}.bxs-barcode:before{content:"\ecbd"}.bxs-baseball:before{content:"\ecbe"}.bxs-basket:before{content:"\ecbf"}.bxs-basketball:before{content:"\ecc0"}.bxs-bath:before{content:"\ecc1"}.bxs-battery:before{content:"\ecc2"}.bxs-battery-charging:before{content:"\ecc3"}.bxs-battery-full:before{content:"\ecc4"}.bxs-battery-low:before{content:"\ecc5"}.bxs-bed:before{content:"\ecc6"}.bxs-been-here:before{content:"\ecc7"}.bxs-beer:before{content:"\ecc8"}.bxs-bell:before{content:"\ecc9"}.bxs-bell-minus:before{content:"\ecca"}.bxs-bell-off:before{content:"\eccb"}.bxs-bell-plus:before{content:"\eccc"}.bxs-bell-ring:before{content:"\eccd"}.bxs-bible:before{content:"\ecce"}.bxs-binoculars:before{content:"\eccf"}.bxs-blanket:before{content:"\ecd0"}.bxs-bolt:before{content:"\ecd1"}.bxs-bolt-circle:before{content:"\ecd2"}.bxs-bomb:before{content:"\ecd3"}.bxs-bone:before{content:"\ecd4"}.bxs-bong:before{content:"\ecd5"}.bxs-book:before{content:"\ecd6"}.bxs-book-add:before{content:"\ecd7"}.bxs-book-alt:before{content:"\ecd8"}.bxs-book-bookmark:before{content:"\ecd9"}.bxs-book-content:before{content:"\ecda"}.bxs-book-heart:before{content:"\ecdb"}.bxs-bookmark:before{content:"\ecdc"}.bxs-bookmark-alt:before{content:"\ecdd"}.bxs-bookmark-alt-minus:before{content:"\ecde"}.bxs-bookmark-alt-plus:before{content:"\ecdf"}.bxs-bookmark-heart:before{content:"\ece0"}.bxs-bookmark-minus:before{content:"\ece1"}.bxs-bookmark-plus:before{content:"\ece2"}.bxs-bookmarks:before{content:"\ece3"}.bxs-bookmark-star:before{content:"\ece4"}.bxs-book-open:before{content:"\ece5"}.bxs-book-reader:before{content:"\ece6"}.bxs-bot:before{content:"\ece7"}.bxs-bowling-ball:before{content:"\ece8"}.bxs-box:before{content:"\ece9"}.bxs-brain:before{content:"\ecea"}.bxs-briefcase:before{content:"\eceb"}.bxs-briefcase-alt:before{content:"\ecec"}.bxs-briefcase-alt-2:before{content:"\eced"}.bxs-brightness:before{content:"\ecee"}.bxs-brightness-half:before{content:"\ecef"}.bxs-brush:before{content:"\ecf0"}.bxs-brush-alt:before{content:"\ecf1"}.bxs-bug:before{content:"\ecf2"}.bxs-bug-alt:before{content:"\ecf3"}.bxs-building:before{content:"\ecf4"}.bxs-building-house:before{content:"\ecf5"}.bxs-buildings:before{content:"\ecf6"}.bxs-bulb:before{content:"\ecf7"}.bxs-bullseye:before{content:"\ecf8"}.bxs-buoy:before{content:"\ecf9"}.bxs-bus:before{content:"\ecfa"}.bxs-business:before{content:"\ecfb"}.bxs-bus-school:before{content:"\ecfc"}.bxs-cabinet:before{content:"\ecfd"}.bxs-cake:before{content:"\ecfe"}.bxs-calculator:before{content:"\ecff"}.bxs-calendar:before{content:"\ed00"}.bxs-calendar-alt:before{content:"\ed01"}.bxs-calendar-check:before{content:"\ed02"}.bxs-calendar-edit:before{content:"\ed03"}.bxs-calendar-event:before{content:"\ed04"}.bxs-calendar-exclamation:before{content:"\ed05"}.bxs-calendar-heart:before{content:"\ed06"}.bxs-calendar-minus:before{content:"\ed07"}.bxs-calendar-plus:before{content:"\ed08"}.bxs-calendar-star:before{content:"\ed09"}.bxs-calendar-week:before{content:"\ed0a"}.bxs-calendar-x:before{content:"\ed0b"}.bxs-camera:before{content:"\ed0c"}.bxs-camera-home:before{content:"\ed0d"}.bxs-camera-movie:before{content:"\ed0e"}.bxs-camera-off:before{content:"\ed0f"}.bxs-camera-plus:before{content:"\ed10"}.bxs-capsule:before{content:"\ed11"}.bxs-captions:before{content:"\ed12"}.bxs-car:before{content:"\ed13"}.bxs-car-battery:before{content:"\ed14"}.bxs-car-crash:before{content:"\ed15"}.bxs-card:before{content:"\ed16"}.bxs-caret-down-circle:before{content:"\ed17"}.bxs-caret-down-square:before{content:"\ed18"}.bxs-caret-left-circle:before{content:"\ed19"}.bxs-caret-left-square:before{content:"\ed1a"}.bxs-caret-right-circle:before{content:"\ed1b"}.bxs-caret-right-square:before{content:"\ed1c"}.bxs-caret-up-circle:before{content:"\ed1d"}.bxs-caret-up-square:before{content:"\ed1e"}.bxs-car-garage:before{content:"\ed1f"}.bxs-car-mechanic:before{content:"\ed20"}.bxs-carousel:before{content:"\ed21"}.bxs-cart:before{content:"\ed22"}.bxs-cart-add:before{content:"\ed23"}.bxs-cart-alt:before{content:"\ed24"}.bxs-cart-download:before{content:"\ed25"}.bxs-car-wash:before{content:"\ed26"}.bxs-category:before{content:"\ed27"}.bxs-category-alt:before{content:"\ed28"}.bxs-cctv:before{content:"\ed29"}.bxs-certification:before{content:"\ed2a"}.bxs-chalkboard:before{content:"\ed2b"}.bxs-chart:before{content:"\ed2c"}.bxs-chat:before{content:"\ed2d"}.bxs-checkbox:before{content:"\ed2e"}.bxs-checkbox-checked:before{content:"\ed2f"}.bxs-checkbox-minus:before{content:"\ed30"}.bxs-check-circle:before{content:"\ed31"}.bxs-check-shield:before{content:"\ed32"}.bxs-check-square:before{content:"\ed33"}.bxs-chess:before{content:"\ed34"}.bxs-chevron-down:before{content:"\ed35"}.bxs-chevron-down-circle:before{content:"\ed36"}.bxs-chevron-down-square:before{content:"\ed37"}.bxs-chevron-left:before{content:"\ed38"}.bxs-chevron-left-circle:before{content:"\ed39"}.bxs-chevron-left-square:before{content:"\ed3a"}.bxs-chevron-right:before{content:"\ed3b"}.bxs-chevron-right-circle:before{content:"\ed3c"}.bxs-chevron-right-square:before{content:"\ed3d"}.bxs-chevrons-down:before{content:"\ed3e"}.bxs-chevrons-left:before{content:"\ed3f"}.bxs-chevrons-right:before{content:"\ed40"}.bxs-chevrons-up:before{content:"\ed41"}.bxs-chevron-up:before{content:"\ed42"}.bxs-chevron-up-circle:before{content:"\ed43"}.bxs-chevron-up-square:before{content:"\ed44"}.bxs-chip:before{content:"\ed45"}.bxs-church:before{content:"\ed46"}.bxs-circle:before{content:"\ed47"}.bxs-city:before{content:"\ed48"}.bxs-clinic:before{content:"\ed49"}.bxs-cloud:before{content:"\ed4a"}.bxs-cloud-download:before{content:"\ed4b"}.bxs-cloud-lightning:before{content:"\ed4c"}.bxs-cloud-rain:before{content:"\ed4d"}.bxs-cloud-upload:before{content:"\ed4e"}.bxs-coffee:before{content:"\ed4f"}.bxs-coffee-alt:before{content:"\ed50"}.bxs-coffee-togo:before{content:"\ed51"}.bxs-cog:before{content:"\ed52"}.bxs-coin:before{content:"\ed53"}.bxs-coin-stack:before{content:"\ed54"}.bxs-collection:before{content:"\ed55"}.bxs-color-fill:before{content:"\ed56"}.bxs-comment:before{content:"\ed57"}.bxs-comment-add:before{content:"\ed58"}.bxs-comment-check:before{content:"\ed59"}.bxs-comment-detail:before{content:"\ed5a"}.bxs-comment-dots:before{content:"\ed5b"}.bxs-comment-edit:before{content:"\ed5c"}.bxs-comment-error:before{content:"\ed5d"}.bxs-comment-minus:before{content:"\ed5e"}.bxs-comment-x:before{content:"\ed5f"}.bxs-compass:before{content:"\ed60"}.bxs-component:before{content:"\ed61"}.bxs-confused:before{content:"\ed62"}.bxs-contact:before{content:"\ed63"}.bxs-conversation:before{content:"\ed64"}.bxs-cookie:before{content:"\ed65"}.bxs-cool:before{content:"\ed66"}.bxs-copy:before{content:"\ed67"}.bxs-copy-alt:before{content:"\ed68"}.bxs-copyright:before{content:"\ed69"}.bxs-coupon:before{content:"\ed6a"}.bxs-credit-card:before{content:"\ed6b"}.bxs-credit-card-alt:before{content:"\ed6c"}.bxs-credit-card-front:before{content:"\ed6d"}.bxs-crop:before{content:"\ed6e"}.bxs-crown:before{content:"\ed6f"}.bxs-cube:before{content:"\ed70"}.bxs-cube-alt:before{content:"\ed71"}.bxs-cuboid:before{content:"\ed72"}.bxs-customize:before{content:"\ed73"}.bxs-cylinder:before{content:"\ed74"}.bxs-dashboard:before{content:"\ed75"}.bxs-data:before{content:"\ed76"}.bxs-detail:before{content:"\ed77"}.bxs-devices:before{content:"\ed78"}.bxs-diamond:before{content:"\ed79"}.bxs-dice-1:before{content:"\ed7a"}.bxs-dice-2:before{content:"\ed7b"}.bxs-dice-3:before{content:"\ed7c"}.bxs-dice-4:before{content:"\ed7d"}.bxs-dice-5:before{content:"\ed7e"}.bxs-dice-6:before{content:"\ed7f"}.bxs-direction-left:before{content:"\ed80"}.bxs-direction-right:before{content:"\ed81"}.bxs-directions:before{content:"\ed82"}.bxs-disc:before{content:"\ed83"}.bxs-discount:before{content:"\ed84"}.bxs-dish:before{content:"\ed85"}.bxs-dislike:before{content:"\ed86"}.bxs-dizzy:before{content:"\ed87"}.bxs-dock-bottom:before{content:"\ed88"}.bxs-dock-left:before{content:"\ed89"}.bxs-dock-right:before{content:"\ed8a"}.bxs-dock-top:before{content:"\ed8b"}.bxs-dollar-circle:before{content:"\ed8c"}.bxs-donate-blood:before{content:"\ed8d"}.bxs-donate-heart:before{content:"\ed8e"}.bxs-door-open:before{content:"\ed8f"}.bxs-doughnut-chart:before{content:"\ed90"}.bxs-down-arrow:before{content:"\ed91"}.bxs-down-arrow-alt:before{content:"\ed92"}.bxs-down-arrow-circle:before{content:"\ed93"}.bxs-down-arrow-square:before{content:"\ed94"}.bxs-download:before{content:"\ed95"}.bxs-downvote:before{content:"\ed96"}.bxs-drink:before{content:"\ed97"}.bxs-droplet-half:before{content:"\ed98"}.bxs-dryer:before{content:"\ed99"}.bxs-duplicate:before{content:"\ed9a"}.bxs-edit:before{content:"\ed9b"}.bxs-edit-alt:before{content:"\ed9c"}.bxs-edit-location:before{content:"\ed9d"}.bxs-eject:before{content:"\ed9e"}.bxs-envelope:before{content:"\ed9f"}.bxs-envelope-open:before{content:"\eda0"}.bxs-eraser:before{content:"\eda1"}.bxs-error:before{content:"\eda2"}.bxs-error-alt:before{content:"\eda3"}.bxs-error-circle:before{content:"\eda4"}.bxs-ev-station:before{content:"\eda5"}.bxs-exit:before{content:"\eda6"}.bxs-extension:before{content:"\eda7"}.bxs-eyedropper:before{content:"\eda8"}.bxs-face:before{content:"\eda9"}.bxs-face-mask:before{content:"\edaa"}.bxs-factory:before{content:"\edab"}.bxs-fast-forward-circle:before{content:"\edac"}.bxs-file:before{content:"\edad"}.bxs-file-archive:before{content:"\edae"}.bxs-file-blank:before{content:"\edaf"}.bxs-file-css:before{content:"\edb0"}.bxs-file-doc:before{content:"\edb1"}.bxs-file-export:before{content:"\edb2"}.bxs-file-find:before{content:"\edb3"}.bxs-file-gif:before{content:"\edb4"}.bxs-file-html:before{content:"\edb5"}.bxs-file-image:before{content:"\edb6"}.bxs-file-import:before{content:"\edb7"}.bxs-file-jpg:before{content:"\edb8"}.bxs-file-js:before{content:"\edb9"}.bxs-file-json:before{content:"\edba"}.bxs-file-md:before{content:"\edbb"}.bxs-file-pdf:before{content:"\edbc"}.bxs-file-plus:before{content:"\edbd"}.bxs-file-png:before{content:"\edbe"}.bxs-file-txt:before{content:"\edbf"}.bxs-film:before{content:"\edc0"}.bxs-filter-alt:before{content:"\edc1"}.bxs-first-aid:before{content:"\edc2"}.bxs-flag:before{content:"\edc3"}.bxs-flag-alt:before{content:"\edc4"}.bxs-flag-checkered:before{content:"\edc5"}.bxs-flame:before{content:"\edc6"}.bxs-flask:before{content:"\edc7"}.bxs-florist:before{content:"\edc8"}.bxs-folder:before{content:"\edc9"}.bxs-folder-minus:before{content:"\edca"}.bxs-folder-open:before{content:"\edcb"}.bxs-folder-plus:before{content:"\edcc"}.bxs-food-menu:before{content:"\edcd"}.bxs-fridge:before{content:"\edce"}.bxs-game:before{content:"\edcf"}.bxs-gas-pump:before{content:"\edd0"}.bxs-ghost:before{content:"\edd1"}.bxs-gift:before{content:"\edd2"}.bxs-graduation:before{content:"\edd3"}.bxs-grid:before{content:"\edd4"}.bxs-grid-alt:before{content:"\edd5"}.bxs-group:before{content:"\edd6"}.bxs-guitar-amp:before{content:"\edd7"}.bxs-hand:before{content:"\edd8"}.bxs-hand-down:before{content:"\edd9"}.bxs-hand-left:before{content:"\edda"}.bxs-hand-right:before{content:"\eddb"}.bxs-hand-up:before{content:"\eddc"}.bxs-happy:before{content:"\eddd"}.bxs-happy-alt:before{content:"\edde"}.bxs-happy-beaming:before{content:"\eddf"}.bxs-happy-heart-eyes:before{content:"\ede0"}.bxs-hdd:before{content:"\ede1"}.bxs-heart:before{content:"\ede2"}.bxs-heart-circle:before{content:"\ede3"}.bxs-heart-square:before{content:"\ede4"}.bxs-help-circle:before{content:"\ede5"}.bxs-hide:before{content:"\ede6"}.bxs-home:before{content:"\ede7"}.bxs-home-circle:before{content:"\ede8"}.bxs-home-heart:before{content:"\ede9"}.bxs-home-smile:before{content:"\edea"}.bxs-hotel:before{content:"\edeb"}.bxs-hourglass:before{content:"\edec"}.bxs-hourglass-bottom:before{content:"\eded"}.bxs-hourglass-top:before{content:"\edee"}.bxs-id-card:before{content:"\edef"}.bxs-image:before{content:"\edf0"}.bxs-image-add:before{content:"\edf1"}.bxs-image-alt:before{content:"\edf2"}.bxs-inbox:before{content:"\edf3"}.bxs-info-circle:before{content:"\edf4"}.bxs-info-square:before{content:"\edf5"}.bxs-institution:before{content:"\edf6"}.bxs-joystick:before{content:"\edf7"}.bxs-joystick-alt:before{content:"\edf8"}.bxs-joystick-button:before{content:"\edf9"}.bxs-key:before{content:"\edfa"}.bxs-keyboard:before{content:"\edfb"}.bxs-label:before{content:"\edfc"}.bxs-landmark:before{content:"\edfd"}.bxs-landscape:before{content:"\edfe"}.bxs-laugh:before{content:"\edff"}.bxs-layer:before{content:"\ee00"}.bxs-layer-minus:before{content:"\ee01"}.bxs-layer-plus:before{content:"\ee02"}.bxs-layout:before{content:"\ee03"}.bxs-left-arrow:before{content:"\ee04"}.bxs-left-arrow-alt:before{content:"\ee05"}.bxs-left-arrow-circle:before{content:"\ee06"}.bxs-left-arrow-square:before{content:"\ee07"}.bxs-left-down-arrow-circle:before{content:"\ee08"}.bxs-left-top-arrow-circle:before{content:"\ee09"}.bxs-like:before{content:"\ee0a"}.bxs-location-plus:before{content:"\ee0b"}.bxs-lock:before{content:"\ee0c"}.bxs-lock-alt:before{content:"\ee0d"}.bxs-lock-open:before{content:"\ee0e"}.bxs-lock-open-alt:before{content:"\ee0f"}.bxs-log-in:before{content:"\ee10"}.bxs-log-in-circle:before{content:"\ee11"}.bxs-log-out:before{content:"\ee12"}.bxs-log-out-circle:before{content:"\ee13"}.bxs-low-vision:before{content:"\ee14"}.bxs-magic-wand:before{content:"\ee15"}.bxs-magnet:before{content:"\ee16"}.bxs-map:before{content:"\ee17"}.bxs-map-alt:before{content:"\ee18"}.bxs-map-pin:before{content:"\ee19"}.bxs-mask:before{content:"\ee1a"}.bxs-medal:before{content:"\ee1b"}.bxs-megaphone:before{content:"\ee1c"}.bxs-meh:before{content:"\ee1d"}.bxs-meh-alt:before{content:"\ee1e"}.bxs-meh-blank:before{content:"\ee1f"}.bxs-memory-card:before{content:"\ee20"}.bxs-message:before{content:"\ee21"}.bxs-message-add:before{content:"\ee22"}.bxs-message-alt:before{content:"\ee23"}.bxs-message-alt-add:before{content:"\ee24"}.bxs-message-alt-check:before{content:"\ee25"}.bxs-message-alt-detail:before{content:"\ee26"}.bxs-message-alt-dots:before{content:"\ee27"}.bxs-message-alt-edit:before{content:"\ee28"}.bxs-message-alt-error:before{content:"\ee29"}.bxs-message-alt-minus:before{content:"\ee2a"}.bxs-message-alt-x:before{content:"\ee2b"}.bxs-message-check:before{content:"\ee2c"}.bxs-message-detail:before{content:"\ee2d"}.bxs-message-dots:before{content:"\ee2e"}.bxs-message-edit:before{content:"\ee2f"}.bxs-message-error:before{content:"\ee30"}.bxs-message-minus:before{content:"\ee31"}.bxs-message-rounded:before{content:"\ee32"}.bxs-message-rounded-add:before{content:"\ee33"}.bxs-message-rounded-check:before{content:"\ee34"}.bxs-message-rounded-detail:before{content:"\ee35"}.bxs-message-rounded-dots:before{content:"\ee36"}.bxs-message-rounded-edit:before{content:"\ee37"}.bxs-message-rounded-error:before{content:"\ee38"}.bxs-message-rounded-minus:before{content:"\ee39"}.bxs-message-rounded-x:before{content:"\ee3a"}.bxs-message-square:before{content:"\ee3b"}.bxs-message-square-add:before{content:"\ee3c"}.bxs-message-square-check:before{content:"\ee3d"}.bxs-message-square-detail:before{content:"\ee3e"}.bxs-message-square-dots:before{content:"\ee3f"}.bxs-message-square-edit:before{content:"\ee40"}.bxs-message-square-error:before{content:"\ee41"}.bxs-message-square-minus:before{content:"\ee42"}.bxs-message-square-x:before{content:"\ee43"}.bxs-message-x:before{content:"\ee44"}.bxs-meteor:before{content:"\ee45"}.bxs-microchip:before{content:"\ee46"}.bxs-microphone:before{content:"\ee47"}.bxs-microphone-alt:before{content:"\ee48"}.bxs-microphone-off:before{content:"\ee49"}.bxs-minus-circle:before{content:"\ee4a"}.bxs-minus-square:before{content:"\ee4b"}.bxs-mobile:before{content:"\ee4c"}.bxs-mobile-vibration:before{content:"\ee4d"}.bxs-moon:before{content:"\ee4e"}.bxs-mouse:before{content:"\ee4f"}.bxs-mouse-alt:before{content:"\ee50"}.bxs-movie:before{content:"\ee51"}.bxs-movie-play:before{content:"\ee52"}.bxs-music:before{content:"\ee53"}.bxs-navigation:before{content:"\ee54"}.bxs-network-chart:before{content:"\ee55"}.bxs-news:before{content:"\ee56"}.bxs-no-entry:before{content:"\ee57"}.bxs-note:before{content:"\ee58"}.bxs-notepad:before{content:"\ee59"}.bxs-notification:before{content:"\ee5a"}.bxs-notification-off:before{content:"\ee5b"}.bxs-offer:before{content:"\ee5c"}.bxs-package:before{content:"\ee5d"}.bxs-paint:before{content:"\ee5e"}.bxs-paint-roll:before{content:"\ee5f"}.bxs-palette:before{content:"\ee60"}.bxs-paper-plane:before{content:"\ee61"}.bxs-parking:before{content:"\ee62"}.bxs-paste:before{content:"\ee63"}.bxs-pen:before{content:"\ee64"}.bxs-pencil:before{content:"\ee65"}.bxs-phone:before{content:"\ee66"}.bxs-phone-call:before{content:"\ee67"}.bxs-phone-incoming:before{content:"\ee68"}.bxs-phone-off:before{content:"\ee69"}.bxs-phone-outgoing:before{content:"\ee6a"}.bxs-photo-album:before{content:"\ee6b"}.bxs-piano:before{content:"\ee6c"}.bxs-pie-chart:before{content:"\ee6d"}.bxs-pie-chart-alt:before{content:"\ee6e"}.bxs-pie-chart-alt-2:before{content:"\ee6f"}.bxs-pin:before{content:"\ee70"}.bxs-pizza:before{content:"\ee71"}.bxs-plane:before{content:"\ee72"}.bxs-plane-alt:before{content:"\ee73"}.bxs-plane-land:before{content:"\ee74"}.bxs-planet:before{content:"\ee75"}.bxs-plane-take-off:before{content:"\ee76"}.bxs-playlist:before{content:"\ee77"}.bxs-plug:before{content:"\ee78"}.bxs-plus-circle:before{content:"\ee79"}.bxs-plus-square:before{content:"\ee7a"}.bxs-pointer:before{content:"\ee7b"}.bxs-polygon:before{content:"\ee7c"}.bxs-printer:before{content:"\ee7d"}.bxs-purchase-tag:before{content:"\ee7e"}.bxs-purchase-tag-alt:before{content:"\ee7f"}.bxs-pyramid:before{content:"\ee80"}.bxs-quote-alt-left:before{content:"\ee81"}.bxs-quote-alt-right:before{content:"\ee82"}.bxs-quote-left:before{content:"\ee83"}.bxs-quote-right:before{content:"\ee84"}.bxs-quote-single-left:before{content:"\ee85"}.bxs-quote-single-right:before{content:"\ee86"}.bxs-radiation:before{content:"\ee87"}.bxs-radio:before{content:"\ee88"}.bxs-receipt:before{content:"\ee89"}.bxs-rectangle:before{content:"\ee8a"}.bxs-registered:before{content:"\ee8b"}.bxs-rename:before{content:"\ee8c"}.bxs-report:before{content:"\ee8d"}.bxs-rewind-circle:before{content:"\ee8e"}.bxs-right-arrow:before{content:"\ee8f"}.bxs-right-arrow-alt:before{content:"\ee90"}.bxs-right-arrow-circle:before{content:"\ee91"}.bxs-right-arrow-square:before{content:"\ee92"}.bxs-right-down-arrow-circle:before{content:"\ee93"}.bxs-right-top-arrow-circle:before{content:"\ee94"}.bxs-rocket:before{content:"\ee95"}.bxs-ruler:before{content:"\ee96"}.bxs-sad:before{content:"\ee97"}.bxs-save:before{content:"\ee98"}.bxs-school:before{content:"\ee99"}.bxs-search:before{content:"\ee9a"}.bxs-search-alt-2:before{content:"\ee9b"}.bxs-select-multiple:before{content:"\ee9c"}.bxs-send:before{content:"\ee9d"}.bxs-server:before{content:"\ee9e"}.bxs-shapes:before{content:"\ee9f"}.bxs-share:before{content:"\eea0"}.bxs-share-alt:before{content:"\eea1"}.bxs-shield:before{content:"\eea2"}.bxs-shield-alt-2:before{content:"\eea3"}.bxs-shield-x:before{content:"\eea4"}.bxs-ship:before{content:"\eea5"}.bxs-shocked:before{content:"\eea6"}.bxs-shopping-bag:before{content:"\eea7"}.bxs-shopping-bag-alt:before{content:"\eea8"}.bxs-shopping-bags:before{content:"\eea9"}.bxs-show:before{content:"\eeaa"}.bxs-skip-next-circle:before{content:"\eeab"}.bxs-skip-previous-circle:before{content:"\eeac"}.bxs-skull:before{content:"\eead"}.bxs-sleepy:before{content:"\eeae"}.bxs-slideshow:before{content:"\eeaf"}.bxs-smile:before{content:"\eeb0"}.bxs-sort-alt:before{content:"\eeb1"}.bxs-spa:before{content:"\eeb2"}.bxs-speaker:before{content:"\eeb3"}.bxs-spray-can:before{content:"\eeb4"}.bxs-spreadsheet:before{content:"\eeb5"}.bxs-square:before{content:"\eeb6"}.bxs-square-rounded:before{content:"\eeb7"}.bxs-star:before{content:"\eeb8"}.bxs-star-half:before{content:"\eeb9"}.bxs-sticker:before{content:"\eeba"}.bxs-stopwatch:before{content:"\eebb"}.bxs-store:before{content:"\eebc"}.bxs-store-alt:before{content:"\eebd"}.bxs-sun:before{content:"\eebe"}.bxs-tachometer:before{content:"\eebf"}.bxs-tag:before{content:"\eec0"}.bxs-tag-alt:before{content:"\eec1"}.bxs-tag-x:before{content:"\eec2"}.bxs-taxi:before{content:"\eec3"}.bxs-tennis-ball:before{content:"\eec4"}.bxs-terminal:before{content:"\eec5"}.bxs-thermometer:before{content:"\eec6"}.bxs-time:before{content:"\eec7"}.bxs-time-five:before{content:"\eec8"}.bxs-timer:before{content:"\eec9"}.bxs-tired:before{content:"\eeca"}.bxs-toggle-left:before{content:"\eecb"}.bxs-toggle-right:before{content:"\eecc"}.bxs-tone:before{content:"\eecd"}.bxs-torch:before{content:"\eece"}.bxs-to-top:before{content:"\eecf"}.bxs-traffic:before{content:"\eed0"}.bxs-traffic-barrier:before{content:"\eed1"}.bxs-traffic-cone:before{content:"\eed2"}.bxs-train:before{content:"\eed3"}.bxs-trash:before{content:"\eed4"}.bxs-trash-alt:before{content:"\eed5"}.bxs-tree:before{content:"\eed6"}.bxs-trophy:before{content:"\eed7"}.bxs-truck:before{content:"\eed8"}.bxs-t-shirt:before{content:"\eed9"}.bxs-tv:before{content:"\eeda"}.bxs-up-arrow:before{content:"\eedb"}.bxs-up-arrow-alt:before{content:"\eedc"}.bxs-up-arrow-circle:before{content:"\eedd"}.bxs-up-arrow-square:before{content:"\eede"}.bxs-upside-down:before{content:"\eedf"}.bxs-upvote:before{content:"\eee0"}.bxs-user:before{content:"\eee1"}.bxs-user-account:before{content:"\eee2"}.bxs-user-badge:before{content:"\eee3"}.bxs-user-check:before{content:"\eee4"}.bxs-user-circle:before{content:"\eee5"}.bxs-user-detail:before{content:"\eee6"}.bxs-user-minus:before{content:"\eee7"}.bxs-user-pin:before{content:"\eee8"}.bxs-user-plus:before{content:"\eee9"}.bxs-user-rectangle:before{content:"\eeea"}.bxs-user-voice:before{content:"\eeeb"}.bxs-user-x:before{content:"\eeec"}.bxs-vector:before{content:"\eeed"}.bxs-vial:before{content:"\eeee"}.bxs-video:before{content:"\eeef"}.bxs-video-off:before{content:"\eef0"}.bxs-video-plus:before{content:"\eef1"}.bxs-video-recording:before{content:"\eef2"}.bxs-videos:before{content:"\eef3"}.bxs-virus:before{content:"\eef4"}.bxs-virus-block:before{content:"\eef5"}.bxs-volume:before{content:"\eef6"}.bxs-volume-full:before{content:"\eef7"}.bxs-volume-low:before{content:"\eef8"}.bxs-volume-mute:before{content:"\eef9"}.bxs-wallet:before{content:"\eefa"}.bxs-wallet-alt:before{content:"\eefb"}.bxs-washer:before{content:"\eefc"}.bxs-watch:before{content:"\eefd"}.bxs-watch-alt:before{content:"\eefe"}.bxs-webcam:before{content:"\eeff"}.bxs-widget:before{content:"\ef00"}.bxs-window-alt:before{content:"\ef01"}.bxs-wine:before{content:"\ef02"}.bxs-wink-smile:before{content:"\ef03"}.bxs-wink-tongue:before{content:"\ef04"}.bxs-wrench:before{content:"\ef05"}.bxs-x-circle:before{content:"\ef06"}.bxs-x-square:before{content:"\ef07"}.bxs-yin-yang:before{content:"\ef08"}.bxs-zap:before{content:"\ef09"}.bxs-zoom-in:before{content:"\ef0a"}.bxs-zoom-out:before{content:"\ef0b"} \ No newline at end of file diff --git a/themes/30coffe/assets/css/magnific-popup.min.css b/themes/30coffe/assets/css/magnific-popup.min.css new file mode 100644 index 00000000..c00c6ff9 --- /dev/null +++ b/themes/30coffe/assets/css/magnific-popup.min.css @@ -0,0 +1 @@ +.mfp-bg{top:0;left:0;width:100%;height:100%;z-index:1042;overflow:hidden;position:fixed;background:#0b0b0b;opacity:.8}.mfp-wrap{top:0;left:0;width:100%;height:100%;z-index:1043;position:fixed;outline:none !important;-webkit-backface-visibility:hidden}.mfp-container{text-align:center;position:absolute;width:100%;height:100%;left:0;top:0;padding:0 8px;box-sizing:border-box}.mfp-container:before{content:'';display:inline-block;height:100%;vertical-align:middle}.mfp-align-top .mfp-container:before{display:none}.mfp-content{position:relative;display:inline-block;vertical-align:middle;margin:0 auto;text-align:left;z-index:1045}.mfp-inline-holder .mfp-content,.mfp-ajax-holder .mfp-content{width:100%;cursor:auto}.mfp-ajax-cur{cursor:progress}.mfp-zoom-out-cur,.mfp-zoom-out-cur .mfp-image-holder .mfp-close{cursor:-moz-zoom-out;cursor:-webkit-zoom-out;cursor:zoom-out}.mfp-zoom{cursor:pointer;cursor:-webkit-zoom-in;cursor:-moz-zoom-in;cursor:zoom-in}.mfp-auto-cursor .mfp-content{cursor:auto}.mfp-close,.mfp-arrow,.mfp-preloader,.mfp-counter{-webkit-user-select:none;-moz-user-select:none;user-select:none}.mfp-loading.mfp-figure{display:none}.mfp-hide{display:none !important}.mfp-preloader{color:#CCC;position:absolute;top:50%;width:auto;text-align:center;margin-top:-0.8em;left:8px;right:8px;z-index:1044}.mfp-preloader a{color:#CCC}.mfp-preloader a:hover{color:#FFF}.mfp-s-ready .mfp-preloader{display:none}.mfp-s-error .mfp-content{display:none}button.mfp-close,button.mfp-arrow{overflow:visible;cursor:pointer;background:transparent;border:0;-webkit-appearance:none;display:block;outline:0;padding:0;z-index:1046;box-shadow:none;touch-action:manipulation}button::-moz-focus-inner{padding:0;border:0}.mfp-close{width:44px;height:44px;line-height:44px;position:absolute;right:0;top:0;text-decoration:none;text-align:center;opacity:.65;padding:0 0 18px 10px;color:#FFF;font-style:normal;font-size:28px;font-family:Arial,Baskerville,monospace}.mfp-close:hover,.mfp-close:focus{opacity:1}.mfp-close:active{top:1px}.mfp-close-btn-in .mfp-close{color:#333}.mfp-image-holder .mfp-close,.mfp-iframe-holder .mfp-close{color:#FFF;right:-6px;text-align:right;padding-right:6px;width:100%}.mfp-counter{position:absolute;top:0;right:0;color:#CCC;font-size:12px;line-height:18px;white-space:nowrap}.mfp-arrow{position:absolute;opacity:.65;margin:0;top:50%;margin-top:-55px;padding:0;width:90px;height:110px;-webkit-tap-highlight-color:transparent}.mfp-arrow:active{margin-top:-54px}.mfp-arrow:hover,.mfp-arrow:focus{opacity:1}.mfp-arrow:before,.mfp-arrow:after{content:'';display:block;width:0;height:0;position:absolute;left:0;top:0;margin-top:35px;margin-left:35px;border:medium inset transparent}.mfp-arrow:after{border-top-width:13px;border-bottom-width:13px;top:8px}.mfp-arrow:before{border-top-width:21px;border-bottom-width:21px;opacity:.7}.mfp-arrow-left{left:0}.mfp-arrow-left:after{border-right:17px solid #FFF;margin-left:31px}.mfp-arrow-left:before{margin-left:25px;border-right:27px solid #3f3f3f}.mfp-arrow-right{right:0}.mfp-arrow-right:after{border-left:17px solid #FFF;margin-left:39px}.mfp-arrow-right:before{border-left:27px solid #3f3f3f}.mfp-iframe-holder{padding-top:40px;padding-bottom:40px}.mfp-iframe-holder .mfp-content{line-height:0;width:100%;max-width:900px}.mfp-iframe-holder .mfp-close{top:-40px}.mfp-iframe-scaler{width:100%;height:0;overflow:hidden;padding-top:56.25%}.mfp-iframe-scaler iframe{position:absolute;display:block;top:0;left:0;width:100%;height:100%;box-shadow:0 0 8px rgba(0,0,0,0.6);background:#000}img.mfp-img{width:auto;max-width:100%;height:auto;display:block;line-height:0;box-sizing:border-box;padding:40px 0 40px;margin:0 auto}.mfp-figure{line-height:0}.mfp-figure:after{content:'';position:absolute;left:0;top:40px;bottom:40px;display:block;right:0;width:auto;height:auto;z-index:-1;box-shadow:0 0 8px rgba(0,0,0,0.6);background:#444}.mfp-figure small{color:#bdbdbd;display:block;font-size:12px;line-height:14px}.mfp-figure figure{margin:0}.mfp-bottom-bar{margin-top:-36px;position:absolute;top:100%;left:0;width:100%;cursor:auto}.mfp-title{text-align:left;line-height:18px;color:#f3f3f3;word-wrap:break-word;padding-right:36px}.mfp-image-holder .mfp-content{max-width:100%}.mfp-gallery .mfp-image-holder .mfp-figure{cursor:pointer}@media screen and (max-width:800px) and (orientation:landscape),screen and (max-height:300px){.mfp-img-mobile .mfp-image-holder{padding-left:0;padding-right:0}.mfp-img-mobile img.mfp-img{padding:0}.mfp-img-mobile .mfp-figure:after{top:0;bottom:0}.mfp-img-mobile .mfp-figure small{display:inline;margin-left:5px}.mfp-img-mobile .mfp-bottom-bar{background:rgba(0,0,0,0.6);bottom:0;margin:0;top:auto;padding:3px 5px;position:fixed;box-sizing:border-box}.mfp-img-mobile .mfp-bottom-bar:empty{padding:0}.mfp-img-mobile .mfp-counter{right:5px;top:3px}.mfp-img-mobile .mfp-close{top:0;right:0;width:35px;height:35px;line-height:35px;background:rgba(0,0,0,0.6);position:fixed;text-align:center;padding:0}}@media all and (max-width:900px){.mfp-arrow{-webkit-transform:scale(0.75);transform:scale(0.75)}.mfp-arrow-left{-webkit-transform-origin:0 0;transform-origin:0 0}.mfp-arrow-right{-webkit-transform-origin:100%;transform-origin:100%}.mfp-container{padding-left:6px;padding-right:6px}} \ No newline at end of file diff --git a/themes/30coffe/assets/css/meanmenu.css b/themes/30coffe/assets/css/meanmenu.css new file mode 100644 index 00000000..02b77f43 --- /dev/null +++ b/themes/30coffe/assets/css/meanmenu.css @@ -0,0 +1 @@ +@import "https://fonts.googleapis.com/css?family=Oswald:200,300,400,500,600,700";body{font-family:oswald,sans-serif;padding:0;margin:0;font-size:15px;background-color:#f1f1f1}a.meanmenu-reveal{display:none}.mean-container .mean-bar{float:left;width:100%;position:absolute;background:0 0;padding:20px 0 0;z-index:999;border-bottom:1px solid rgba(0,0,0,.03);height:55px}.mean-container a.meanmenu-reveal{width:35px;height:30px;padding:12px 15px 0 0;position:absolute;right:0;cursor:pointer;color:#fff;text-decoration:none;font-size:16px;text-indent:-9999em;line-height:22px;font-size:1px;display:block;font-weight:700}.mean-container a.meanmenu-reveal span{display:block;background:#fff;height:4px;margin-top:3px;border-radius:3px}.mean-container .mean-nav{float:left;width:100%;background:#fff;margin-top:55px}.mean-container .mean-nav ul{padding:0;margin:0;width:100%;border:none;list-style-type:none}.mean-container .mean-nav ul li{position:relative;float:left;width:100%}.mean-container .mean-nav ul li a{display:block;float:left;width:90%;padding:1em 5%;margin:0;text-align:left;color:#677294;border-top:1px solid #dbeefd;text-decoration:none}.mean-container .mean-nav ul li a.active{color:#000}.mean-container .mean-nav ul li li a{width:80%;padding:1em 10%;color:#677294;border-top:1px solid #dbeefd;opacity:1;filter:alpha(opacity=75);text-shadow:none!important;visibility:visible;text-transform:none;font-size:14px}.mean-container .mean-nav ul li.mean-last a{border-bottom:none;margin-bottom:0}.mean-container .mean-nav ul li li li a{width:70%;padding:1em 15%}.mean-container .mean-nav ul li li li li a{width:60%;padding:1em 20%}.mean-container .mean-nav ul li li li li li a{width:50%;padding:1em 25%}.mean-container .mean-nav ul li a:hover{background:#252525;background:rgba(255,255,255,.1)}.mean-container .mean-nav ul li a.mean-expand{margin-top:3px;width:100%;height:24px;padding:12px!important;text-align:right;position:absolute;right:0;top:0;z-index:2;font-weight:700;background:0 0;border:none!important}.mean-container .mean-push{float:left;width:100%;padding:0;margin:0;clear:both}.mean-nav .wrapper{width:100%;padding:0;margin:0}.mean-container .mean-bar,.mean-container .mean-bar *{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box}.mean-remove{display:none!important}.mobile-nav{display:none}.mobile-nav.mean-container .mean-nav ul li a.active{color:#ff2d55}.main-nav{background:#000;position:absolute;top:0;left:0;padding-top:15px;padding-bottom:15px;width:100%;z-index:999;height:auto}.mean-nav .dropdown-toggle::after{display:none}.navbar-light .navbar-brand,.navbar-light .navbar-brand:hover{color:#fff;font-weight:700;text-transform:uppercase}.main-nav nav ul{padding:0;margin:0;list-style-type:none}.main-nav nav .navbar-nav .nav-item{position:relative;padding:15px 0}.main-nav nav .navbar-nav .nav-item a{font-weight:500;font-size:16px;text-transform:uppercase;color:#fff;padding-left:0;padding-right:0;padding-top:0;padding-bottom:0;margin-left:15px;margin-right:15px}.main-nav nav .navbar-nav .nav-item a:hover,.main-nav nav .navbar-nav .nav-item a:focus,.main-nav nav .navbar-nav .nav-item a.active{color:#ff2d55}.main-nav nav .navbar-nav .nav-item:hover a{color:#ff2d55}.main-nav nav .navbar-nav .nav-item .dropdown-menu{-webkit-box-shadow:0 0 30px 0 rgba(0,0,0,.05);box-shadow:0 0 30px 0 rgba(0,0,0,.05);background:#0d1028;position:absolute;top:80px;left:0;width:250px;z-index:99;display:block;padding-top:20px;padding-left:5px;padding-right:5px;padding-bottom:20px;opacity:0;visibility:hidden;-webkit-transition:all .3s ease-in-out;transition:all .3s ease-in-out}.main-nav nav .navbar-nav .nav-item .dropdown-menu li{position:relative;padding:0}.main-nav nav .navbar-nav .nav-item .dropdown-menu li a{font-size:15px;font-weight:500;text-transform:capitalize;padding:9px 15px;margin:0;display:block;color:#fff}.main-nav nav .navbar-nav .nav-item .dropdown-menu li a:hover,.main-nav nav .navbar-nav .nav-item .dropdown-menu li a:focus,.main-nav nav .navbar-nav .nav-item .dropdown-menu li a.active{color:#ff2d55}.main-nav nav .navbar-nav .nav-item .dropdown-menu li .dropdown-menu{position:absolute;left:-100%;top:0;opacity:0!important;visibility:hidden!important}.main-nav nav .navbar-nav .nav-item .dropdown-menu li:hover .dropdown-menu{opacity:1!important;visibility:visible!important;top:-20px!important}.main-nav nav .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li .dropdown-menu{position:absolute;left:-100%;top:0;opacity:0!important;visibility:hidden!important}.main-nav nav .navbar-nav .nav-item .dropdown-menu li:hover .dropdown-menu li:hover .dropdown-menu{opacity:1!important;visibility:visible!important;top:-20px!important}.main-nav nav .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li a{color:#fff;text-transform:capitalize}.main-nav nav .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li a:hover,.main-nav nav .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li a:focus,.main-nav nav .navbar-nav .nav-item .dropdown-menu li .dropdown-menu li a.active{color:#ff2d55}.main-nav nav .navbar-nav .nav-item .dropdown-menu li:hover a{color:#ff2d55}.main-nav nav .navbar-nav .nav-item:hover ul{opacity:1;visibility:visible;top:100%}.main-nav nav .navbar-nav .nav-item:last-child .dropdown-menu{left:auto;right:0}@media only screen and (max-width:991px){.mobile-nav{display:block;position:relative}.mobile-nav .logo{text-decoration:none;position:absolute;top:11px;z-index:999;left:15px;color:#fff;font-weight:700;text-transform:uppercase;font-size:20px}.mean-container .mean-bar{background-color:#000;padding:0}.mean-container a.meanmenu-reveal{padding:15px 15px 0 0}.mobile-nav nav .navbar-nav .nav-item a i{display:none}.main-nav{display:none!important}} \ No newline at end of file diff --git a/themes/30coffe/assets/css/owl.carousel.min.css b/themes/30coffe/assets/css/owl.carousel.min.css new file mode 100644 index 00000000..107223dd --- /dev/null +++ b/themes/30coffe/assets/css/owl.carousel.min.css @@ -0,0 +1,6 @@ +/** + * Owl Carousel v2.3.4 + * Copyright 2013-2018 David Deutsch + * Licensed under: SEE LICENSE IN https://github.com/OwlCarousel2/OwlCarousel2/blob/master/LICENSE + */ +.owl-carousel,.owl-carousel .owl-item{-webkit-tap-highlight-color:transparent;position:relative}.owl-carousel{display:none;width:100%;z-index:1}.owl-carousel .owl-stage{position:relative;-ms-touch-action:pan-Y;touch-action:manipulation;-moz-backface-visibility:hidden}.owl-carousel .owl-stage:after{content:".";display:block;clear:both;visibility:hidden;line-height:0;height:0}.owl-carousel .owl-stage-outer{position:relative;overflow:hidden;-webkit-transform:translate3d(0,0,0)}.owl-carousel .owl-item,.owl-carousel .owl-wrapper{-webkit-backface-visibility:hidden;-moz-backface-visibility:hidden;-ms-backface-visibility:hidden;-webkit-transform:translate3d(0,0,0);-moz-transform:translate3d(0,0,0);-ms-transform:translate3d(0,0,0)}.owl-carousel .owl-item{min-height:1px;float:left;-webkit-backface-visibility:hidden;-webkit-touch-callout:none}.owl-carousel .owl-item img{display:block;width:100%}.owl-carousel .owl-dots.disabled,.owl-carousel .owl-nav.disabled{display:none}.no-js .owl-carousel,.owl-carousel.owl-loaded{display:block}.owl-carousel .owl-dot,.owl-carousel .owl-nav .owl-next,.owl-carousel .owl-nav .owl-prev{cursor:pointer;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.owl-carousel .owl-nav button.owl-next,.owl-carousel .owl-nav button.owl-prev,.owl-carousel button.owl-dot{background:0 0;color:inherit;border:none;padding:0!important;font:inherit}.owl-carousel.owl-loading{opacity:0;display:block}.owl-carousel.owl-hidden{opacity:0}.owl-carousel.owl-refresh .owl-item{visibility:hidden}.owl-carousel.owl-drag .owl-item{-ms-touch-action:pan-y;touch-action:pan-y;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.owl-carousel.owl-grab{cursor:move;cursor:grab}.owl-carousel.owl-rtl{direction:rtl}.owl-carousel.owl-rtl .owl-item{float:right}.owl-carousel .animated{animation-duration:1s;animation-fill-mode:both}.owl-carousel .owl-animated-in{z-index:0}.owl-carousel .owl-animated-out{z-index:1}.owl-carousel .fadeOut{animation-name:fadeOut}@keyframes fadeOut{0%{opacity:1}100%{opacity:0}}.owl-height{transition:height .5s ease-in-out}.owl-carousel .owl-item .owl-lazy{opacity:0;transition:opacity .4s ease}.owl-carousel .owl-item .owl-lazy:not([src]),.owl-carousel .owl-item .owl-lazy[src^=""]{max-height:0}.owl-carousel .owl-item img.owl-lazy{transform-style:preserve-3d}.owl-carousel .owl-video-wrapper{position:relative;height:100%;background:#000}.owl-carousel .owl-video-play-icon{position:absolute;height:80px;width:80px;left:50%;top:50%;margin-left:-40px;margin-top:-40px;background:url(owl.video.play.html) no-repeat;cursor:pointer;z-index:1;-webkit-backface-visibility:hidden;transition:transform .1s ease}.owl-carousel .owl-video-play-icon:hover{-ms-transform:scale(1.3,1.3);transform:scale(1.3,1.3)}.owl-carousel .owl-video-playing .owl-video-play-icon,.owl-carousel .owl-video-playing .owl-video-tn{display:none}.owl-carousel .owl-video-tn{opacity:0;height:100%;background-position:center center;background-repeat:no-repeat;background-size:contain;transition:opacity .4s ease}.owl-carousel .owl-video-frame{position:relative;z-index:1;height:100%;width:100%} \ No newline at end of file diff --git a/themes/30coffe/assets/css/owl.theme.default.min.css b/themes/30coffe/assets/css/owl.theme.default.min.css new file mode 100644 index 00000000..487088d2 --- /dev/null +++ b/themes/30coffe/assets/css/owl.theme.default.min.css @@ -0,0 +1,6 @@ +/** + * Owl Carousel v2.3.4 + * Copyright 2013-2018 David Deutsch + * Licensed under: SEE LICENSE IN https://github.com/OwlCarousel2/OwlCarousel2/blob/master/LICENSE + */ +.owl-theme .owl-dots,.owl-theme .owl-nav{text-align:center;-webkit-tap-highlight-color:transparent}.owl-theme .owl-nav{margin-top:10px}.owl-theme .owl-nav [class*=owl-]{color:#FFF;font-size:14px;margin:5px;padding:4px 7px;background:#D6D6D6;display:inline-block;cursor:pointer;border-radius:3px}.owl-theme .owl-nav [class*=owl-]:hover{background:#869791;color:#FFF;text-decoration:none}.owl-theme .owl-nav .disabled{opacity:.5;cursor:default}.owl-theme .owl-nav.disabled+.owl-dots{margin-top:10px}.owl-theme .owl-dots .owl-dot{display:inline-block;zoom:1}.owl-theme .owl-dots .owl-dot span{width:10px;height:10px;margin:5px 7px;background:#D6D6D6;display:block;-webkit-backface-visibility:visible;transition:opacity .2s ease;border-radius:30px}.owl-theme .owl-dots .owl-dot.active span,.owl-theme .owl-dots .owl-dot:hover span{background:#869791} \ No newline at end of file diff --git a/themes/30coffe/assets/css/owl.video.play.html b/themes/30coffe/assets/css/owl.video.play.html new file mode 100644 index 00000000..94da9ab8 --- /dev/null +++ b/themes/30coffe/assets/css/owl.video.play.html @@ -0,0 +1,7 @@ + +404 Not Found + +

    404 Not Found

    +
    nginx/1.18.0
    + + diff --git a/themes/30coffe/assets/css/responsive.css b/themes/30coffe/assets/css/responsive.css new file mode 100644 index 00000000..af49d4f8 --- /dev/null +++ b/themes/30coffe/assets/css/responsive.css @@ -0,0 +1 @@ +@media only screen and (max-width:767px){body{font-size:14px}.ptb-100{padding-top:50px;padding-bottom:50px}.pt-100{padding-top:50px}.pb-70{padding-bottom:20px}.pb-100{padding-bottom:50px}.navbar-area .side-nav{position:absolute;top:0;right:75px}.navbar-area .side-nav .nav-cart{border:1px solid #fff;color:#fff;width:35px;height:35px;line-height:42px;font-size:20px;top:10px}.navbar-area .side-nav .nav-cart span{color:#0b0320;background-color:#fff}.navbar-area .side-nav .nav-tel{display:none}.navbar-area .side-nav .modal-btn{display:none}.main-nav nav .navbar-nav .nav-item a{font-size:14px}.mean-container .mean-bar{background-color:#0b0320}.mobile-nav.mean-container .mean-nav ul li a.active{color:#784400}.mean-container .mean-nav .navbar-nav{height:350px;overflow-y:scroll}.mean-container a.meanmenu-reveal span{position:relative;top:8px;margin-top:-6px}.banner-area{height:100%;padding-top:125px;padding-bottom:100px}.banner-area .banner-shape img:nth-child(1){display:none}.banner-area .banner-shape img:nth-child(2){display:none}.banner-area .banner-shape img:nth-child(4){display:none}.banner-area .banner-content{text-align:center}.banner-area .banner-content h1{font-size:35px}.banner-area .banner-content form .form-control{font-size:14px;padding-left:20px;height:60px}.banner-area .banner-content form .banner-form-btn{padding:12px 14px;position:absolute;top:7px;right:7px;font-size:14px}.banner-area .banner-slider{display:none}.section-title{margin-bottom:35px;margin-top:-4px}.section-title .sub-title{font-size:14px;margin-bottom:4px}.section-title h2{font-size:25px;margin-bottom:12px}.feature-area .section-title{text-align:center}.feature-area .section-title p{margin-left:auto}.feature-item .feature-inner{bottom:15px;max-width:265px}.feature-item .feature-inner ul li img{top:-1px}.feature-item .feature-inner ul li span{font-size:16px;margin-left:8px}.feature-item .feature-inner ul li a{width:35px;height:35px;line-height:40px;font-size:21px}.service-area .service-item{padding:32px 15px 30px}.service-area .service-slider .owl-prev{display:none!important}.service-area .service-slider .owl-next{display:none!important}.restant-area{padding-top:95px;padding-bottom:50px}.restant-area .restant-shape img{max-width:75px}.restant-area .restant-content{padding-left:0;text-align:center;max-width:100%}.restant-area .restant-content .section-title{margin-bottom:25px;text-align:center}.restant-area .restant-content .section-title p{margin-left:auto}.restant-area .restant-img{margin-bottom:75px}.restant-area .restant-img img:nth-child(1){max-width:285px}.restant-area .restant-img img:nth-child(2){top:-45px;max-width:185px}.restant-area .restant-img img:nth-child(3){display:none}.restant-area .restant-img img:nth-child(4){max-width:190px;bottom:-85px}.restant-area .restant-img img:nth-child(5){display:none}.cmn-btn{font-size:14px}.collection-area .more-collection a{font-size:15px}.collection-item .collection-bottom{padding:15px 15px 17px}.collection-item .collection-bottom h3{font-size:18px;margin-bottom:12px}.collection-item .collection-bottom ul{vertical-align:middle}.collection-item .collection-bottom ul li span{font-size:22px}.collection-item .collection-bottom ul li .minus,.collection-item .collection-bottom ul li .plus{width:30px;height:30px;line-height:31px;font-size:20px}.collection-item .collection-bottom ul li .form-control{width:40px}.sorting-menu ul{margin-bottom:30px}.sorting-menu ul li{font-size:14px;padding:7px 20px;margin-left:2px;margin-right:2px}.menu-item{padding:35px 25px 32px;border:1px solid #784400}.menu-item img{margin-bottom:22px}.reservation-area{border-radius:0 0 20px 0}.reservation-area .reservation-item{padding-top:50px;padding-bottom:30px}.reservation-area .reservation-item .section-title{text-align:center;margin-bottom:35px}.reservation-area .reservation-item .section-title p{margin-left:auto}.reservation-area .reservation-item ul{padding:20px 10px 10px;border-radius:5px}.reservation-area .reservation-item ul li{margin-bottom:10px;display:block}.reservation-area .reservation-item ul li .form-control{width:100%}.reservation-area .reservation-item ul li:first-child{padding-right:0;margin-right:0}.reservation-area .reservation-item ul li:first-child:before{display:none}.reservation-area .reservation-img{position:relative;top:0;padding-bottom:40px}.chef-area{padding-top:50px}.chef-item .chef-bottom ul li:nth-child(1){opacity:1;z-index:1;bottom:0}.chef-item .chef-bottom ul li:nth-child(2){opacity:1;z-index:1;bottom:0}.chef-item .chef-bottom ul li:nth-child(3){bottom:-10px;opacity:1;z-index:1;bottom:0}.review-area .slick-prev i{left:110px}.review-area .slick-next i{right:110px}.review-area .review-img img:nth-child(2){top:12px}.review-area .review-item{padding-top:30px;padding-bottom:33px}.review-area .review-item .slider-nav{margin-bottom:25px}.review-area .review-item .slider-for p{padding-left:5px;padding-right:5px}.blog-item .blog-bottom{padding:28px 8px 45px 15px}.blog-item .blog-bottom h3{font-size:20px}.blog-area .read-blog-btn{font-size:15px}.subscribe-area{border-radius:0 0 20px 0}.subscribe-item{padding-top:50px;padding-bottom:30px}.subscribe-item .section-title{text-align:center}.subscribe-item .section-title p{margin-left:auto}.subscribe-item .newsletter-form .form-control{height:55px;padding:5px 15px;font-size:15px}.subscribe-item .newsletter-form .cmn-btn{right:8px;top:6px;padding:10px 25px}.subscribe-item .social-link ul{text-align:center}.subscribe-img{padding-bottom:40px}.footer-item .footer-service h3{margin-bottom:20px}.copyright-area{padding-top:20px;padding-bottom:20px;border-radius:20px 20px 0 0}#myModalRight{display:none}.banner-area-two .banner-shape img:nth-child(3){left:0}.banner-area-two .banner-content{padding-top:115px;padding-bottom:70px;text-align:center}.banner-area-two .banner-content h1{font-size:30px}.banner-area-two .banner-content p{margin-bottom:30px;margin-left:auto;margin-right:auto}.banner-area-two .banner-content .banner-btn-wrap .cmn-btn{margin-right:10px}.banner-area-two .banner-content .banner-btn-wrap .banner-btn-two{padding:11px 20px}.banner-area-two .banner-img{position:relative;bottom:0;right:0;left:0;text-align:center}.banner-area-two .banner-img img{max-width:100%}.food-img-area{margin-top:50px}.about-area .about-shape{display:none}.about-area .about-img{position:relative;text-align:center}.about-area .about-img img:nth-child(1){display:none}.about-area .about-img img:nth-child(2){max-width:100%;right:0}.about-area .about-content{margin-top:31px;padding-left:0;text-align:center;margin-left:auto;margin-right:auto}.about-area .about-content .section-title{text-align:center;margin-bottom:25px}.about-area .about-content .section-title h2{margin-bottom:20px}.about-area .about-content .section-title p{margin-left:auto}.download-area .download-content .section-title{text-align:center}.download-area .download-content ul li{padding:10px 15px;font-size:17px}.download-area .download-content ul li:hover{margin-left:0}.download-area .download-content ul li span{width:45px;height:45px;line-height:47px;font-size:20px;margin-right:10px}.download-area .download-content .app-wrap{margin-top:30px}.download-area .download-content .app-wrap a{margin-right:10px;max-width:130px}.join-area{margin-top:50px}.join-area .join-img img{position:relative;top:-60px;max-width:100%}.join-area .join-content{padding-top:0;padding-bottom:50px;margin-top:-30px;text-align:center}.join-area .join-content .section-title{text-align:center}.join-area .join-content .section-title p{margin-left:auto;margin-right:auto}.chef-area-two{padding-top:50px}.review-area-two{padding-top:50px}.review-area-two .review-shape img{max-width:100px}.review-area-two .review-img{text-align:center}.review-area-two .review-img img{border-radius:42px}.review-area-two .review-item{padding-top:30px;padding-bottom:94px}.review-area-two .slick-prev i{left:90px;bottom:-120px}.review-area-two .slick-next i{right:90px;bottom:-120px}.footer-area-two{border-radius:20px 20px 0 0}.copyright-area-two{border-radius:0}.banner-area-three .banner-shape img:nth-child(1){display:none}.banner-area-three .banner-shape img:nth-child(2){top:74px;max-width:62px}.banner-area-three .banner-shape img:nth-child(3){bottom:0;right:0;max-width:90px}.banner-area-three .banner-content{padding-top:115px;padding-bottom:40px;text-align:center}.banner-area-three .banner-content h1{font-size:28px}.banner-area-three .banner-content p{margin-bottom:30px;margin-left:auto;margin-right:auto}.banner-area-three .banner-content .banner-btn-wrap .cmn-btn{margin-right:10px}.banner-area-three .banner-content .banner-btn-wrap .banner-btn-two{padding:11px 20px}.banner-area-three .banner-img{position:relative;bottom:0;right:0;text-align:center;padding-bottom:80px}.banner-area-three .banner-img img{max-width:100%}.about-area-two .about-shape img{bottom:0;right:0;max-width:130px}.about-area-two .about-img{margin-bottom:75px;text-align:center}.about-area-two .about-img img:nth-child(1){max-width:100%;top:0}.about-area-two .about-img img:nth-child(2){display:none}.about-area-two .about-img .video-wrap{bottom:-45px;right:0;left:0}.about-area-two .about-img .video-wrap a{margin-left:auto;margin-right:auto}.about-area-two .about-content{padding-left:0;text-align:center}.about-area-two .about-content .section-title{text-align:center;margin-bottom:30px}.service-area-three .service-item .accordion a{font-size:14px}.service-area-three .service-item .accordion a span{font-size:14px}.service-area-three .service-img img{max-width:100%}.join-area-two{margin-top:60px}.page-title-area{padding-top:150px;padding-bottom:110px}.page-title-item{padding-left:30px}.page-title-item:before{width:5px;height:77px}.page-title-item h2{font-size:30px;margin-bottom:6px}.story-area .story-shape img{max-width:75px}.story-area .story-head{margin-bottom:35px}.story-area .story-head h2{font-size:25px;margin-bottom:15px;margin-top:-6px}.story-area .story-item h3{font-size:16px;max-width:245px;padding:12px;bottom:20px}.download-area-two{border-radius:0 0 20px 0}.download-area-two .download-item{text-align:center}.download-area-two .download-item h2{font-size:25px;margin-bottom:35px}.download-area-two .download-item ul li{margin-right:15px}.download-area-two .download-item ul li a{max-width:130px}.download-area-two .download-img{margin-bottom:30px;text-align:center}.download-area-two .download-img img{position:relative;top:0;right:0;left:0;max-width:100%}.page-title-area-two:before{display:none}.page-title-area-two .page-title-plate{display:none}.service-details-area .service-details-item .service-details-more h3{margin-bottom:25px}.service-details-area .service-details-item .service-details-more ul li a{font-size:14px}.service-details-area .service-details-item .service-details-fresh{text-align:center}.service-details-area .service-details-item .service-details-fresh h2{font-size:25px}.page-title-img-two:before{-webkit-clip-path:polygon(0 0,79% 0%,55% 100%,0% 100%);clip-path:polygon(0 0,79% 0%,55% 100%,0% 100%)}.blog-details-more{padding-bottom:20px}.blog-details-nav ul li a{font-size:14px}.cart-wrap{text-align:center}.cart-wrap .table .thead tr .table-head{font-size:13px;padding-right:4px;padding-left:4px}.cart-wrap .table tr td{font-size:14px}.cart-wrap .shop-back{margin-bottom:15px}.cart-wrap .shop-back a{margin-top:15px}.cart-wrap .total-shopping h2{font-size:25px;margin-bottom:15px}.cart-wrap .total-shopping h3{font-size:18px}.cart-wrap .total-shopping a{font-size:14px;padding:14px 25px}.checkout-item h2{font-size:25px;padding-top:10px;padding-bottom:14px;margin-bottom:30px}.checkout-item .checkout-one{max-width:100%;padding:0 10px}.checkout-item .checkout-one label{font-size:14px;margin-right:7px}.checkout-item .checkout-one .form-group .form-control{width:64%}.checkout-item .checkout-two{max-width:100%;padding:0 10px 30px}.checkout-item .checkout-two h3{margin-bottom:12px;font-size:20px}.checkout-item .checkout-two p{font-size:14px}.checkout-item .checkout-two .form-check span{font-size:14px}.checkout-item .checkout-two .form-check .form-check-input{margin-top:3px}.coming-item{height:100vh}.coming-item h1{font-size:28px;margin-bottom:12px}.coming-item .coming-wrap{margin-bottom:20px}.coming-item .coming-wrap .coming-inner h3{font-size:35px}.coming-item .coming-wrap .coming-inner p{font-size:14px}.faq-area .faq-head h2{margin-bottom:30px;font-size:25px}.faq-area .accordion p{font-size:14px;padding:12px 12px 12px 18px}.faq-area .accordion a{font-size:15px}.privacy-area{padding-bottom:0}.privacy-item{margin-bottom:50px}.privacy-item h2{font-size:22px}.error-item{height:480px}.error-item h1{font-size:80px;margin-bottom:0}.error-item p{font-size:20px;padding-left:5px;padding-right:5px}.error-item a{padding:13px 25px;margin-top:30px;font-size:14px}.contact-location-area .location-item{padding:40px 15px;border:1px solid #784400}.contact-form-area .contact-item{padding:30px 15px}.contact-form-area .contact-item #contactForm{margin-bottom:20px}.contact-form-area .contact-item #contactForm .form-group .form-control{height:50px;padding-left:25px;font-size:14px}.contact-form-area .contact-item .text-danger{font-size:16px}.contact-form-area .contact-item .text-success{font-size:16px}.contact-form-area .contact-img img{position:relative;left:0;right:0;top:50px}.book-table-area .book-table-wrap{padding:30px 10px 35px}}@media only screen and (min-width:576px) and (max-width:767px){.service-area .service-item h3{font-size:18px}}@media only screen and (min-width:768px) and (max-width:991px){body{font-size:14px}.ptb-100{padding-top:70px;padding-bottom:70px}.pt-100{padding-top:70px}.pb-70{padding-bottom:40px}.pb-100{padding-bottom:70px}.navbar-area .side-nav{position:absolute;top:0;right:75px}.navbar-area .side-nav .nav-cart{border:1px solid #fff;color:#fff;width:35px;height:35px;line-height:42px;font-size:20px;top:10px}.navbar-area .side-nav .nav-cart span{color:#0b0320;background-color:#fff}.navbar-area .side-nav .nav-tel{display:none}.navbar-area .side-nav .modal-btn{display:none}.main-nav nav .navbar-nav .nav-item a{font-size:14px}.mean-container .mean-bar{background-color:#0b0320}.mobile-nav.mean-container .mean-nav ul li a.active{color:#784400}.mean-container .mean-nav .navbar-nav{height:350px;overflow-y:scroll}.mean-container a.meanmenu-reveal span{position:relative;top:8px;margin-top:-6px}.banner-area{height:100%;padding-top:110px;padding-bottom:70px}.banner-area .banner-shape img:nth-child(1){display:none}.banner-area .banner-shape img:nth-child(2){display:none}.banner-area .banner-shape img:nth-child(4){display:none}.banner-area .banner-content{text-align:center}.banner-area .banner-content h1{font-size:35px}.banner-area .banner-content form .form-control{font-size:14px;padding-left:20px;height:60px}.banner-area .banner-content form .banner-form-btn{padding:12px 14px;position:absolute;top:7px;right:7px;font-size:14px}.banner-area .banner-slider{display:none}.section-title{margin-bottom:40px;margin-top:-2px}.section-title .sub-title{font-size:14px;margin-bottom:4px}.section-title h2{font-size:30px;margin-bottom:12px}.feature-area .section-title{text-align:center}.feature-area .section-title p{margin-left:auto}.feature-item .feature-inner{bottom:15px;max-width:265px}.feature-item .feature-inner ul li img{top:-1px}.feature-item .feature-inner ul li span{font-size:16px;margin-left:8px}.feature-item .feature-inner ul li a{width:35px;height:35px;line-height:40px;font-size:21px}.service-area .service-slider .owl-prev{display:none!important}.service-area .service-slider .owl-next{display:none!important}.restant-area{padding-top:115px;padding-bottom:70px}.restant-area .restant-shape img{max-width:100px}.restant-area .restant-content{padding-left:0;text-align:center;margin-left:auto;margin-right:auto}.restant-area .restant-content .section-title{margin-bottom:25px;text-align:center}.restant-area .restant-content .section-title p{margin-left:auto}.restant-area .restant-img{margin-bottom:75px}.restant-area .restant-img img:nth-child(1){max-width:100%}.restant-area .restant-img img:nth-child(2){top:-45px;max-width:210px}.restant-area .restant-img img:nth-child(3){top:260px;right:-36px;max-width:195px}.restant-area .restant-img img:nth-child(4){max-width:220px;bottom:-85px}.restant-area .restant-img img:nth-child(5){top:245px;max-width:230px}.cmn-btn{font-size:14px}.collection-area .more-collection a{font-size:15px}.collection-item .collection-bottom{padding:15px 15px 17px}.collection-item .collection-bottom h3{font-size:18px;margin-bottom:12px}.collection-item .collection-bottom ul{vertical-align:middle}.collection-item .collection-bottom ul li span{font-size:22px}.collection-item .collection-bottom ul li .minus,.collection-item .collection-bottom ul li .plus{width:30px;height:30px;line-height:31px;font-size:20px}.collection-item .collection-bottom ul li .form-control{width:40px}.sorting-menu ul{margin-bottom:30px}.sorting-menu ul li{font-size:14px;padding:7px 20px;margin-left:2px;margin-right:2px}.reservation-area{border-radius:0 0 20px 0}.reservation-area .reservation-item{padding-top:70px;padding-bottom:30px}.reservation-area .reservation-item .section-title{text-align:center;margin-bottom:35px}.reservation-area .reservation-item .section-title p{margin-left:auto}.reservation-area .reservation-img{position:relative;top:0;padding-bottom:60px}.chef-area{padding-top:70px}.chef-item .chef-bottom ul li:nth-child(1){opacity:1;z-index:1;bottom:0}.chef-item .chef-bottom ul li:nth-child(2){opacity:1;z-index:1;bottom:0}.chef-item .chef-bottom ul li:nth-child(3){bottom:-10px;opacity:1;z-index:1;bottom:0}.review-area .slick-prev i{left:230px}.review-area .slick-next i{right:230px}.review-area .review-img img:nth-child(2){top:12px;right:25px}.review-area .review-item{padding-top:30px;padding-bottom:45px;margin-left:auto;margin-right:auto}.review-area .review-item .slider-nav{margin-bottom:25px}.review-area .review-item .slider-for p{padding-left:5px;padding-right:5px}.subscribe-area{border-radius:0 0 20px 0}.subscribe-item{padding-top:70px;padding-bottom:30px}.subscribe-item .section-title{text-align:center}.subscribe-item .section-title p{margin-left:auto}.subscribe-item .newsletter-form .form-control{height:55px;padding:5px 15px 5px 30px;font-size:15px}.subscribe-item .newsletter-form .cmn-btn{right:8px;top:6px;padding:10px 25px}.subscribe-item .social-link ul{text-align:center}.subscribe-img{padding-bottom:60px}.footer-item .footer-service h3{margin-bottom:20px}.copyright-area{padding-top:20px;padding-bottom:20px;border-radius:20px 20px 0 0}#myModalRight{display:none}.banner-area-two .banner-shape img:nth-child(3){left:0}.banner-area-two .banner-content{padding-top:115px;padding-bottom:70px;text-align:center}.banner-area-two .banner-content h1{font-size:42px}.banner-area-two .banner-content p{margin-bottom:30px;font-size:16px;margin-left:auto;margin-right:auto}.banner-area-two .banner-img{position:relative;bottom:0;right:0;left:0;text-align:center}.banner-area-two .banner-img img{max-width:100%}.food-img-area{margin-top:70px}.about-area .about-shape{display:none}.about-area .about-img img:nth-child(2){max-width:100%}.about-area .about-content{margin-top:31px;padding-left:0;text-align:center;margin-left:auto;margin-right:auto}.about-area .about-content .section-title{text-align:center;margin-bottom:25px}.about-area .about-content .section-title h2{margin-bottom:20px}.about-area .about-content .section-title p{margin-left:auto}.download-area .download-content .section-title{text-align:center}.download-area .download-content ul li{padding:10px 15px;font-size:17px;margin-left:auto;margin-right:auto}.download-area .download-content ul li:hover{margin-left:0}.download-area .download-content ul li span{width:45px;height:45px;line-height:47px;font-size:20px;margin-right:10px}.download-area .download-content .app-wrap{margin-top:30px;text-align:center}.download-area .download-content .app-wrap a{margin-right:10px;max-width:130px}.download-area .download-img{text-align:center}.join-area{margin-top:60px}.join-area .join-img img{position:relative;top:-60px;max-width:100%}.join-area .join-content{padding-top:0;padding-bottom:50px;margin-top:-30px;text-align:center}.join-area .join-content .section-title{text-align:center}.join-area .join-content .section-title p{margin-left:auto;margin-right:auto}.chef-area-two{padding-top:70px}.review-area-two{padding-top:70px}.review-area-two .review-shape img{max-width:100px}.review-area-two .review-img{text-align:center}.review-area-two .review-img img{border-radius:42px}.review-area-two .review-item{padding-top:30px;padding-bottom:135px}.review-area-two .slick-prev i{left:215px;bottom:-120px}.review-area-two .slick-next i{right:215px;bottom:-120px}.footer-area-two{border-radius:20px 20px 0 0}.copyright-area-two{border-radius:0}.banner-area-three .banner-shape img:nth-child(1){display:none}.banner-area-three .banner-shape img:nth-child(2){top:74px;max-width:62px}.banner-area-three .banner-shape img:nth-child(3){bottom:0;right:0;max-width:90px}.banner-area-three .banner-content{padding-top:115px;padding-bottom:40px;text-align:center}.banner-area-three .banner-content h1{font-size:38px}.banner-area-three .banner-content p{margin-bottom:30px;margin-left:auto;margin-right:auto}.banner-area-three .banner-content .banner-btn-wrap .cmn-btn{margin-right:10px}.banner-area-three .banner-content .banner-btn-wrap .banner-btn-two{padding:11px 20px}.banner-area-three .banner-img{position:relative;bottom:0;right:0;text-align:center;padding-bottom:80px}.banner-area-three .banner-img img{max-width:100%}.about-area-two .about-shape img{bottom:0;right:0;max-width:130px}.about-area-two .about-img{margin-bottom:75px;text-align:center}.about-area-two .about-img img:nth-child(1){max-width:100%;top:0}.about-area-two .about-img img:nth-child(2){display:none}.about-area-two .about-img .video-wrap{bottom:-45px;right:0;left:0}.about-area-two .about-img .video-wrap a{margin-left:auto;margin-right:auto}.about-area-two .about-content{padding-left:0;text-align:center}.about-area-two .about-content .section-title{text-align:center;margin-bottom:30px}.service-area-three .service-item .accordion a{font-size:14px}.service-area-three .service-item .accordion a span{font-size:14px}.service-area-three .service-img img{max-width:100%}.join-area-two{margin-top:60px}.page-title-area{padding-top:150px;padding-bottom:110px}.page-title-item{padding-left:30px}.page-title-item:before{width:5px;height:77px}.page-title-item h2{font-size:30px;margin-bottom:6px}.story-area .story-shape img{max-width:75px}.story-area .story-head{margin-bottom:35px}.story-area .story-head h2{font-size:30px;margin-bottom:15px;margin-top:-8px}.story-area .story-item h3{font-size:16px;max-width:245px;padding:12px;bottom:20px}.download-area-two{border-radius:0 0 20px 0}.download-area-two .download-item{text-align:center}.download-area-two .download-item h2{font-size:25px;margin-bottom:35px}.download-area-two .download-item ul li{margin-right:15px}.download-area-two .download-item ul li a{max-width:130px}.download-area-two .download-img{margin-bottom:30px;text-align:center}.download-area-two .download-img img{position:relative;top:0;right:0;left:0;max-width:100%}.page-title-area-two:before{display:none}.page-title-area-two .page-title-plate{display:none}.service-details-area .service-details-item .service-details-more h3{margin-bottom:25px}.service-details-area .service-details-item .service-details-more ul li a{font-size:14px}.service-details-area .service-details-item .service-details-fresh{text-align:center}.service-details-area .service-details-item .service-details-fresh h2{font-size:25px}.blog-details-more{padding-bottom:20px}.blog-details-nav ul li a{font-size:14px}.checkout-item h2{font-size:30px}.checkout-item .checkout-one{max-width:100%;padding-left:15px;padding-right:15px}.checkout-item .checkout-two{max-width:100%;padding-left:15px;padding-right:15px}.coming-item{height:100vh}.coming-item h1{font-size:35px;margin-bottom:12px}.coming-item .coming-wrap{margin-bottom:20px}.coming-item .coming-wrap .coming-inner h3{font-size:35px}.coming-item .coming-wrap .coming-inner p{font-size:14px}.faq-area .faq-head h2{margin-bottom:30px;font-size:30px}.faq-area .accordion p{font-size:14px;padding:12px 12px 12px 18px}.faq-area .accordion a{font-size:15px}.privacy-area{padding-bottom:20px}.privacy-item{margin-bottom:50px}.privacy-item h2{font-size:22px}.error-item{height:480px}.error-item h1{font-size:80px;margin-bottom:0}.error-item p{font-size:20px;padding-left:5px;padding-right:5px}.error-item a{padding:13px 25px;margin-top:30px;font-size:14px}.contact-location-area .location-item{padding:40px 15px;border:1px solid #784400}.contact-form-area .contact-item{padding:30px 15px}.contact-form-area .contact-item #contactForm{margin-bottom:20px}.contact-form-area .contact-item #contactForm .form-group .form-control{height:50px;padding-left:25px;font-size:14px}.contact-form-area .contact-item .text-danger{font-size:18px}.contact-form-area .contact-item .text-success{font-size:18px}.contact-form-area .contact-img img{position:relative;left:0;right:0;top:70px}.book-table-area .book-table-wrap{padding:40px 30px 45px}.reservation-area .reservation-item ul{max-width:540px;margin-left:auto;margin-right:auto}}@media only screen and (min-width:992px) and (max-width:1199px){.main-nav nav .navbar-nav .nav-item a{font-size:14px;margin-left:5px;margin-right:5px}.main-nav nav .side-nav .nav-tel{margin-left:10px}.feature-item .feature-inner{max-width:265px}.service-area .service-slider .owl-prev{display:none}.service-area .service-slider .owl-next{display:none}.collection-item .collection-bottom{padding:15px 10px 17px}.collection-item .collection-bottom h3{font-size:16px}.reservation-area .reservation-item ul li:last-child .cmn-btn{margin-top:10px;margin-left:0}.review-area .slick-prev i{left:185px}.review-area .slick-next i{right:185px}.banner-area-two .banner-img img{max-width:350px}.join-area .join-img img{top:-95px}.banner-area-three .banner-img img{max-width:340px}.service-area-three .service-item .accordion a{font-size:14px}.checkout-item .checkout-one{padding-left:10px;padding-right:10px}.checkout-item .checkout-two{padding-left:10px;padding-right:10px}.checkout-item .checkout-one .form-group .form-control{width:75%}.banner-area .banner-content h1{font-size:50px}.banner-area .banner-shape img:nth-child(4){display:none}.reservation-area .reservation-item ul{padding:20px 11px 20px 20px}.restant-area .restant-img img:nth-child(1){max-width:495px}.about-area-two .about-img img:nth-child(1){max-width:385px}}@media only screen and (min-width:1400px){.about-area-two .about-img img:nth-child(1){max-width:555px}.about-area-two .about-img .video-wrap{bottom:15px}}@media only screen and (min-width:1800px){.banner-area-three .banner-img img{max-width:100%}.about-area-two .about-img .video-wrap{bottom:12px}.about-area-two .about-img img:nth-child(1){max-width:560px}}@media only screen and (min-width:1800px){.restant-area .restant-img{margin-right:0}}@media only screen and (max-width:991px){.mean-container .mean-nav ul li a.mean-expand{margin-top:0}} \ No newline at end of file diff --git a/themes/30coffe/assets/css/slick-theme.min.css b/themes/30coffe/assets/css/slick-theme.min.css new file mode 100644 index 00000000..51ff5ed6 --- /dev/null +++ b/themes/30coffe/assets/css/slick-theme.min.css @@ -0,0 +1,2 @@ +@charset 'UTF-8';.slick-loading .slick-list{background:#fff url(../fonts/ajax-loader.gif) center center no-repeat}@font-face{font-family:slick;font-weight:400;font-style:normal;src:url(../fonts/slick.eot);src:url(../fonts/slickd41d.eot?#iefix) format('embedded-opentype'),url(../fonts/slick.woff) format('woff'),url(../fonts/slick.ttf) format('truetype'),url(../fonts/slick.svg#slick) format('svg')}.slick-next,.slick-prev{font-size:0;line-height:0;position:absolute;top:50%;display:block;width:20px;height:20px;padding:0;-webkit-transform:translate(0,-50%);-ms-transform:translate(0,-50%);transform:translate(0,-50%);cursor:pointer;color:transparent;border:none;outline:0;background:0 0}.slick-next:focus,.slick-next:hover,.slick-prev:focus,.slick-prev:hover{color:transparent;outline:0;background:0 0}.slick-next:focus:before,.slick-next:hover:before,.slick-prev:focus:before,.slick-prev:hover:before{opacity:1}.slick-next.slick-disabled:before,.slick-prev.slick-disabled:before{opacity:.25}.slick-next:before,.slick-prev:before{font-family:slick;font-size:20px;line-height:1;opacity:.75;color:#fff;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.slick-prev{left:-25px}[dir=rtl] .slick-prev{right:-25px;left:auto}.slick-prev:before{content:'←'}[dir=rtl] .slick-prev:before{content:'→'}.slick-next{right:-25px}[dir=rtl] .slick-next{right:auto;left:-25px}.slick-next:before{content:'→'}[dir=rtl] .slick-next:before{content:'←'}.slick-dotted.slick-slider{margin-bottom:30px}.slick-dots{position:absolute;bottom:-25px;display:block;width:100%;padding:0;margin:0;list-style:none;text-align:center}.slick-dots li{position:relative;display:inline-block;width:20px;height:20px;margin:0 5px;padding:0;cursor:pointer}.slick-dots li button{font-size:0;line-height:0;display:block;width:20px;height:20px;padding:5px;cursor:pointer;color:transparent;border:0;outline:0;background:0 0}.slick-dots li button:focus,.slick-dots li button:hover{outline:0}.slick-dots li button:focus:before,.slick-dots li button:hover:before{opacity:1}.slick-dots li button:before{font-family:slick;font-size:6px;line-height:20px;position:absolute;top:0;left:0;width:20px;height:20px;content:'•';text-align:center;opacity:.25;color:#000;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.slick-dots li.slick-active button:before{opacity:.75;color:#000} +/*# sourceMappingURL=slick-theme.min.css.map */ \ No newline at end of file diff --git a/themes/30coffe/assets/css/slick.min.css b/themes/30coffe/assets/css/slick.min.css new file mode 100644 index 00000000..41691514 --- /dev/null +++ b/themes/30coffe/assets/css/slick.min.css @@ -0,0 +1,2 @@ +.slick-slider{position:relative;display:block;box-sizing:border-box;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;-webkit-touch-callout:none;-khtml-user-select:none;-ms-touch-action:pan-y;touch-action:pan-y;-webkit-tap-highlight-color:transparent}.slick-list{position:relative;display:block;overflow:hidden;margin:0;padding:0}.slick-list:focus{outline:0}.slick-list.dragging{cursor:pointer;cursor:hand}.slick-slider .slick-list,.slick-slider .slick-track{-webkit-transform:translate3d(0,0,0);-moz-transform:translate3d(0,0,0);-ms-transform:translate3d(0,0,0);-o-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}.slick-track{position:relative;top:0;left:0;display:block;margin-left:auto;margin-right:auto}.slick-track:after,.slick-track:before{display:table;content:''}.slick-track:after{clear:both}.slick-loading .slick-track{visibility:hidden}.slick-slide{display:none;float:left;height:100%;min-height:1px}[dir=rtl] .slick-slide{float:right}.slick-slide img{display:block}.slick-slide.slick-loading img{display:none}.slick-slide.dragging img{pointer-events:none}.slick-initialized .slick-slide{display:block}.slick-loading .slick-slide{visibility:hidden}.slick-vertical .slick-slide{display:block;height:auto;border:1px solid transparent}.slick-arrow.slick-hidden{display:none} +/*# sourceMappingURL=slick.min.css.map */ \ No newline at end of file diff --git a/themes/30coffe/assets/css/style.css b/themes/30coffe/assets/css/style.css new file mode 100644 index 00000000..8a850335 --- /dev/null +++ b/themes/30coffe/assets/css/style.css @@ -0,0 +1 @@ +@import "https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&display=swap";@import "https://fonts.googleapis.com/css2?family=Playfair+Display:wght@500;600;700&display=swap";body{font-family:poppins,sans-serif;color:#2e2e30;background-color:#fff;font-size:15px}h1,h2,h3,h4,h5,h6{line-height:1.3;font-family:playfair display,serif;color:#0b0320}p{line-height:1.7;color:#2e2e30}a{-webkit-transition:.5s all ease;transition:.5s all ease;text-decoration:none;color:#784400}a:hover{color:#0b0320}img{max-width:100%}.d-table{width:100%;height:100%}.d-table-cell{padding-top: 120px;padding-bottom: 0px;}.ptb-100{padding-top:100px;padding-bottom:100px}.pt-100{padding-top:100px}.pb-70{padding-bottom:70px}.pb-100{padding-bottom:100px}button:focus{outline:0}.btn.focus,.btn:focus{-webkit-box-shadow:none;box-shadow:none}.navbar-light .navbar-brand .logo-two{display:none}.navbar-area .side-nav{position:relative;top:4px}.navbar-area .side-nav .nav-cart{width:42px;height:42px;line-height:48px;border-radius:50%;border:1px solid #0b0320;color:#0b0320;display:inline-block;text-align:center;font-size:22px;position:relative}.navbar-area .side-nav .nav-cart:hover{color:#fff;background-color:#0b0320}.navbar-area .side-nav .nav-cart span{display:inline-block;color:#fff;background-color:#814100;border-radius:50%;width:18px;height:18px;line-height:18px;font-size:12px;position:absolute;top:-3px;right:-5px;font-weight:500}.navbar-area .side-nav .nav-tel{color:#f5e9b3;border-radius:50px;padding:12px 15px 11px;background-color:#784400;font-size:14px;position:relative;top:-6px;margin-left:15px;display:inline-block}.navbar-area .side-nav .nav-tel i{display:inline-block;margin-right:5px;font-size:18px;position:relative;top:3px}.navbar-area .side-nav .nav-tel:hover{color:#fff;background-color:#0b0320}.main-nav{background:0 0;-webkit-transition:.5s all ease;transition:.5s all ease}.main-nav nav .navbar-nav{margin-left:auto;margin-right:auto}.main-nav nav .navbar-nav .nav-link:focus,.main-nav nav .navbar-nav .nav-link:hover{color:#784400}.main-nav nav .navbar-nav .nav-item:hover a{color:#784400}.main-nav nav .navbar-nav .nav-item a{font-weight:400;font-size:15px;color:#0b0320;font-family:poppins,sans-serif;text-transform:unset}.main-nav nav .navbar-nav .nav-item a:hover,.main-nav nav .navbar-nav .nav-item a:focus,.main-nav nav .navbar-nav .nav-item a.active{color:#784400}.main-nav nav .navbar-nav .nav-item a:hover i{-webkit-transform:rotate(0deg);transform:rotate(0deg);color:#784400}.main-nav nav .navbar-nav .nav-item a i{display:inline-block;font-size:18px;position:relative;top:2px;-webkit-transform:rotate(-90deg);transform:rotate(-90deg);-webkit-transition:.5s all ease;transition:.5s all ease}.main-nav nav .navbar-nav .nav-item .dropdown-menu{background:#fff;padding:0;border:0;border-radius:0}.main-nav nav .navbar-nav .nav-item .dropdown-menu li{border-bottom:1px solid #0b032026;position:relative;-webkit-transition:.5s all ease;transition:.5s all ease}.main-nav nav .navbar-nav .nav-item .dropdown-menu li:hover{padding-left:10px}.main-nav nav .navbar-nav .nav-item .dropdown-menu li:hover:before{opacity:1}.main-nav nav .navbar-nav .nav-item .dropdown-menu li:hover a{color:#784400;text-transform:unset}.main-nav nav .navbar-nav .nav-item .dropdown-menu li:before{position:absolute;content:'';width:15px;height:1px;left:0;top:22px;background-color:#784400;opacity:0;-webkit-transition:.5s all ease;transition:.5s all ease}.main-nav nav .navbar-nav .nav-item .dropdown-menu li:last-child{border-bottom:0}.main-nav nav .navbar-nav .nav-item .dropdown-menu li a{color:#0b0320;padding:12px 15px}.main-nav nav .navbar-nav .nav-item .dropdown-menu li a:hover,.main-nav nav .navbar-nav .nav-item .dropdown-menu li a:focus,.main-nav nav .navbar-nav .nav-item .dropdown-menu li a.active{color:#784400}.main-nav .dropdown-toggle::after{display:none}.menu-shrink{background-color:#fff;padding-top:5px;padding-bottom:5px;-webkit-box-shadow:0 0 20px 0 #dddddd8c;box-shadow:0 0 20px 0 #dddddd8c}.banner-area{position:relative}.banner-area .banner-shape img{position:absolute;z-index:-1}.banner-area .banner-shape img:nth-child(1){bottom:125px;left:165px}.banner-area .banner-shape img:nth-child(2){top:130px;left:170px;margin-left:auto;margin-right:auto}.banner-area .banner-shape img:nth-child(3){bottom:30px;left:0;right:20px;margin-left:auto;margin-right:auto}.banner-area .banner-shape img:nth-child(4){top:275px;right:0;max-width:140px}.banner-area .banner-content h1{font-weight:700;font-size:60px;margin-bottom:20px}.banner-area .banner-content p{margin-bottom:30px}.banner-area .banner-content form{position:relative}.banner-area .banner-content form ::-webkit-input-placeholder{color:#a5a4a9}.banner-area .banner-content form :-ms-input-placeholder{color:#a5a4a9}.banner-area .banner-content form ::-ms-input-placeholder{color:#a5a4a9}.banner-area .banner-content form ::placeholder{color:#a5a4a9}.banner-area .banner-content form .form-control{font-size:16px;padding-left:25px;border:0;border-radius:50px;-webkit-box-shadow:0 0 20px 0 #dddddd5c;box-shadow:0 0 20px 0 #dddddd5c;height:70px;padding-top:10px;padding-bottom:10px}.banner-area .banner-content form .form-control:focus{-webkit-box-shadow:0 0 20px 0 #dddddd5c;box-shadow:0 0 20px 0 #dddddd5c;border:0}.banner-area .banner-content form .banner-form-btn{font-weight:500;font-size:15px;color:#f7e9ac;border-radius:50px;background-color:#784400;padding:12px 32px;position:absolute;top:10px;right:12px;-webkit-transition:.5s all ease;transition:.5s all ease}.banner-area .banner-content form .banner-form-btn:hover{color:#fff;background-color:#0b0320}.banner-area .owl-theme .owl-nav{margin-top:-5px;position:relative;right:0;left:0}.banner-area .banner-slider .owl-prev{font-size:55px!important;color:#784400!important;-webkit-transition:.5s all ease;transition:.5s all ease}.banner-area .banner-slider .owl-prev:hover{color:#0b0320!important;background-color:transparent!important}.banner-area .banner-slider .owl-next{font-size:55px!important;color:#784400!important;-webkit-transition:.5s all ease;transition:.5s all ease}.banner-area .banner-slider .owl-next:hover{color:#0b0320!important;background-color:transparent!important}.section-title{margin-bottom:50px;text-align:center;margin-top:-8px}.section-title .sub-title{font-weight:500;font-size:16px;color:#784400;display:block;margin-bottom:8px}.section-title h2{font-weight:700;font-size:35px;margin-bottom:16px}.section-title p{max-width:580px;margin-bottom:0;margin-left:auto;margin-right:auto}.feature-area .section-title{text-align:left}.feature-area .section-title p{margin-left:0}.feature-item{position:relative;margin-bottom:30px;-webkit-transition:.5s all ease;transition:.5s all ease}.feature-item img{width:100%;border-radius:25px}.feature-item:hover{-webkit-transform:translate(0,-10px);transform:translate(0,-10px)}.feature-item:hover .feature-inner ul li a{background-color:#0b0320}.feature-item .feature-inner{position:absolute;left:0;right:0;bottom:30px;background-color:#fff;max-width:310px;border-radius:50px;margin-left:auto;margin-right:auto;padding-left:25px;padding-top:12px;padding-bottom:12px}.feature-item .feature-inner ul{margin:0;padding:0;position:relative}.feature-item .feature-inner ul li{list-style-type:none;display:inline-block}.feature-item .feature-inner ul li img{position:relative;top:-2px}.feature-item .feature-inner ul li span{display:block;font-weight:500;font-size:18px;color:#0b0320;margin-left:12px}.feature-item .feature-inner ul li a{display:block;width:40px;height:40px;line-height:46px;border-radius:50%;text-align:center;font-size:23px;color:#fff;background-color:#784400;position:absolute;right:7px;top:-7px}.service-area{background-color:#fffdf9}.service-area .service-item{text-align:center;padding:32px 20px 30px;border-radius:25px;-webkit-transition:.5s all ease;transition:.5s all ease;position:relative}.service-area .service-item a{display:block}.service-area .service-item:hover{background-color:#784400}.service-area .service-item:hover .service-shape{opacity:1}.service-area .service-item:hover h3{color:#fff}.service-area .service-item:hover p{color:#fff}.service-area .service-item img{margin-bottom:20px;width:80px!important;height:80px;margin-left:auto;margin-right:auto;display:block}.service-area .service-item .service-shape{position:absolute;top:0;left:0;-webkit-transition:.5s all ease;transition:.5s all ease;opacity:0;width:100%!important;height:100%!important;margin-bottom:0}.service-area .service-item h3{font-weight:700;font-size:22px;margin-bottom:15px;-webkit-transition:.5s all ease;transition:.5s all ease;color:#0b0320}.service-area .service-item p{margin-bottom:0;-webkit-transition:.5s all ease;transition:.5s all ease;color:#2e2e30}.service-area .service-slider{margin-bottom:-9px}.service-area .service-slider .center{background-color:#784400;border-radius:25px}.service-area .service-slider .center .service-shape{opacity:1}.service-area .service-slider .center h3{color:#fff}.service-area .service-slider .center p{color:#fff}.service-area .service-slider .owl-prev{height:40px;width:40px;line-height:45px!important;border-radius:50%!important;text-align:center;color:#fff!important;background-color:#ffe7a2!important;font-size:25px!important;-webkit-transition:.5s all ease;transition:.5s all ease;position:absolute;top:45%;left:-55px;opacity:0}.service-area .service-slider .owl-prev:hover{background-color:#784400!important}.service-area .service-slider .owl-next{height:40px;width:40px;line-height:45px!important;border-radius:50%!important;text-align:center;color:#fff!important;background-color:#ffe7a2!important;font-size:25px!important;-webkit-transition:.5s all ease;transition:.5s all ease;position:absolute;top:45%;right:-55px;opacity:0}.service-area .service-slider .owl-next:hover{background-color:#784400!important}.service-area .service-slider:hover .owl-prev{opacity:1;left:-48px}.service-area .service-slider:hover .owl-next{opacity:1;right:-48px}.restant-area{ padding-top:150px;position:relative;padding-bottom:130px}.restant-area .restant-shape img{position:absolute;right:0;bottom:0;max-width:150px}.restant-area .restant-content{max-width:600px;padding-left:20px}.restant-area .restant-content .section-title{text-align:left;margin-bottom:35px}.restant-area .restant-content .section-title p{max-width:100%;margin-left:0;margin-bottom:10px}.restant-area .restant-img{text-align:center;position:relative;max-width:620px;margin-left:auto;margin-right:auto}.restant-area .restant-img img:nth-child(1){position:relative;max-width:520px;margin-left:auto;margin-right:auto}.restant-area .restant-img img:nth-child(2){position:absolute;left:0;right:0;top:-50px;margin-left:auto;margin-right:auto;max-width:280px;-webkit-animation:restant-amination 10s infinite linear;animation:restant-amination 10s infinite linear}.restant-area .restant-img img:nth-child(3){position:absolute;top:175px;right:-50px;max-width:260px;-webkit-animation:restant-amination 10s infinite linear;animation:restant-amination 10s infinite linear}.restant-area .restant-img img:nth-child(4){position:absolute;left:0;right:0;max-width:260px;bottom:-100px;margin-left:auto;margin-right:auto;-webkit-animation:restant-amination 10s infinite linear;animation:restant-amination 10s infinite linear}.restant-area .restant-img img:nth-child(5){position:absolute;top:160px;left:-65px;max-width:275px;-webkit-animation:restant-amination 10s infinite linear;animation:restant-amination 10s infinite linear}.cmn-btn{font-weight:500;font-size:15px;color:#f7e9ac;background-color:#784400;padding:12px 30px;border-radius:50px;display:inline-block}.cmn-btn:hover{background-color:#0b0320;color:#fff}@-webkit-keyframes restant-amination{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}40%{-webkit-transform:rotate(10deg);transform:rotate(10deg)}70%{-webkit-transform:rotate(-15deg);transform:rotate(-15deg)}100%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}}@keyframes restant-amination{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}40%{-webkit-transform:rotate(10deg);transform:rotate(10deg)}70%{-webkit-transform:rotate(-15deg);transform:rotate(-15deg)}100%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}}.collection-area .more-collection{text-align:center}.collection-area .more-collection a{display:inline-block;font-weight:500;color:#0b0320;font-size:18px;border-bottom:1px solid #0b0320}.collection-area .more-collection a:hover{color:#784400;border-bottom:1px solid #784400}.collection-item{margin-bottom:30px;-webkit-box-shadow:0 0 20px 0 #dddddd82;box-shadow:0 0 20px 0 #dddddd82;border-radius:10px 10px 0 0;overflow:hidden}.collection-item:hover .collection-top ul{opacity:1;bottom:0}.collection-item:hover .collection-top .add-cart{right:10px}.collection-item:hover .collection-top .add-cart a{opacity:1}.collection-item .collection-top{position:relative}.collection-item .collection-top img{width:100%;border-radius:10px 10px 0 0}.collection-item .collection-top ul{margin:0;padding:0;background-color:#fff;position:absolute;bottom:-10px;left:0;right:0;opacity:0;padding:4px 15px;border-radius:8px 8px 0 0;max-width:128px;text-align:center;margin-left:auto;margin-right:auto;-webkit-box-shadow:0 6px 15px 0 #dddddd40;box-shadow:0 6px 15px 0 #dddddd40;-webkit-transition:.5s all ease;transition:.5s all ease}.collection-item .collection-top ul li{list-style-type:none;display:inline-block}.collection-item .collection-top ul li i{color:#ddd;display:block}.collection-item .collection-top ul li .checked{color:#ffc107}.collection-item .collection-top .add-cart{display:inline-block;position:absolute;top:10px;right:-15px}.collection-item .collection-top .add-cart a{display:block;color:#0b0320;background-color:#fff;border-radius:5px;padding:4px 12px 8px;font-size:13px;opacity:0}.collection-item .collection-top .add-cart a i{display:inline-block;color:#784400;font-size:22px;position:relative;top:4px;margin-right:3px}.collection-item .collection-top .add-cart a:hover{color:#784400;background-color:#f7e9ac}.collection-item .collection-bottom{padding:15px 20px 17px}.collection-item .collection-bottom h3{font-weight:500;font-size:20px;margin-bottom:15px;font-family:poppins,sans-serif}.collection-item .collection-bottom ul{margin:0;padding:0;display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap}.collection-item .collection-bottom ul li{list-style-type:none;display:inline-block}.collection-item .collection-bottom ul li:first-child{-webkit-box-flex:0;-ms-flex:0 0 20%;flex:0 0 20%;max-width:20%}.collection-item .collection-bottom ul li:last-child{text-align:right;-webkit-box-flex:0;-ms-flex:0 0 80%;flex:0 0 80%;max-width:80%}.collection-item .collection-bottom ul li span{display:block;font-weight:500;font-size:20px;color:#fe3333;position:relative;top:4px}.collection-item .collection-bottom ul li .minus,.collection-item .collection-bottom ul li .plus{width:35px;height:35px;line-height:35px;color:#fff;background-color:#e8c940;display:inline-block;text-align:center;cursor:pointer;margin-bottom:0;vertical-align:middle;-webkit-transition:.5s all ease;transition:.5s all ease;border-radius:10px;top:0}.collection-item .collection-bottom ul li .minus:hover,.collection-item .collection-bottom ul li .plus:hover{background-color:#784400}.collection-item .collection-bottom ul li .form-control{height:25px;width:50px;text-align:center;font-size:20px;font-weight:500;border:0;color:#784400;display:inline-block;vertical-align:middle;margin-left:-4px;margin-right:-4px}.collection-item .collection-bottom ul li .form-control:focus{-webkit-box-shadow:none;box-shadow:none;border:0}.sorting-menu ul{margin:0;padding:0;text-align:center;margin-bottom:40px}.sorting-menu ul li{list-style-type:none;display:inline-block;font-size:15px;color:#0b0320;cursor:pointer;border-radius:50px;padding:10px 20px;margin-left:5px;margin-right:5px;-webkit-transition:.5s all ease;transition:.5s all ease;font-weight:500;background-color:#f5f5f5;margin-bottom:7px}.sorting-menu ul li:hover,.sorting-menu ul li.mixitup-control-active{color:#fff;background-color:#784400}.menu-area{background-color:#fffdf9}.menu-item{text-align:center;margin-bottom:30px;background-color:#fff;padding:45px 30px 42px;border-radius:18px;position:relative;-webkit-transition:.5s all ease;transition:.5s all ease;border:1px solid #784400}.menu-item:hover,.menu-item.active{background-color:#784400;-webkit-transform:translate(0,-10px);transform:translate(0,-10px)}.menu-item:hover .menu-shape,.menu-item.active .menu-shape{opacity:1}.menu-item:hover h3,.menu-item.active h3{color:#fff}.menu-item img{margin-bottom:22px}.menu-item .menu-shape{position:absolute;left:0;top:0;opacity:0;margin-bottom:0;-webkit-transition:.5s all ease;transition:.5s all ease;width:100%;height:100%}.menu-item h3{font-size:22px;font-weight:700;margin-bottom:0;-webkit-transition:.5s all ease;transition:.5s all ease}.reservation-area{background-color:#0b0320;border-radius:0 0 85px 0;position:relative}.reservation-area .reservation-shape img{position:absolute;top:0;left:0}.reservation-area .row{position:relative}.reservation-area .reservation-item{padding-top:100px;padding-bottom:100px;position:relative}.reservation-area .reservation-item .section-title{text-align:left;margin-bottom:40px}.reservation-area .reservation-item .section-title h2{color:#fff}.reservation-area .reservation-item .section-title p{margin-left:0;color:#fff}.reservation-area .reservation-item ul{margin:0;padding:10px 11px 10px 20px;background-color:#fff;border-radius:50px;text-align:center}.reservation-area .reservation-item ul li{list-style-type:none;display:inline-block}.reservation-area .reservation-item ul li .form-control{width:165px;font-size:13px;border:1px solid #0b0320;padding:12px}.reservation-area .reservation-item ul li .form-control:focus{-webkit-box-shadow:none;box-shadow:none;border:1px solid #0b0320}.reservation-area .reservation-item ul li:first-child{padding-right:10px;position:relative;margin-right:5px}.reservation-area .reservation-item ul li:first-child:before{position:absolute;content:'';width:1px;height:45px;right:0;top:0;background-color:#784400}.reservation-area .reservation-item ul li:last-child .cmn-btn{padding:12px 22px;margin-left:7px}.reservation-area .reservation-img{text-align:center;margin-right:auto;margin-left:auto}.chef-item{margin-bottom:30px;text-align:center}.chef-item:hover .chef-bottom ul li:nth-child(1),.chef-item.active .chef-bottom ul li:nth-child(1){bottom:0;opacity:1;z-index:1}.chef-item:hover .chef-bottom ul li:nth-child(2),.chef-item.active .chef-bottom ul li:nth-child(2){bottom:0;opacity:1;z-index:1}.chef-item:hover .chef-bottom ul li:nth-child(3),.chef-item.active .chef-bottom ul li:nth-child(3){bottom:0;opacity:1;z-index:1}.chef-item .chef-top{position:relative;border-radius:10px 10px 0 0}.chef-item .chef-top img{width:100%;border-radius:10px 10px 0 0}.chef-item .chef-top .chef-inner{background-color:#0b0320bf;border-radius:10px 10px 0 0;padding:10px 10px 12px;position:absolute;left:0;right:0;bottom:0}.chef-item .chef-top .chef-inner h3{color:#fff;font-family:poppins,sans-serif;font-weight:500;font-size:20px;margin-bottom:5px}.chef-item .chef-top .chef-inner span{display:block;font-size:14px;color:#fff}.chef-item .chef-bottom ul{margin:0;padding:0;margin-top:10px}.chef-item .chef-bottom ul li{list-style-type:none;display:inline-block;margin-left:5px;margin-right:5px}.chef-item .chef-bottom ul li:nth-child(1){position:relative;bottom:-10px;opacity:0;z-index:-1;-webkit-transition:.5s all ease;transition:.5s all ease}.chef-item .chef-bottom ul li:nth-child(2){position:relative;bottom:-10px;opacity:0;z-index:-1;-webkit-transition:.7s all ease;transition:.7s all ease}.chef-item .chef-bottom ul li:nth-child(3){position:relative;bottom:-10px;opacity:0;z-index:-1;-webkit-transition:.9s all ease;transition:.9s all ease}.chef-item .chef-bottom ul li a{display:block;width:35px;height:35px;line-height:40px;border-radius:50%;text-align:center;color:#fff;background-color:#0b0320;font-size:18px}.chef-item .chef-bottom ul li a:hover{background-color:#784400}.review-area{position:relative}.review-area .slick-prev{left:0}.review-area .slick-prev:before{display:none}.review-area .slick-prev i{color:#784400;display:block;font-size:35px;position:absolute;bottom:-98px;left:240px;-webkit-transition:.5s all ease;transition:.5s all ease}.review-area .slick-prev i:hover{color:#0b0320}.review-area .slick-next{right:0}.review-area .slick-next:before{display:none}.review-area .slick-next i{color:#784400;display:block;font-size:35px;position:absolute;bottom:-98px;right:240px;-webkit-transition:.5s all ease;transition:.5s all ease}.review-area .slick-next i:hover{color:#0b0320}.review-area .review-img{position:relative}.review-area .review-img img:nth-child(1){position:relative}.review-area .review-img img:nth-child(2){position:absolute;top:60px;right:0}.review-area .review-item{text-align:center;max-width:560px;padding-top:100px;padding-bottom:100px}.review-area .review-item .section-title{margin-bottom:30px}.review-area .review-item .slider-nav{margin-bottom:40px}.review-area .review-item .slider-nav .slick-center img{-webkit-transform:scale(1.3);transform:scale(1.3);margin-top:12px;margin-bottom:12px}.review-area .review-item .slider-nav img{margin-left:auto;margin-right:auto;margin-top:12px;-webkit-transition:.5s all ease;transition:.5s all ease;max-width:75px}.review-area .review-item .slider-nav img:focus{border:0;-webkit-box-shadow:none;box-shadow:none}.review-area .review-item .slider-for h3{font-weight:500;font-size:20px;font-family:poppins,sans-serif;margin-bottom:15px}.review-area .review-item .slider-for p{margin-bottom:0;max-width:500px;margin-left:auto;margin-right:auto}.blog-item{margin-bottom:50px;-webkit-box-shadow:0 0 20px 0 #dddddd8c;box-shadow:0 0 20px 0 #dddddd8c;border-radius:65px 65px 0 0}.blog-item:hover .blog-bottom .cmn-btn{right:0}.blog-item .blog-top{position:relative}.blog-item .blog-top a{display:block}.blog-item .blog-top span{display:inline-block;font-size:14px;color:#0b0320;background-color:#f6e8ab;padding:5px 12px;border-radius:6px;position:absolute;right:15px;bottom:-15px}.blog-item .blog-top img{border-radius:12px 12px 0 0;width:100%}.blog-item .blog-bottom{padding:35px 15px 50px 30px;position:relative}.blog-item .blog-bottom h3{font-family:poppins,sans-serif;font-weight:500;font-size:22px;margin-bottom:10px;line-height:1.4}.blog-item .blog-bottom h3 a{display:block;color:#0b0320}.blog-item .blog-bottom h3 a:hover{color:#784400}.blog-item .blog-bottom p{color:#2e2e30;margin-bottom:0}.blog-item .blog-bottom .cmn-btn{position:absolute;right:15px;bottom:-20px}.blog-area .read-blog-btn{text-align:center;display:inline-block;font-weight:500;font-size:18px;color:#0b0320;border-bottom:1px solid #0b0320}.blog-area .read-blog-btn:hover{color:#784400;border-bottom:1px solid #784400}.subscribe-area{background-color:#0b0320;border-radius:0 0 75px 0;position:relative}.subscribe-area .subscribe-shape img{position:absolute;top:0;left:0}.subscribe-item{padding-top:100px;padding-bottom:100px}.subscribe-item .section-title{text-align:left;margin-bottom:35px}.subscribe-item .section-title h2{color:#fff}.subscribe-item .section-title p{color:#fff;margin-left:0}.subscribe-item .newsletter-form{position:relative;margin-bottom:30px}.subscribe-item .newsletter-form .form-control{height:70px;padding:10px 15px 10px 30px;border-radius:50px;background-color:#fff;border:0;font-size:16px}.subscribe-item .newsletter-form .form-control ::-webkit-input-placeholder{color:#a5a4a9}.subscribe-item .newsletter-form .form-control :-ms-input-placeholder{color:#a5a4a9}.subscribe-item .newsletter-form .form-control ::-ms-input-placeholder{color:#a5a4a9}.subscribe-item .newsletter-form .form-control ::placeholder{color:#a5a4a9}.subscribe-item .newsletter-form .form-control:focus{-webkit-box-shadow:none;box-shadow:none;border:0}.subscribe-item .newsletter-form .cmn-btn{opacity:1;position:absolute;right:10px;top:10px;-webkit-transition:.5s all ease;transition:.5s all ease;padding:12px 40px}.subscribe-item .newsletter-form .validation-danger{font-size:14px;color:#fff;margin-top:10px}.subscribe-item .social-link ul{margin:0;padding:0}.subscribe-item .social-link ul li{list-style-type:none;display:inline-block;margin-right:12px}.subscribe-item .social-link ul li a{display:block;width:40px;height:40px;line-height:46px;border-radius:50%;text-align:center;font-size:20px;color:#fff;background-color:#1a1c3b}.subscribe-item .social-link ul li a:hover{background-color:#784400}.subscribe-img{text-align:center}.footer-item{margin-bottom:30px}.footer-item .footer-logo a{display:block;margin-bottom:25px}.footer-item .footer-logo a .footer-logo2{display:none}.footer-item .footer-logo p{color:#0b0320;margin-bottom:25px}.footer-item .footer-logo ul{margin:0;padding:0}.footer-item .footer-logo ul li{list-style-type:none;display:inline-block;margin-right:5px}.footer-item .footer-logo ul li a{display:block;width:35px;height:35px;line-height:39px;border-radius:50%;text-align:center;font-size:18px;color:#fff;background-color:#1a1c3b;margin-bottom:0}.footer-item .footer-logo ul li a:hover{background-color:#784400}.footer-item .footer-service h3{font-weight:500;font-size:22px;font-family:poppins,sans-serif;margin-bottom:30px}.footer-item .footer-service ul{margin:0;padding:0}.footer-item .footer-service ul li{list-style-type:none;display:block;margin-bottom:15px;color:#00011e;position:relative;padding-left:28px}.footer-item .footer-service ul li i{display:inline-block;font-size:20px;margin-right:3px;position:absolute;top:3px;left:0}.footer-item .footer-service ul li a{display:block;color:#00011e}.footer-item .footer-service ul li a:hover{margin-left:10px;color:#784400}.footer-item .footer-service ul li:last-child{margin-bottom:0}.copyright-area{background-color:#0b0320;padding-top:30px;padding-bottom:30px;border-radius:35px 35px 0 0}.copyright-area .copyright-item{text-align:center}.copyright-area .copyright-item p{color:#fff;font-size:15px;margin-bottom:0}.copyright-area .copyright-item p a{display:inline-block;color:#f7e9ac;font-weight:500}.copyright-area .copyright-item p a:hover{color:#fff}.main-nav-two .nav-two-logo-one{display:block}.main-nav-two .nav-two-logo-two{display:none}.main-nav-two nav .navbar-nav .nav-item a{color:#fff}.main-nav-two nav .navbar-nav .nav-item .dropdown-menu li a{color:#0b0320}.main-nav-two nav .side-nav .nav-cart{border:1px solid #fff;color:#fff}.main-nav-two nav .side-nav .nav-cart:hover{color:#fff;background-color:#0b0320;border:1px solid #0b0320}.main-nav-two nav .side-nav .nav-cart span{color:#0b0320;background-color:#fff}.main-nav-two nav .side-nav .modal-btn{width:42px;height:42px;line-height:48px;background-color:#784400;border-radius:50%;padding:0;font-size:28px;margin-left:15px;position:relative;top:-7px;-webkit-transition:.5s all ease;transition:.5s all ease}.main-nav-two nav .side-nav .modal-btn:hover{color:#fff;background-color:#0b0320}.main-nav-two.menu-shrink .nav-two-logo-one{display:none}.main-nav-two.menu-shrink .nav-two-logo-two{display:block}.main-nav-two.menu-shrink nav .navbar-nav .nav-item:hover a{color:#0b0320}.main-nav-two.menu-shrink nav .navbar-nav .nav-item a{color:#0b0320}.main-nav-two.menu-shrink nav .navbar-nav .nav-item a:focus,.main-nav-two.menu-shrink nav .navbar-nav .nav-item a:hover,.main-nav-two.menu-shrink nav .navbar-nav .nav-item a.active{color:#784400}.main-nav-two.menu-shrink nav .side-nav .nav-cart{border:1px solid #784400;color:#0b0320;background-color:#784400}.main-nav-two.menu-shrink nav .side-nav .nav-cart:hover{color:#fff;background-color:#0b0320;border:1px solid #0b0320}.main-nav-two.menu-shrink nav .side-nav .nav-cart span{color:#784400;background-color:#0b0320}#myModalRight{z-index:99999}#myModalRight .modal-content .modal-header img{max-width:120px}#myModalRight .modal-content .modal-header .modal-header-logo2{display:none}#myModalRight .modal-content .modal-header .close{position:relative;top:3px}#myModalRight .modal-content .modal-body{padding:40px 30px 50px}#myModalRight .modal-content .modal-body h2{font-size:20px;font-weight:600;margin-bottom:12px;color:#0b0320}#myModalRight .modal-content .modal-body p{color:#2e2e30;font-size:14px;margin-bottom:20px}#myModalRight .modal-content .modal-body .image-area{margin-bottom:10px}#myModalRight .modal-content .modal-body .image-area .col-lg-4{padding-right:5px;margin-right:-5px}#myModalRight .modal-content .modal-body .image-area a{display:block;margin-bottom:15px}#myModalRight .modal-content .modal-body .modal-item{margin-bottom:30px}#myModalRight .modal-content .modal-body .social-area{text-align:center}#myModalRight .modal-content .modal-body .social-area h3{font-size:20px;margin-bottom:12px;font-weight:600;color:#0b0320}#myModalRight .modal-content .modal-body .social-area ul{margin:0;padding:0}#myModalRight .modal-content .modal-body .social-area ul li{list-style-type:none;display:inline-block}#myModalRight .modal-content .modal-body .social-area ul li a{display:block;color:#0b0320;border:1px solid #0b0320;width:32px;height:32px;line-height:34px;border-radius:50%;margin-right:3px;margin-left:3px;font-size:16px;text-align:center}#myModalRight .modal-content .modal-body .social-area ul li a:hover{color:#fff;background-color:#0b0320}#myModalRight .btn-close:focus{-webkit-box-shadow:none;box-shadow:none;border:0}.modal.modal-right .modal-dialog{max-width:380px;min-height:100vh}.modal.modal-right.show .modal-dialog{-webkit-transform:translate(0,0);transform:translate(0,0)}.modal.modal-right .modal-content{height:100vh;overflow-y:auto;border-radius:0}.modal.modal-left .modal-dialog{-webkit-transform:translate(-100%,0);transform:translate(-100%,0);margin:0 auto 0 0}.modal.modal-right .modal-dialog{-webkit-transform:translate(100%,0);transform:translate(100%,0);margin:0 0 0 auto}.banner-area-two{background-image:url(../img/home-two/banner/1.jpg);background-size:cover;background-position:center center;background-repeat:no-repeat;position:relative}.banner-area-two:before{position:absolute;content:'';width:100%;height:100%;left:0;top:0;background-color:#0b0320;opacity:.6}.banner-area-two .banner-shape img{position:absolute}.banner-area-two .banner-shape img:nth-child(1){bottom:60px;left:70px;max-width:120px}.banner-area-two .banner-shape img:nth-child(2){top:100px;left:100px;max-width:120px}.banner-area-two .banner-shape img:nth-child(3){top:245px;left:680px;max-width:120px}.banner-area-two .banner-content{position:relative;padding-top:230px;padding-bottom:200px}.banner-area-two .banner-content h1{color:#fff;font-weight:700;font-size:65px;margin-bottom:20px;max-width:700px}.banner-area-two .banner-content p{color:#fff;margin-bottom:35px;max-width:575px;margin-left:0}.banner-area-two .banner-content .banner-btn-wrap .cmn-btn{margin-right:18px}.banner-area-two .banner-content .banner-btn-wrap .banner-btn-two{font-weight:500;color:#fff;border:1px solid #fff;border-radius:50px;display:inline-block;padding:11px 30px}.banner-area-two .banner-content .banner-btn-wrap .banner-btn-two:hover{background-color:#fff;color:#0b0320}.banner-area-two .banner-img{position:absolute;bottom:0;right:80px}.banner-area-two .banner-img img{max-width:415px}.food-img-area{margin-top:-55px}.food-img-area .food-img-item{text-align:center;-webkit-transition:.5s all ease;transition:.5s all ease;margin-bottom:30px}.food-img-area .food-img-item:hover{-webkit-transform:scale(1.1);transform:scale(1.1)}.food-img-area .food-img-item img{max-width:100px;margin-left:auto;margin-right:auto;z-index:1;position:relative}.about-area{position:relative}.about-area .about-shape img{position:absolute}.about-area .about-shape img:nth-child(1){right:0;top:95px;max-width:780px;z-index:-1}.about-area .about-shape img:nth-child(2){right:98px;top:92px}.about-area .about-shape img:nth-child(3){right:255px;top:138px}.about-area .about-shape img:nth-child(4){right:450px;top:108px}.about-area .about-shape img:nth-child(5){right:650px;top:88px}.about-area .about-img{position:relative;text-align:right}.about-area .about-img img:nth-child(1){position:absolute;left:0;bottom:0}.about-area .about-img img:nth-child(2){position:relative;max-width:335px;right:50px}.about-area .about-content{max-width:620px;margin-top:100px;padding-left:40px}.about-area .about-content .section-title{text-align:left;margin-bottom:35px}.about-area .about-content .section-title h2{margin-bottom:25px}.about-area .about-content .section-title p{margin-left:0;margin-bottom:10px}.service-area-two{background-color:unset}.collection-area-two{background-color:#fffdf9}.download-area .download-content{margin-bottom:30px}.download-area .download-content .section-title{text-align:left}.download-area .download-content .section-title p{margin-left:0}.download-area .download-content ul{margin:0;padding:0}.download-area .download-content ul li{list-style-type:none;display:block;padding:20px;background-color:#fff;border-radius:10px;-webkit-box-shadow:0 0 20px 0 #dddddd52;box-shadow:0 0 20px 0 #dddddd52;margin-bottom:30px;max-width:390px;font-weight:500;font-size:20px;color:#0b0320;-webkit-transition:.5s all ease;transition:.5s all ease;border:1px solid transparent}.download-area .download-content ul li:hover{margin-left:15px;border:1px solid #784400}.download-area .download-content ul li:last-child{margin-bottom:0}.download-area .download-content ul li span{width:50px;height:50px;line-height:50px;border-radius:50%;text-align:center;background-color:#784400;color:#0b0320;display:inline-block;font-weight:600;font-size:25px;margin-right:15px;-webkit-box-shadow:0 0 20px 0 #dddddd52;box-shadow:0 0 20px 0 #dddddd52}.download-area .download-content .app-wrap{margin-top:50px}.download-area .download-content .app-wrap a{display:inline-block;margin-right:20px;max-width:180px}.download-area .download-content .app-wrap a:hover{-webkit-transform:scale(1.1);transform:scale(1.1)}.download-area .download-img{margin-bottom:30px;text-align:center}.download-area .download-img img{-webkit-animation:download-animation 3s infinite linear;animation:download-animation 3s infinite linear}@-webkit-keyframes download-animation{0%{-webkit-transform:translate(0,0);transform:translate(0,0)}50%{-webkit-transform:translate(0,-20px);transform:translate(0,-20px)}100%{-webkit-transform:translate(0,0);transform:translate(0,0)}}@keyframes download-animation{0%{-webkit-transform:translate(0,0);transform:translate(0,0)}50%{-webkit-transform:translate(0,-20px);transform:translate(0,-20px)}100%{-webkit-transform:translate(0,0);transform:translate(0,0)}}.join-area{background-color:#0b0320;margin-top:130px}.join-area .join-img{position:relative;text-align:center}.join-area .join-img img{position:absolute;left:0;top:-132px;max-width:490px;right:0;margin-left:auto;margin-right:auto}.join-area .join-content{padding-top:100px;padding-bottom:100px}.join-area .join-content .section-title{margin-bottom:30px;text-align:left}.join-area .join-content .section-title h2{color:#784400;margin-bottom:22px}.join-area .join-content .section-title p{color:#fff;margin-left:0}.join-area .join-content .cmn-btn{border:1px solid #784400}.join-area .join-content .cmn-btn:hover{border:1px solid #fff}.chef-area-two{padding-top:100px}.review-area-two{background-color:#0b0320;position:relative}.review-area-two .review-shape img{position:absolute;bottom:0;right:0;max-width:200px;-webkit-animation:review-animation 8s infinite linear;animation:review-animation 8s infinite linear}.review-area-two .review-img{text-align:center}.review-area-two .review-img img{border-radius:42px}.review-area-two .review-item{padding-top:100px;padding-bottom:150px}.review-area-two .review-item .section-title h2{color:#784400}.review-area-two .review-item .section-title p{color:#fff}.review-area-two .review-item .slider-for h3{color:#784400}.review-area-two .review-item .slider-for p{color:#fff}.review-area-two .slick-prev i{left:230px}.review-area-two .slick-prev i:hover{color:#fff}.review-area-two .slick-next i{right:230px}.review-area-two .slick-next i:hover{color:#fff}@-webkit-keyframes review-animation{0%{-webkit-transform:translate(0,0);transform:translate(0,0)}50%{-webkit-transform:translate(-50px,0);transform:translate(-50px,0)}10%{-webkit-transform:translate(0,0);transform:translate(0,0)}}@keyframes review-animation{0%{-webkit-transform:translate(0,0);transform:translate(0,0)}50%{-webkit-transform:translate(-50px,0);transform:translate(-50px,0)}10%{-webkit-transform:translate(0,0);transform:translate(0,0)}}.footer-area-two{background-color:#0b0320;border-radius:45px 45px 0 0;border-bottom:1px solid #8788988c}.footer-area-two .footer-item .footer-logo p{color:#fff}.footer-area-two .footer-item .footer-service h3{color:#fff}.footer-area-two .footer-item .footer-service ul li{color:#fff}.footer-area-two .footer-item .footer-service ul li a{color:#fff}.footer-area-two .footer-item .footer-service ul li a:hover{color:#784400}.copyright-area-two{border-radius:0}.banner-area-three{background-image:url(../img/home-three/banner-main.jpg);background-size:cover;background-position:center center;background-repeat:no-repeat;position:relative}.banner-area-three:before{position:absolute;content:'';width:100%;height:100%;left:0;top:0;background-color:#fff;opacity:.9}.banner-area-three .banner-shape img{position:absolute}.banner-area-three .banner-shape img:nth-child(1){bottom:35px;left:140px;max-width:120px}.banner-area-three .banner-shape img:nth-child(2){top:85px;left:45px;max-width:120px}.banner-area-three .banner-shape img:nth-child(3){bottom:0;right:20px;max-width:120px}.banner-area-three .banner-content{position:relative;padding-top:230px;padding-bottom:150px}.banner-area-three .banner-content h1{color:#444;font-weight:700;font-size:65px;margin-bottom:20px;max-width:700px}.banner-area-three .banner-content p{color:#444;margin-bottom:35px;max-width:575px;margin-left:0}.banner-area-three .banner-content .banner-btn-wrap .cmn-btn{margin-right:18px}.banner-area-three .banner-content .banner-btn-wrap .banner-btn-two{font-weight:500;color:#444;border:1px solid #444;border-radius:50px;display:inline-block;padding:11px 30px}.banner-area-three .banner-content .banner-btn-wrap .banner-btn-two:hover{background-color:#0b0320;border:1px solid #0b0320;color:#fff}.banner-area-three .banner-img{position:absolute;bottom:140px;right:40px}.banner-area-three .banner-img img{max-width:505px;border-radius:12px}.about-area-two{position:relative}.about-area-two .about-shape img{position:absolute;bottom:60px;right:65px;max-width:190px;-webkit-animation:review-animation 8s infinite linear;animation:review-animation 8s infinite linear}.about-area-two .about-img{margin-bottom:80px;position:relative;text-align:center}.about-area-two .about-img img:nth-child(1){border-radius:10px;max-width:470px;margin-left:auto;margin-right:auto;top:28px;position:relative}.about-area-two .about-img img:nth-child(2){position:absolute;left:0;top:0}.about-area-two .about-img .video-wrap{position:absolute;bottom:0;right:-35px}.about-area-two .about-img .video-wrap a{z-index:10;display:block;width:100px;height:100px;line-height:110px;border-radius:50%;position:relative;font-size:60px;text-align:center;color:#fff}.about-area-two .about-img .video-wrap a:before{content:"";position:absolute;z-index:0;left:50%;top:50%;-webkit-transform:translateX(-50%) translateY(-50%);transform:translateX(-50%) translateY(-50%);display:block;width:100px;height:100px;background-color:#0b0320;border-radius:50%;z-index:-1;-webkit-animation:pulse-border 1500ms ease-out infinite;animation:pulse-border 1500ms ease-out infinite}.about-area-two .about-img .video-wrap a:after{content:"";position:absolute;z-index:1;left:50%;top:50%;-webkit-transform:translateX(-50%) translateY(-50%);transform:translateX(-50%) translateY(-50%);display:block;width:100px;height:100px;background-color:#0b0320;border-radius:50%;z-index:-1;-webkit-transition:.5s all ease;transition:.5s all ease}.about-area-two .about-img .video-wrap a:hover{color:#fff}.about-area-two .about-img .video-wrap a:hover:before{background-color:#784400}.about-area-two .about-img .video-wrap a:hover:after{background-color:#784400}.about-area-two .about-content{margin-bottom:30px;padding-left:60px}.about-area-two .about-content .section-title{text-align:left;margin-bottom:32px}.about-area-two .about-content .section-title p{margin-bottom:10px}@-webkit-keyframes pulse-border{0%{-webkit-transform:translateX(-50%) translateY(-50%) translateZ(0) scale(1);transform:translateX(-50%) translateY(-50%) translateZ(0) scale(1);opacity:1}100%{-webkit-transform:translateX(-50%) translateY(-50%) translateZ(0) scale(1.5);transform:translateX(-50%) translateY(-50%) translateZ(0) scale(1.5);opacity:0}}@keyframes pulse-border{0%{-webkit-transform:translateX(-50%) translateY(-50%) translateZ(0) scale(1);transform:translateX(-50%) translateY(-50%) translateZ(0) scale(1);opacity:1}100%{-webkit-transform:translateX(-50%) translateY(-50%) translateZ(0) scale(1.5);transform:translateX(-50%) translateY(-50%) translateZ(0) scale(1.5);opacity:0}}.service-area-three{background-color:#0b0320}.service-area-three .service-item .section-title{text-align:left}.service-area-three .service-item .section-title h2{color:#fff}.service-area-three .service-item .section-title p{color:#fff;margin-left:0}.service-area-three .service-item .accordion{padding-left:0;margin-bottom:0}.service-area-three .service-item .accordion a{padding:13px 15px;border-radius:8px;background-color:#fff;width:100%;font-weight:500;display:block;cursor:pointer;font-size:15px;color:#fff;color:#0b0320}.service-area-three .service-item .accordion a:after{position:absolute;content:'+';width:15px;height:2px;right:12px;top:10px;font-size:20px;color:#0b0320;background-color:transparent}.service-area-three .service-item .accordion a span{display:inline-block;font-weight:600;color:#0b0320;font-size:15px;margin-right:10px}.service-area-three .service-item .accordion .active:after{content:'x';top:11px;font-size:17px}.service-area-three .service-item .accordion p{display:none;margin-bottom:0;color:#fff;font-size:15px;padding:15px 8px 5px 10px}.service-area-three .service-item .accordion li{position:relative;list-style-type:none;margin-bottom:30px}.service-area-three .service-img{margin-bottom:30px;text-align:center}.service-area-three .service-img img{max-width:420px;margin-left:auto;margin-right:auto;-webkit-animation:service-two-animation 10s infinite linear;animation:service-two-animation 10s infinite linear}@-webkit-keyframes service-two-animation{0%{-webkit-transform:scale(1);transform:scale(1)}50%{-webkit-transform:scale(1.1);transform:scale(1.1)}100%{-webkit-transform:scale(1);transform:scale(1)}}@keyframes service-two-animation{0%{-webkit-transform:scale(1);transform:scale(1)}50%{-webkit-transform:scale(1.1);transform:scale(1.1)}100%{-webkit-transform:scale(1);transform:scale(1)}}.join-area-two{margin-top:132px}.footer-item .footer-logo .footer-subscribe{position:relative}.footer-item .footer-logo .footer-subscribe .form-control{height:45px;border-radius:50px;-webkit-box-shadow:0 0 20px 0 #dddddd7d;box-shadow:0 0 20px 0 #dddddd7d;border:0;padding-left:20px;font-size:15px}.footer-item .footer-logo .footer-subscribe ::-webkit-input-placeholder{color:#b2b2b2}.footer-item .footer-logo .footer-subscribe :-ms-input-placeholder{color:#b2b2b2}.footer-item .footer-logo .footer-subscribe ::-ms-input-placeholder{color:#b2b2b2}.footer-item .footer-logo .footer-subscribe ::placeholder{color:#b2b2b2}.footer-item .footer-logo .footer-subscribe .footer-btn{width:35px;height:35px;line-height:37px;font-size:18px;border-radius:50%;color:#0b0320;background-color:#eec900;-webkit-transition:.5s all ease;transition:.5s all ease;position:absolute;top:5px;right:6px;padding:0}.footer-item .footer-logo .footer-subscribe .footer-btn:hover{color:#fff;background-color:#0b0320}.main-nav-three{background-color:#fff}.page-title-img-one{background-image:url(../img/about/page-title.jpg)}.page-title-area{background-position:center center;background-repeat:no-repeat;background-size:cover;padding-top:230px;padding-bottom:135px}.page-title-item{padding-left:40px;position:relative}.page-title-item:before{position:absolute;content:'';width:10px;height:100px;left:0;top:0;border-radius:50px;background-color:#784400}.page-title-item h2{font-size:40px;color:#fff;margin-bottom:10px}.page-title-item ul{margin:0;padding:0}.page-title-item ul li{list-style-type:none;display:inline-block;color:#784400}.page-title-item ul li i{display:inline-block;font-size:25px;position:relative;top:6px;color:#fff}.page-title-item ul li a{display:inline-block;color:#fff}.page-title-item ul li a:hover{color:#784400}.story-area{text-align:center;position:relative}.story-area .story-shape img{position:absolute;right:0;bottom:0;max-width:135px}.story-area .story-head{margin-bottom:50px}.story-area .story-head h2{font-weight:700;font-size:36px;margin-bottom:25px;margin-top:-10px}.story-area .story-head p{max-width:825px;margin-left:auto;margin-right:auto;margin-bottom:0}.story-area .story-item{margin-bottom:30px;position:relative;-webkit-transition:.5s all ease;transition:.5s all ease}.story-area .story-item:hover{-webkit-transform:translate(0,-10px);transform:translate(0,-10px)}.story-area .story-item:hover h3{-webkit-transform:translate(0,10px);transform:translate(0,10px);color:#fff;background-color:#784400}.story-area .story-item img{width:100%;border-radius:12px}.story-area .story-item h3{text-align:center;font-weight:600;font-size:20px;font-family:poppins,sans-serif;background-color:#fff;border-radius:10px;max-width:370px;margin-left:auto;margin-right:auto;padding:18px;margin-bottom:0;position:absolute;left:0;right:0;bottom:30px;-webkit-transition:.5s all ease;transition:.5s all ease}.download-area-two{background-color:#0b0320;position:relative;border-radius:0 0 80px 0}.download-area-two .download-shape img{position:absolute}.download-area-two .download-shape img:nth-child(1){left:0;top:0}.download-area-two .download-shape img:nth-child(2){left:100px;bottom:20px;-webkit-animation:download-one 10s infinite linear;animation:download-one 10s infinite linear}.download-area-two .download-item{margin-bottom:30px}.download-area-two .download-item h2{color:#fff;font-weight:700;font-size:35px;margin-bottom:40px}.download-area-two .download-item ul{margin:0;padding:0}.download-area-two .download-item ul li{list-style-type:none;display:inline-block;margin-right:20px}.download-area-two .download-item ul li:last-child{margin-right:0}.download-area-two .download-item ul li a{display:block;max-width:180px}.download-area-two .download-item ul li a:hover{-webkit-transform:scale(1.1);transform:scale(1.1)}.download-area-two .download-img{position:relative}.download-area-two .download-img img{position:absolute;top:-55px;right:0;left:0;max-width:460px;margin-left:auto;margin-right:auto}@-webkit-keyframes download-one{0%{-webkit-transform:translate(0,0);transform:translate(0,0)}30%{-webkit-transform:translate(0,-100px);transform:translate(0,-100px)}70%{-webkit-transform:translate(100px,0);transform:translate(100px,0)}100%{-webkit-transform:translate(0,0);transform:translate(0,0)}}@keyframes download-one{0%{-webkit-transform:translate(0,0);transform:translate(0,0)}30%{-webkit-transform:translate(0,-100px);transform:translate(0,-100px)}70%{-webkit-transform:translate(100px,0);transform:translate(100px,0)}100%{-webkit-transform:translate(0,0);transform:translate(0,0)}}.footer-item .footer-logo .footer-subscriber-two .form-control{-webkit-box-shadow:none;box-shadow:none}.service-area-four .service-item{border:1px solid #784400;margin-bottom:30px}.page-title-area-two{background-color:#0b0320;position:relative}.page-title-area-two:before{position:absolute;content:'';width:100%;height:100%;top:0;right:0;background-color:#784400;-webkit-clip-path:polygon(70% 0,100% 0%,100% 100%,40% 100%);clip-path:polygon(70% 0,100% 0%,100% 100%,40% 100%)}.page-title-area-two .page-title-plate ul{margin:0;padding:0;margin-left:75px}.page-title-area-two .page-title-plate ul li{list-style-type:none;display:inline-block;position:relative}.page-title-area-two .page-title-plate ul li img{display:inline-block;max-width:130px}.page-title-area-two .page-title-plate ul li:nth-child(1){z-index:4}.page-title-area-two .page-title-plate ul li:nth-child(2){left:-25px;top:0;z-index:3}.page-title-area-two .page-title-plate ul li:nth-child(3){left:-55px;top:0;z-index:2}.page-title-area-two .page-title-plate ul li:nth-child(4){left:-85px;top:0}.service-details-area .service-details-item{margin-bottom:30px}.service-details-area .service-details-item .service-details-more h3{font-weight:700;font-size:25px;margin-bottom:30px}.service-details-area .service-details-item .service-details-more ul{margin:0;padding:0}.service-details-area .service-details-item .service-details-more ul li{list-style-type:none;display:block;-webkit-box-shadow:0 0 20px 0 #dddddd8a;box-shadow:0 0 20px 0 #dddddd8a;margin-bottom:20px;position:relative;border-radius:10px;-webkit-transition:.5s all ease;transition:.5s all ease}.service-details-area .service-details-item .service-details-more ul li:hover{-webkit-box-shadow:none;box-shadow:none}.service-details-area .service-details-item .service-details-more ul li:hover a{color:#fff;background-color:#784400}.service-details-area .service-details-item .service-details-more ul li a{display:block;font-weight:500;font-size:15px;color:#0b0320;padding:15px 20px;border-radius:10px}.service-details-area .service-details-item .service-details-more ul li a i{display:inline-block;position:absolute;top:18px;right:17px;font-weight:700}.service-details-area .service-details-item .service-details-order{text-align:center;background-color:#0b0320;border-radius:10px;padding:30px 10px 0;position:relative;margin-top:30px}.service-details-area .service-details-item .service-details-order h3{font-weight:700;font-size:20px;color:#fff;margin-bottom:10px}.service-details-area .service-details-item .service-details-order span{display:block;color:#fff;margin-bottom:30px}.service-details-area .service-details-item .service-details-order .offer-off{background-color:#784400;width:70px;height:70px;border-radius:50%;text-align:center;padding-top:8px;position:absolute;bottom:135px;right:40px}.service-details-area .service-details-item .service-details-order .offer-off span{display:block;font-weight:600;font-size:18px;color:#0b0320;margin-bottom:0}.service-details-area .service-details-item .service-details-fresh h2{font-weight:700;font-size:35px;margin-bottom:15px}.service-details-area .service-details-item .service-details-fresh p{margin-bottom:25px}.service-details-area .service-details-item .service-details-fresh img{margin-bottom:25px;border-radius:18px}.service-details-area .service-details-item .service-details-fresh .service-details-p{margin-top:-15px}.page-title-img-two{background-image:url(../img/blog-details/1.jpg);position:relative}.page-title-img-two:before{position:absolute;content:'';width:100%;height:100%;left:0;top:0;background-color:#0b0320;-webkit-clip-path:polygon(0 0,60% 0%,40% 100%,0% 100%);clip-path:polygon(0 0,60% 0%,40% 100%,0% 100%);opacity:.9}.blog-details-more{padding-bottom:20px}.blog-details-tags h3{font-weight:700;font-size:25px;margin-bottom:30px}.blog-details-tags ul{margin:0;padding:0}.blog-details-tags ul li{list-style-type:none;display:inline-block;margin-right:5px;margin-bottom:10px}.blog-details-tags ul li a{display:block;font-size:12px;color:#696969;background-color:#f0f0f0;border-radius:30px;padding:7px 16px}.blog-details-tags ul li a:hover{color:#fff;background-color:#0b0320}.blog-details-nav ul{margin:0;padding:0}.blog-details-nav ul li{list-style-type:none;display:inline-block;margin-right:12px}.blog-details-nav ul li a{display:block;font-weight:500;font-size:15px;color:#0b0320;border:1px solid #0b0320;padding:9px 25px;border-radius:50px}.blog-details-nav ul li a:hover{border:1px solid #784400;background-color:#784400;color:#0b0320}.cart-wrap{text-align:center}.cart-wrap .table{margin:0}.cart-wrap .table .thead{background-color:#784400}.cart-wrap .table .thead tr .table-head{color:#fff;font-size:18px;padding-top:18px;padding-bottom:18px}.cart-wrap .table tr .table-item img{width:50px}.cart-wrap .table tr td{font-size:15px;color:#0b0320}.cart-wrap .table tr td a{color:#0b0320;display:block;font-weight:700}.cart-wrap .table tr td a:hover{color:#784400}.cart-wrap .table td,.cart-wrap .table th{border:1px solid #dee2e6;vertical-align:middle}.cart-wrap .table>:not(:first-child){border-top:0}.cart-wrap .shop-back{margin-bottom:20px}.cart-wrap .shop-back a{display:block;color:#0b0320;font-weight:500;margin-top:20px;font-size:15px}.cart-wrap .shop-back a:hover{color:#784400}.cart-wrap .total-shopping h2{color:#0b0320;font-size:35px;padding-bottom:5px;border-bottom:1px solid #000;display:inline-block;margin:0;margin-bottom:20px;font-weight:700}.cart-wrap .total-shopping h3{color:#0b0320;font-size:20px}.cart-wrap .total-shopping h3 span{display:inline-block;margin-left:70px}.cart-wrap .total-shopping a{margin-top:20px;display:inline-block;color:#fff;background-color:#784400;font-size:17px;padding:15px 60px}.cart-wrap .total-shopping a:hover{background-color:#0b0320}.checkout-area .cmn-btn{padding:12px 45px;-webkit-transition:.5s all ease;transition:.5s all ease}.checkout-item{border:1px solid #dddddd40;margin-bottom:30px}.checkout-item h2{text-align:center;font-size:35px;margin-bottom:50px;background-color:#784400;padding-top:15px;padding-bottom:15px;font-weight:700;color:#fff}.checkout-item .checkout-one{max-width:515px;margin:auto;padding-bottom:20px}.checkout-item .checkout-one label{color:#0b0320;font-size:15px;margin-right:10px;position:relative;top:2px;font-weight:500}.checkout-item .checkout-one .form-group{margin:0;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:end;-ms-flex-pack:end;justify-content:flex-end;-webkit-box-align:center;-ms-flex-align:center;align-items:center;margin-bottom:30px}.checkout-item .checkout-one .form-group .form-control{display:inline-block;width:78%;background-color:#dddddd40;border:1px solid transparent;border-radius:0;height:45px;font-size:15px}.checkout-item .checkout-one .form-group .form-control:focus{border:1px solid #784400;-webkit-box-shadow:none;box-shadow:none}.checkout-item .checkout-two{max-width:460px;margin:auto;padding-bottom:45px}.checkout-item .checkout-two h3{margin:0;margin-bottom:15px;font-size:22px;font-weight:500;font-family:poppins,sans-serif}.checkout-item .checkout-two p{font-size:15px;margin-bottom:10px}.checkout-item .checkout-two .form-check{margin-top:15px;margin-bottom:15px}.checkout-item .checkout-two .form-check span{display:inline-block;color:#797979;font-size:16px}.checkout-item .checkout-two .form-check span a{color:#797979}.checkout-item .checkout-two .form-check span a:hover{color:#0b0320}.checkout-item .checkout-two .form-check .form-check-input{margin-top:5px;width:14px;height:14px}.checkout-item .checkout-two .text-center{position:relative}.checkout-item .checkout-two .text-center:before{position:absolute;content:'';width:100%;height:1px;left:0;top:14px;background-color:#ddd}.checkout-item .checkout-two .text-center span{display:inline-block;color:#0b0320;font-size:15px;margin-bottom:20px;background-color:#fff;position:relative;padding-left:10px;padding-right:10px}.checkout-item .checkout-two ul{margin:0;padding:0;text-align:center}.checkout-item .checkout-two ul li{list-style-type:none;display:inline-block;margin-right:4px;margin-left:4px}.checkout-item .checkout-two ul li a{border:1px solid transparent;display:block}.checkout-item .checkout-two ul li a:hover{border:1px solid #784400}.coming-item{height:100vh;text-align:center}.coming-item h1{font-size:75px;font-weight:700;font-style:italic;margin-bottom:20px}.coming-item p{margin-bottom:40px;max-width:865px;margin-left:auto;margin-right:auto}.coming-item .coming-wrap{max-width:700px;margin-left:auto;margin-right:auto;margin-bottom:30px}.coming-item .coming-wrap .coming-inner{text-align:center;background-color:#efefef;padding-top:15px;padding-bottom:12px;margin-bottom:30px}.coming-item .coming-wrap .coming-inner h3{font-size:40px;font-weight:600;color:#232323;margin-bottom:5px}.coming-item .coming-wrap .coming-inner p{font-size:16px;margin-bottom:0}.coming-item ul{margin:0;padding:0}.coming-item ul li{list-style-type:none;display:inline-block;margin-right:2px;margin-left:2px}.coming-item ul li a{display:block;color:#fff;background-color:#784400;width:35px;height:35px;line-height:39px;border-radius:50%;font-size:16px}.coming-item ul li a:hover{background-color:#0b0320}.faq-area .faq-head h2{margin-bottom:35px;font-weight:600;font-size:30px;margin-top:-7px}.faq-area .faq-wrap{margin-bottom:50px}.faq-area .faq-wrap:last-child{margin-bottom:30px}.faq-area .accordion{padding-left:0;margin:0;padding:0}.faq-area .accordion p{font-size:15px;display:none;padding:20px 45px 15px 20px;margin-bottom:0}.faq-area .accordion a{color:#232323;font-size:17px;width:100%;display:block;cursor:pointer;font-weight:600;padding:15px 0 15px 18px;border:1px solid #232323;border-radius:8px 8px 0 0}.faq-area .accordion a:hover{color:#0b0320}.faq-area .accordion a:after{position:absolute;right:20px;content:"+";top:10px;color:#232323;font-size:25px;font-weight:700}.faq-area .accordion li{position:relative;list-style-type:none;margin-bottom:30px}.faq-area .accordion li:first-child{border-top:0}.faq-area .accordion li:last-child{margin-bottom:0}.faq-area .accordion li a.active{color:#fff;background-color:#0b0320;border:1px solid #0b0320}.faq-area .accordion li a.active:after{content:"-";font-size:25px;color:#fff}.privacy-area{padding-bottom:50px}.privacy-item{margin-bottom:50px}.privacy-item h2{font-size:26px;margin-bottom:15px;font-weight:600;font-family:poppins,sans-serif}.privacy-item p{margin-bottom:0}.privacy-item ul{margin:0;padding:0}.privacy-item ul li{list-style-type:none;display:block;margin-bottom:18px}.privacy-item ul li i{display:inline-block;font-size:20px;position:relative;bottom:-2px}.privacy-item ul li:last-child{margin-bottom:0}.error-item{height:700px;text-align:center;margin-top:25px}.error-item h1{font-size:130px;font-weight:700;margin-bottom:8px;font-family:poppins,sans-serif}.error-item p{margin-bottom:10px;font-weight:600;font-size:35px}.error-item span{display:block}.error-item a{display:inline-block;color:#fff;background-color:#784400;border-radius:10px;padding:16px 40px;margin-top:70px;font-size:18px}.error-item a:hover{background-color:#0b0320}.page-title-img-three{background-image:url(../img/contact-bg.jpg);position:relative}.page-title-img-three:before{position:absolute;content:'';width:100%;height:100%;top:0;left:0;opacity:.7;background-color:#0b0320}.contact-location-area{background-color:#fffdf9}.contact-location-area .location-item{text-align:center;background-color:#fff;padding:40px 20px;border-radius:20px;position:relative;margin-bottom:30px;-webkit-transition:.5s all ease;transition:.5s all ease;z-index:1}.contact-location-area .location-item:hover,.contact-location-area .location-item.active{background-color:#784400}.contact-location-area .location-item:hover img,.contact-location-area .location-item.active img{opacity:1}.contact-location-area .location-item:hover i,.contact-location-area .location-item.active i{color:#fff;background-color:#0b0320}.contact-location-area .location-item:hover ul li,.contact-location-area .location-item.active ul li{color:#fff}.contact-location-area .location-item img{position:absolute;top:0;left:0;width:100%;height:100%;opacity:0;-webkit-transition:.5s all ease;transition:.5s all ease;z-index:-1}.contact-location-area .location-item i{width:65px;height:65px;line-height:65px;border-radius:50%;text-align:center;display:block;font-size:33px;color:#0b0320;margin-bottom:15px;margin-left:auto;margin-right:auto;-webkit-transition:.5s all ease;transition:.5s all ease;background-color:#784400}.contact-location-area .location-item ul{margin:0;padding:0}.contact-location-area .location-item ul li{list-style-type:none;display:block;color:#0b0320;font-weight:500;font-size:16px;-webkit-transition:.5s all ease;transition:.5s all ease;margin-bottom:5px}.contact-location-area .location-item ul li:last-child{margin-bottom:0}.contact-location-area .location-item ul li a{display:block;color:#0b0320}.contact-location-area .location-item ul li a:hover{color:#fff}.contact-form-area{background-image:url(../img/contact-form-bg.jpg);background-size:cover;background-position:center center;background-repeat:no-repeat}.contact-form-area .contact-item{background-color:#ffffff9e;padding:70px 50px;border-radius:15px}.contact-form-area .contact-item #contactForm{margin-bottom:40px}.contact-form-area .contact-item #contactForm .form-group{margin-bottom:20px}.contact-form-area .contact-item #contactForm .form-group .form-control{height:55px;border-radius:30px;padding-left:30px;border:0;background-color:#fff;font-size:15px}.contact-form-area .contact-item #contactForm .form-group .form-control:focus{border:0;-webkit-box-shadow:none;box-shadow:none}.contact-form-area .contact-item #contactForm .form-group ::-webkit-input-placeholder{color:#b2b2b2}.contact-form-area .contact-item #contactForm .form-group :-ms-input-placeholder{color:#b2b2b2}.contact-form-area .contact-item #contactForm .form-group ::-ms-input-placeholder{color:#b2b2b2}.contact-form-area .contact-item #contactForm .form-group ::placeholder{color:#b2b2b2}.contact-form-area .contact-item #contactForm .form-group textarea{height:auto!important;padding-top:10px}.contact-form-area .contact-item #contactForm .cmn-btn{opacity:1;-webkit-transition:.5s all ease;transition:.5s all ease;margin-top:10px}.contact-form-area .contact-item .contact-social span{display:block;font-size:16px;color:#0b0320;margin-bottom:15px}.contact-form-area .contact-item .contact-social ul{margin:0;padding:0}.contact-form-area .contact-item .contact-social ul li{list-style-type:none;display:inline-block;margin-right:10px}.contact-form-area .contact-item .contact-social ul li:last-child{margin-right:0}.contact-form-area .contact-item .contact-social ul li a{display:block;width:40px;height:40px;line-height:45px;text-align:center;border-radius:50%;color:#fff;background-color:#0b0320;font-size:20px}.contact-form-area .contact-item .contact-social ul li a:hover{background-color:#784400}.contact-form-area .contact-item .list-unstyled{color:#dc3545;margin-bottom:0;margin-top:10px}.contact-form-area .contact-item .text-danger{color:#dc3545;margin-top:14px;margin-bottom:0}.contact-form-area .contact-item .text-success{color:#28a745;margin-top:14px;margin-bottom:0}.contact-form-area .contact-img{text-align:center;position:relative}.contact-form-area .contact-img img{position:absolute;left:0;right:0;top:43px;margin-left:auto;margin-right:auto}.book-table-area .book-table-wrap{background-color:#f5f5f5;padding:60px 80px 65px;border-radius:10px}.book-table-area .form-group{margin-bottom:30px}.book-table-area .form-group .form-control{height:50px;border-radius:5px;border:1px solid #ddd;padding-left:25px;font-size:15px}.book-table-area .form-group .form-control:focus{-webkit-box-shadow:none;box-shadow:none;border:1px solid #784400}.book-table-area .form-group textarea{height:auto!important;padding-top:15px}.book-table-area .cmn-btn{-webkit-transition:.5s all ease;transition:.5s all ease}.loader{position:fixed;top:0;left:0;width:100%;height:100%;z-index:99999;background:#0b0320}.spinner{width:50px;height:50px;margin:100px auto;background-color:#333;border-radius:100%;-webkit-animation:sk-scaleout 1s infinite ease-in-out;animation:sk-scaleout 1s infinite ease-in-out}@-webkit-keyframes sk-scaleout{0%{-webkit-transform:scale(0);transform:scale(0)}100%{-webkit-transform:scale(1);transform:scale(1);opacity:0}}@keyframes sk-scaleout{0%{-webkit-transform:scale(0);transform:scale(0)}100%{-webkit-transform:scale(1);transform:scale(1);opacity:0}}#toTop{position:fixed;bottom:30px;right:0;cursor:pointer;display:none;z-index:10}.back-to-top-btn i{background:#784400;color:#fff;height:50px;width:50px;line-height:50px;display:inline-block;text-align:center;font-size:30px;border-radius:50%;-webkit-transition:.5s all ease;transition:.5s all ease;margin-right:28px;-webkit-box-shadow:0 0 14px 0 #784400;box-shadow:0 0 14px 0 #784400}.back-to-top-btn i:hover{background-color:#0b0320;color:#fff;-webkit-box-shadow:0 0 14px 0 #0b0320;box-shadow:0 0 14px 0 #0b0320}.buy-now-btn{right:20px;z-index:99;top:50%;position:fixed;-webkit-transform:translateY(-50%);transform:translateY(-50%);border-radius:30px;display:inline-block;color:#fff;background-color:#82b440;padding:10px 20px 10px 42px;-webkit-box-shadow:0 1px 20px 1px #82b440;box-shadow:0 1px 20px 1px #82b440;font-size:13px;font-weight:600}.buy-now-btn img{top:50%;left:20px;width:15px;position:absolute;-webkit-transform:translateY(-50%);transform:translateY(-50%)}.buy-now-btn:hover{color:#fff;background-color:#94be5d} \ No newline at end of file diff --git a/themes/30coffe/assets/css/theme-dark.css b/themes/30coffe/assets/css/theme-dark.css new file mode 100644 index 00000000..f31811fe --- /dev/null +++ b/themes/30coffe/assets/css/theme-dark.css @@ -0,0 +1 @@ +.switch-box{position:fixed;bottom:15px;right:120px;z-index:9999}.switch-box .slider{position:absolute;cursor:pointer;top:0;left:0;right:0;bottom:0;background-color:transparent;border:1.5px solid #fff;-webkit-transition:.4s;transition:.4s}.switch-box .slider::before{position:absolute;content:"";height:25px;width:25px;left:0;bottom:4px;top:0;bottom:0;margin:auto 0;-webkit-transition:.4s;transition:.4s;-webkit-box-shadow:0 0 15px #2020203d;box-shadow:0 0 15px #2020203d;background:#fff url(../img/night.png);background-repeat:no-repeat;background-position:center}.switch-box .slider.round{border-radius:34px}.switch-box .slider.round::before{border-radius:50%}.switch-box .switch{position:relative;display:inline-block;width:50px;height:30px}.switch-box .switch input{opacity:0;width:0;height:0}.switch-box input:checked+.slider{background-color:transparent;border:1.5px solid #fdb819}.switch-box input:focus+.slider{-webkit-box-shadow:0 0 1px #fdb819;box-shadow:0 0 1px #fdb819}.switch-box input:checked+.slider:before{-webkit-transform:translateX(24px);transform:translateX(24px);background:#fff url(../img/sunny.png);background-repeat:no-repeat;background-position:center}.theme-dark h1,.theme-dark h2,.theme-dark h3,.theme-dark h4,.theme-dark h5,.theme-dark h6{color:#fff}.theme-dark h3{color:#fff!important}.theme-dark h3 a{color:#fff!important;-webkit-transition:.7s;transition:.7s}.theme-dark h3 a:hover{color:#fdb819!important}.theme-dark p{color:#fff!important}.theme-dark .content h3{color:#fff}.theme-dark .content h3 a{color:#fff}.theme-dark .content span{color:#fff}.theme-dark .content .read-btn{color:#fff}.theme-dark body{background:#1d1d1d;color:#fff}.theme-dark .navbar-area .side-nav .nav-cart{border:1px solid #fff;color:#fff}.theme-dark .navbar-area .side-nav .nav-cart:hover{color:#fff;background-color:#fdb819}.theme-dark .navbar-area .side-nav .nav-cart span{color:#fdb819;background-color:#fff}.theme-dark .navbar-light .navbar-brand .logo-one{display:none}.theme-dark .navbar-light .navbar-brand .logo-two{display:inline-block}.theme-dark .main-nav{background-color:transparent}.theme-dark .main-nav nav .navbar-nav .nav-item a{color:#fff}.theme-dark .main-nav nav .navbar-nav .nav-item a i{color:#fff}.theme-dark .main-nav nav .navbar-nav .nav-item a:hover{color:#fdb819!important}.theme-dark .main-nav nav .navbar-nav .nav-item a.active{color:#fdb819!important}.theme-dark .main-nav nav .navbar-nav .nav-item .dropdown-menu{background-color:#252525!important}.theme-dark .main-nav nav .navbar-nav .nav-item .dropdown-menu li a{color:#fff!important}.theme-dark .main-nav nav .navbar-nav .nav-item .dropdown-menu li a.active{color:#fdb819!important}.theme-dark .main-nav nav .navbar-nav .nav-item .dropdown-menu li a:hover{color:#fdb819!important}.theme-dark .menu-shrink{background-color:#252525}.theme-dark .main-nav-two.menu-shrink .nav-two-logo-one{display:block}.theme-dark .main-nav-two.menu-shrink .nav-two-logo-two{display:none}.theme-dark #myModalRight .modal-header .btn-close{background-image:none;color:#fff;position:relative}.theme-dark #myModalRight .modal-header .btn-close::before{content:'\ec8d';font-family:boxicons!important;font-size:30px;color:#fff;position:absolute;top:-7px;left:0;right:0}.theme-dark #myModalRight .modal-header .modal-header-logo1{display:none}.theme-dark #myModalRight .modal-header .modal-header-logo2{display:inline-block}.theme-dark #myModalRight .modal-content .modal-body h2{color:#fff}.theme-dark #myModalRight .modal-content .modal-body .social-area ul li a{color:#fff;border-color:#fff;-webkit-transition:.7s;transition:.7s}.theme-dark #myModalRight .modal-content .modal-body .social-area ul li a:hover{background-color:#fdb819}.theme-dark .modal.modal-right .modal-content{background-color:#252525}.theme-dark .banner-area .banner-content form .form-control{color:#fff;background-color:#0e0e0e}.theme-dark .banner-area-three::before{background-color:#252525}.theme-dark .banner-area-three .banner-content h1{color:#fff}.theme-dark .banner-area-three .banner-content .banner-btn-wrap .banner-btn-two{color:#fff;border-color:#fff}.theme-dark .page-title-img-three:before{background-color:#0e0e0e;opacity:.8}.theme-dark .service-area{background-color:#252525}.theme-dark .service-area-three{background-color:#000}.theme-dark .service-area-three .service-item .accordion a{background-color:#252525;color:#fff}.theme-dark .service-area-three .service-item .accordion a::after{color:#fff}.theme-dark .service-area-three .service-item .accordion a span{color:#fff}.theme-dark .sorting-menu ul li{color:#fff;background-color:#0e0e0e}.theme-dark .sorting-menu ul li:hover,.theme-dark .sorting-menu ul li.active{color:#fff;background-color:#fdb819}.theme-dark .collection-item .collection-bottom ul li .form-control{background-color:transparent}.theme-dark .collection-area .more-collection a{color:#fff;border-color:#fff;-webkit-transition:.7s;transition:.7s}.theme-dark .collection-area .more-collection a:hover{color:#fdb819;border-color:#fdb819}.theme-dark .collection-area-two{background-color:#0f0f0f}.theme-dark .download-area .download-content ul li{background-color:#252525;color:#fff}.theme-dark .menu-area{background-color:#252525}.theme-dark .menu-item{background-color:#0e0e0e}.theme-dark .reservation-area{background-color:#0e0e0e}.theme-dark .reservation-area .reservation-item ul{background-color:#252525}.theme-dark .reservation-area .reservation-item ul li .form-control{background-color:#252525;color:#fff;border-color:#fff}.theme-dark .review-area-two{background-color:#000}.theme-dark .story-area .story-item h3{background-color:#252525}.theme-dark .blog-area .read-blog-btn{color:#fff;border-color:#fff;-webkit-transition:.7s;transition:.7s}.theme-dark .blog-area .read-blog-btn:hover{color:#fdb819;border-color:#fdb819}.theme-dark .service-details-area .service-details-item .service-details-more ul li a{color:#fff}.theme-dark .blog-details-tags ul li a{background-color:#252525;color:#fff;-webkit-transition:.7s;transition:.7s}.theme-dark .blog-details-tags ul li a:hover{background-color:#fdb819;color:#fff}.theme-dark .blog-details-nav ul li a{color:#fff;border-color:#fff}.theme-dark .subscribe-area{background-color:#0e0e0e}.theme-dark .subscribe-item .newsletter-form .form-control{color:#fff;background-color:#252525}.theme-dark .footer-item .footer-logo a .footer-logo1{display:none}.theme-dark .footer-item .footer-logo a .footer-logo2{display:inline-block}.theme-dark .footer-item .footer-logo .footer-subscriber-two .form-control{background-color:#252525;color:#fff}.theme-dark .footer-item .footer-service ul li{color:#fff}.theme-dark .footer-item .footer-service ul li a{color:#fff;-webkit-transition:.7s;transition:.7s}.theme-dark .footer-item .footer-service ul li a:hover{color:#fdb819}.theme-dark .contact-location-area{background-color:#252525}.theme-dark .contact-location-area .location-item{background-color:#0e0e0e}.theme-dark .contact-location-area .location-item ul li{color:#fff}.theme-dark .contact-location-area .location-item ul li a{color:#fff}.theme-dark .contact-location-area .location-item:hover,.theme-dark .contact-location-area .location-item.active{background-color:#784400}.theme-dark .contact-form-area .contact-item{background-color:#00000082}.theme-dark .contact-form-area .contact-item #contactForm .form-group .form-control{background-color:#0e0e0e;color:#fff}.theme-dark .contact-form-area .contact-item .contact-social span{color:#fff}.theme-dark .book-table-area .book-table-wrap{background-color:#252525}.theme-dark .book-table-area .book-table-wrap .form-group .form-control{background-color:#0e0e0e;color:#fff}.theme-dark .cart-wrap .table tr td{color:#fff}.theme-dark .cart-wrap .table tr td a{color:#fff}.theme-dark .cart-wrap .table tr td a:hover{color:#fdb819}.theme-dark .cart-wrap .shop-back a{color:#fff}.theme-dark .cart-wrap .shop-back a:hover{color:#fdb819}.theme-dark .cart-wrap .total-shopping h2{color:#fff;border-color:#fff}.theme-dark .cart-wrap .total-shopping h3{color:#fff}.theme-dark .cart-wrap .total-shopping a{color:#fff}.theme-dark .checkout-item .checkout-one label{color:#fff}.theme-dark .checkout-item .checkout-two .form-check span a{color:#fdb819;-webkit-transition:.7s;transition:.7s}.theme-dark .checkout-item .checkout-two .form-check span a:hover{color:#fff}.theme-dark .faq-area .accordion a{color:#fff;border-color:#fff}.theme-dark .faq-area .accordion a::after{color:#fff}.theme-dark .coming-item .coming-wrap .coming-inner{background-color:#252525}.theme-dark .coming-item .coming-wrap .coming-inner h3{color:#fff}.theme-dark .copyright-area,.theme-dark .footer-area-two{background-color:#000} \ No newline at end of file diff --git a/themes/30coffe/assets/fonts/ajax-loader.gif b/themes/30coffe/assets/fonts/ajax-loader.gif new file mode 100644 index 00000000..d3cb0f1e Binary files /dev/null and b/themes/30coffe/assets/fonts/ajax-loader.gif differ diff --git a/themes/30coffe/assets/fonts/boxicons.eot b/themes/30coffe/assets/fonts/boxicons.eot new file mode 100644 index 00000000..9b0bf769 Binary files /dev/null and b/themes/30coffe/assets/fonts/boxicons.eot differ diff --git a/themes/30coffe/assets/fonts/boxicons.ttf b/themes/30coffe/assets/fonts/boxicons.ttf new file mode 100644 index 00000000..0b52d5cb Binary files /dev/null and b/themes/30coffe/assets/fonts/boxicons.ttf differ diff --git a/themes/30coffe/assets/fonts/boxicons.woff b/themes/30coffe/assets/fonts/boxicons.woff new file mode 100644 index 00000000..429c00df Binary files /dev/null and b/themes/30coffe/assets/fonts/boxicons.woff differ diff --git a/themes/30coffe/assets/fonts/boxicons.woff2 b/themes/30coffe/assets/fonts/boxicons.woff2 new file mode 100644 index 00000000..9179e64b Binary files /dev/null and b/themes/30coffe/assets/fonts/boxicons.woff2 differ diff --git a/themes/30coffe/assets/fonts/boxiconsd41d.svg b/themes/30coffe/assets/fonts/boxiconsd41d.svg new file mode 100644 index 00000000..7732686b --- /dev/null +++ b/themes/30coffe/assets/fonts/boxiconsd41d.svg @@ -0,0 +1,1626 @@ + + + + + + +{ + "fontFamily": "boxicons", + "majorVersion": 2, + "minorVersion": 0.7, + "version": "Version 2.0", + "fontId": "boxicons", + "psName": "boxicons", + "subFamily": "Regular", + "fullName": "boxicons", + "description": "Font generated by IcoMoon." +} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/themes/30coffe/assets/fonts/slick.eot b/themes/30coffe/assets/fonts/slick.eot new file mode 100644 index 00000000..2cbab9ca Binary files /dev/null and b/themes/30coffe/assets/fonts/slick.eot differ diff --git a/themes/30coffe/assets/fonts/slick.svg b/themes/30coffe/assets/fonts/slick.svg new file mode 100644 index 00000000..b36a66a6 --- /dev/null +++ b/themes/30coffe/assets/fonts/slick.svg @@ -0,0 +1,14 @@ + + + +Generated by Fontastic.me + + + + + + + + + + diff --git a/themes/30coffe/assets/fonts/slick.ttf b/themes/30coffe/assets/fonts/slick.ttf new file mode 100644 index 00000000..9d03461b Binary files /dev/null and b/themes/30coffe/assets/fonts/slick.ttf differ diff --git a/themes/30coffe/assets/fonts/slick.woff b/themes/30coffe/assets/fonts/slick.woff new file mode 100644 index 00000000..8ee99721 Binary files /dev/null and b/themes/30coffe/assets/fonts/slick.woff differ diff --git a/themes/30coffe/assets/fonts/slickd41d.eot b/themes/30coffe/assets/fonts/slickd41d.eot new file mode 100644 index 00000000..2cbab9ca Binary files /dev/null and b/themes/30coffe/assets/fonts/slickd41d.eot differ diff --git a/themes/30coffe/assets/img/1.png b/themes/30coffe/assets/img/1.png new file mode 100644 index 00000000..91c77817 Binary files /dev/null and b/themes/30coffe/assets/img/1.png differ diff --git a/themes/30coffe/assets/img/2.png b/themes/30coffe/assets/img/2.png new file mode 100644 index 00000000..534c687a Binary files /dev/null and b/themes/30coffe/assets/img/2.png differ diff --git a/themes/30coffe/assets/img/3.png b/themes/30coffe/assets/img/3.png new file mode 100644 index 00000000..307fa0e3 Binary files /dev/null and b/themes/30coffe/assets/img/3.png differ diff --git a/themes/30coffe/assets/img/about/app.png b/themes/30coffe/assets/img/about/app.png new file mode 100644 index 00000000..740286c2 Binary files /dev/null and b/themes/30coffe/assets/img/about/app.png differ diff --git a/themes/30coffe/assets/img/about/download2.png b/themes/30coffe/assets/img/about/download2.png new file mode 100644 index 00000000..6b16a551 Binary files /dev/null and b/themes/30coffe/assets/img/about/download2.png differ diff --git a/themes/30coffe/assets/img/about/page-title.jpg b/themes/30coffe/assets/img/about/page-title.jpg new file mode 100644 index 00000000..3c950bca Binary files /dev/null and b/themes/30coffe/assets/img/about/page-title.jpg differ diff --git a/themes/30coffe/assets/img/about/story1.jpg b/themes/30coffe/assets/img/about/story1.jpg new file mode 100644 index 00000000..7dbb2143 Binary files /dev/null and b/themes/30coffe/assets/img/about/story1.jpg differ diff --git a/themes/30coffe/assets/img/about/story2.jpg b/themes/30coffe/assets/img/about/story2.jpg new file mode 100644 index 00000000..e2548d77 Binary files /dev/null and b/themes/30coffe/assets/img/about/story2.jpg differ diff --git a/themes/30coffe/assets/img/about/story3.png b/themes/30coffe/assets/img/about/story3.png new file mode 100644 index 00000000..274bf9b1 Binary files /dev/null and b/themes/30coffe/assets/img/about/story3.png differ diff --git a/themes/30coffe/assets/img/blog-details/1.jpg b/themes/30coffe/assets/img/blog-details/1.jpg new file mode 100644 index 00000000..f58a7fe9 Binary files /dev/null and b/themes/30coffe/assets/img/blog-details/1.jpg differ diff --git a/themes/30coffe/assets/img/blog-details/2.jpg b/themes/30coffe/assets/img/blog-details/2.jpg new file mode 100644 index 00000000..eb5cda1c Binary files /dev/null and b/themes/30coffe/assets/img/blog-details/2.jpg differ diff --git a/themes/30coffe/assets/img/contact-bg.jpg b/themes/30coffe/assets/img/contact-bg.jpg new file mode 100644 index 00000000..4a2c5b0f Binary files /dev/null and b/themes/30coffe/assets/img/contact-bg.jpg differ diff --git a/themes/30coffe/assets/img/contact-form-bg.jpg b/themes/30coffe/assets/img/contact-form-bg.jpg new file mode 100644 index 00000000..e3be79b2 Binary files /dev/null and b/themes/30coffe/assets/img/contact-form-bg.jpg differ diff --git a/themes/30coffe/assets/img/contact-man.png b/themes/30coffe/assets/img/contact-man.png new file mode 100644 index 00000000..8606d4a3 Binary files /dev/null and b/themes/30coffe/assets/img/contact-man.png differ diff --git a/themes/30coffe/assets/img/favicon.png b/themes/30coffe/assets/img/favicon.png new file mode 100644 index 00000000..f87f0138 Binary files /dev/null and b/themes/30coffe/assets/img/favicon.png differ diff --git a/themes/30coffe/assets/img/home-one/banner/banner-shape.png b/themes/30coffe/assets/img/home-one/banner/banner-shape.png new file mode 100644 index 00000000..f9115566 Binary files /dev/null and b/themes/30coffe/assets/img/home-one/banner/banner-shape.png differ diff --git a/themes/30coffe/assets/img/home-one/banner/banner-slider1.png b/themes/30coffe/assets/img/home-one/banner/banner-slider1.png new file mode 100644 index 00000000..a05078e7 Binary files /dev/null and b/themes/30coffe/assets/img/home-one/banner/banner-slider1.png differ diff --git a/themes/30coffe/assets/img/home-one/banner/banner-slider2.png b/themes/30coffe/assets/img/home-one/banner/banner-slider2.png new file mode 100644 index 00000000..2a510716 Binary files /dev/null and b/themes/30coffe/assets/img/home-one/banner/banner-slider2.png differ diff --git a/themes/30coffe/assets/img/home-one/banner/banner-slider3.png b/themes/30coffe/assets/img/home-one/banner/banner-slider3.png new file mode 100644 index 00000000..bf509b13 Binary files /dev/null and b/themes/30coffe/assets/img/home-one/banner/banner-slider3.png differ diff --git a/themes/30coffe/assets/img/home-one/banner/shape1.png b/themes/30coffe/assets/img/home-one/banner/shape1.png new file mode 100644 index 00000000..58acd104 Binary files /dev/null and b/themes/30coffe/assets/img/home-one/banner/shape1.png differ diff --git a/themes/30coffe/assets/img/home-one/banner/shape2.png b/themes/30coffe/assets/img/home-one/banner/shape2.png new file mode 100644 index 00000000..94b35505 Binary files /dev/null and b/themes/30coffe/assets/img/home-one/banner/shape2.png differ diff --git a/themes/30coffe/assets/img/home-one/banner/shape3.png b/themes/30coffe/assets/img/home-one/banner/shape3.png new file mode 100644 index 00000000..4fca02c8 Binary files /dev/null and b/themes/30coffe/assets/img/home-one/banner/shape3.png differ diff --git a/themes/30coffe/assets/img/home-one/blog1.jpg b/themes/30coffe/assets/img/home-one/blog1.jpg new file mode 100644 index 00000000..4ee8f37b Binary files /dev/null and b/themes/30coffe/assets/img/home-one/blog1.jpg differ diff --git a/themes/30coffe/assets/img/home-one/blog2.jpg b/themes/30coffe/assets/img/home-one/blog2.jpg new file mode 100644 index 00000000..c2351ad5 Binary files /dev/null and b/themes/30coffe/assets/img/home-one/blog2.jpg differ diff --git a/themes/30coffe/assets/img/home-one/blog3.jpg b/themes/30coffe/assets/img/home-one/blog3.jpg new file mode 100644 index 00000000..cd6cc67e Binary files /dev/null and b/themes/30coffe/assets/img/home-one/blog3.jpg differ diff --git a/themes/30coffe/assets/img/home-one/blog4.jpg b/themes/30coffe/assets/img/home-one/blog4.jpg new file mode 100644 index 00000000..8b213a38 Binary files /dev/null and b/themes/30coffe/assets/img/home-one/blog4.jpg differ diff --git a/themes/30coffe/assets/img/home-one/blog5.jpg b/themes/30coffe/assets/img/home-one/blog5.jpg new file mode 100644 index 00000000..39479918 Binary files /dev/null and b/themes/30coffe/assets/img/home-one/blog5.jpg differ diff --git a/themes/30coffe/assets/img/home-one/blog6.jpg b/themes/30coffe/assets/img/home-one/blog6.jpg new file mode 100644 index 00000000..0cd761fc Binary files /dev/null and b/themes/30coffe/assets/img/home-one/blog6.jpg differ diff --git a/themes/30coffe/assets/img/home-one/chef/1.jpg b/themes/30coffe/assets/img/home-one/chef/1.jpg new file mode 100644 index 00000000..106699a2 Binary files /dev/null and b/themes/30coffe/assets/img/home-one/chef/1.jpg differ diff --git a/themes/30coffe/assets/img/home-one/chef/2.jpg b/themes/30coffe/assets/img/home-one/chef/2.jpg new file mode 100644 index 00000000..94103a3d Binary files /dev/null and b/themes/30coffe/assets/img/home-one/chef/2.jpg differ diff --git a/themes/30coffe/assets/img/home-one/chef/3.jpg b/themes/30coffe/assets/img/home-one/chef/3.jpg new file mode 100644 index 00000000..9f565cf8 Binary files /dev/null and b/themes/30coffe/assets/img/home-one/chef/3.jpg differ diff --git a/themes/30coffe/assets/img/home-one/chef/4.jpg b/themes/30coffe/assets/img/home-one/chef/4.jpg new file mode 100644 index 00000000..181d2eb3 Binary files /dev/null and b/themes/30coffe/assets/img/home-one/chef/4.jpg differ diff --git a/themes/30coffe/assets/img/home-one/chef/5.jpg b/themes/30coffe/assets/img/home-one/chef/5.jpg new file mode 100644 index 00000000..ed114548 Binary files /dev/null and b/themes/30coffe/assets/img/home-one/chef/5.jpg differ diff --git a/themes/30coffe/assets/img/home-one/chef/6.jpg b/themes/30coffe/assets/img/home-one/chef/6.jpg new file mode 100644 index 00000000..9f6c480c Binary files /dev/null and b/themes/30coffe/assets/img/home-one/chef/6.jpg differ diff --git a/themes/30coffe/assets/img/home-one/chef/7.jpg b/themes/30coffe/assets/img/home-one/chef/7.jpg new file mode 100644 index 00000000..8e41843e Binary files /dev/null and b/themes/30coffe/assets/img/home-one/chef/7.jpg differ diff --git a/themes/30coffe/assets/img/home-one/chef/8.jpg b/themes/30coffe/assets/img/home-one/chef/8.jpg new file mode 100644 index 00000000..82d38bb4 Binary files /dev/null and b/themes/30coffe/assets/img/home-one/chef/8.jpg differ diff --git a/themes/30coffe/assets/img/home-one/collection/1.jpg b/themes/30coffe/assets/img/home-one/collection/1.jpg new file mode 100644 index 00000000..3c1ff164 Binary files /dev/null and b/themes/30coffe/assets/img/home-one/collection/1.jpg differ diff --git a/themes/30coffe/assets/img/home-one/collection/2.jpg b/themes/30coffe/assets/img/home-one/collection/2.jpg new file mode 100644 index 00000000..8122f4ae Binary files /dev/null and b/themes/30coffe/assets/img/home-one/collection/2.jpg differ diff --git a/themes/30coffe/assets/img/home-one/collection/3.jpg b/themes/30coffe/assets/img/home-one/collection/3.jpg new file mode 100644 index 00000000..eae54b63 Binary files /dev/null and b/themes/30coffe/assets/img/home-one/collection/3.jpg differ diff --git a/themes/30coffe/assets/img/home-one/collection/4.jpg b/themes/30coffe/assets/img/home-one/collection/4.jpg new file mode 100644 index 00000000..bb600d60 Binary files /dev/null and b/themes/30coffe/assets/img/home-one/collection/4.jpg differ diff --git a/themes/30coffe/assets/img/home-one/collection/5.jpg b/themes/30coffe/assets/img/home-one/collection/5.jpg new file mode 100644 index 00000000..081774da Binary files /dev/null and b/themes/30coffe/assets/img/home-one/collection/5.jpg differ diff --git a/themes/30coffe/assets/img/home-one/collection/6.jpg b/themes/30coffe/assets/img/home-one/collection/6.jpg new file mode 100644 index 00000000..5517f22e Binary files /dev/null and b/themes/30coffe/assets/img/home-one/collection/6.jpg differ diff --git a/themes/30coffe/assets/img/home-one/collection/7.jpg b/themes/30coffe/assets/img/home-one/collection/7.jpg new file mode 100644 index 00000000..4f458587 Binary files /dev/null and b/themes/30coffe/assets/img/home-one/collection/7.jpg differ diff --git a/themes/30coffe/assets/img/home-one/collection/8.jpg b/themes/30coffe/assets/img/home-one/collection/8.jpg new file mode 100644 index 00000000..7b6575b2 Binary files /dev/null and b/themes/30coffe/assets/img/home-one/collection/8.jpg differ diff --git a/themes/30coffe/assets/img/home-one/feature1.jpg b/themes/30coffe/assets/img/home-one/feature1.jpg new file mode 100644 index 00000000..c3d77cb9 Binary files /dev/null and b/themes/30coffe/assets/img/home-one/feature1.jpg differ diff --git a/themes/30coffe/assets/img/home-one/feature1.png b/themes/30coffe/assets/img/home-one/feature1.png new file mode 100644 index 00000000..fe9a0a9b Binary files /dev/null and b/themes/30coffe/assets/img/home-one/feature1.png differ diff --git a/themes/30coffe/assets/img/home-one/feature2.jpg b/themes/30coffe/assets/img/home-one/feature2.jpg new file mode 100644 index 00000000..0ad20bf3 Binary files /dev/null and b/themes/30coffe/assets/img/home-one/feature2.jpg differ diff --git a/themes/30coffe/assets/img/home-one/feature2.png b/themes/30coffe/assets/img/home-one/feature2.png new file mode 100644 index 00000000..d9f93db1 Binary files /dev/null and b/themes/30coffe/assets/img/home-one/feature2.png differ diff --git a/themes/30coffe/assets/img/home-one/feature3.jpg b/themes/30coffe/assets/img/home-one/feature3.jpg new file mode 100644 index 00000000..2ac9713e Binary files /dev/null and b/themes/30coffe/assets/img/home-one/feature3.jpg differ diff --git a/themes/30coffe/assets/img/home-one/feature3.png b/themes/30coffe/assets/img/home-one/feature3.png new file mode 100644 index 00000000..6e258629 Binary files /dev/null and b/themes/30coffe/assets/img/home-one/feature3.png differ diff --git a/themes/30coffe/assets/img/home-one/feature4.jpg b/themes/30coffe/assets/img/home-one/feature4.jpg new file mode 100644 index 00000000..af133e75 Binary files /dev/null and b/themes/30coffe/assets/img/home-one/feature4.jpg differ diff --git a/themes/30coffe/assets/img/home-one/feature5.jpg b/themes/30coffe/assets/img/home-one/feature5.jpg new file mode 100644 index 00000000..cf9a77d6 Binary files /dev/null and b/themes/30coffe/assets/img/home-one/feature5.jpg differ diff --git a/themes/30coffe/assets/img/home-one/feature6.jpg b/themes/30coffe/assets/img/home-one/feature6.jpg new file mode 100644 index 00000000..a9506a08 Binary files /dev/null and b/themes/30coffe/assets/img/home-one/feature6.jpg differ diff --git a/themes/30coffe/assets/img/home-one/menu1.png b/themes/30coffe/assets/img/home-one/menu1.png new file mode 100644 index 00000000..844ae081 Binary files /dev/null and b/themes/30coffe/assets/img/home-one/menu1.png differ diff --git a/themes/30coffe/assets/img/home-one/menu2.png b/themes/30coffe/assets/img/home-one/menu2.png new file mode 100644 index 00000000..94ae6a1c Binary files /dev/null and b/themes/30coffe/assets/img/home-one/menu2.png differ diff --git a/themes/30coffe/assets/img/home-one/menu3.png b/themes/30coffe/assets/img/home-one/menu3.png new file mode 100644 index 00000000..64ef7282 Binary files /dev/null and b/themes/30coffe/assets/img/home-one/menu3.png differ diff --git a/themes/30coffe/assets/img/home-one/reservation-shape.png b/themes/30coffe/assets/img/home-one/reservation-shape.png new file mode 100644 index 00000000..11aa2f47 Binary files /dev/null and b/themes/30coffe/assets/img/home-one/reservation-shape.png differ diff --git a/themes/30coffe/assets/img/home-one/reservation.png b/themes/30coffe/assets/img/home-one/reservation.png new file mode 100644 index 00000000..9eda434e Binary files /dev/null and b/themes/30coffe/assets/img/home-one/reservation.png differ diff --git a/themes/30coffe/assets/img/home-one/restant.png b/themes/30coffe/assets/img/home-one/restant.png new file mode 100644 index 00000000..2fe00828 Binary files /dev/null and b/themes/30coffe/assets/img/home-one/restant.png differ diff --git a/themes/30coffe/assets/img/home-one/restant2.png b/themes/30coffe/assets/img/home-one/restant2.png new file mode 100644 index 00000000..cb6bac92 Binary files /dev/null and b/themes/30coffe/assets/img/home-one/restant2.png differ diff --git a/themes/30coffe/assets/img/home-one/restant3.png b/themes/30coffe/assets/img/home-one/restant3.png new file mode 100644 index 00000000..30940178 Binary files /dev/null and b/themes/30coffe/assets/img/home-one/restant3.png differ diff --git a/themes/30coffe/assets/img/home-one/restant4.png b/themes/30coffe/assets/img/home-one/restant4.png new file mode 100644 index 00000000..0a58d90a Binary files /dev/null and b/themes/30coffe/assets/img/home-one/restant4.png differ diff --git a/themes/30coffe/assets/img/home-one/restant5.png b/themes/30coffe/assets/img/home-one/restant5.png new file mode 100644 index 00000000..3d657a89 Binary files /dev/null and b/themes/30coffe/assets/img/home-one/restant5.png differ diff --git a/themes/30coffe/assets/img/home-one/review1.png b/themes/30coffe/assets/img/home-one/review1.png new file mode 100644 index 00000000..fdd758cd Binary files /dev/null and b/themes/30coffe/assets/img/home-one/review1.png differ diff --git a/themes/30coffe/assets/img/home-one/review2.png b/themes/30coffe/assets/img/home-one/review2.png new file mode 100644 index 00000000..187d8ec8 Binary files /dev/null and b/themes/30coffe/assets/img/home-one/review2.png differ diff --git a/themes/30coffe/assets/img/home-one/review3.png b/themes/30coffe/assets/img/home-one/review3.png new file mode 100644 index 00000000..a9325e00 Binary files /dev/null and b/themes/30coffe/assets/img/home-one/review3.png differ diff --git a/themes/30coffe/assets/img/home-one/review4.png b/themes/30coffe/assets/img/home-one/review4.png new file mode 100644 index 00000000..8f53b63b Binary files /dev/null and b/themes/30coffe/assets/img/home-one/review4.png differ diff --git a/themes/30coffe/assets/img/home-one/review5.png b/themes/30coffe/assets/img/home-one/review5.png new file mode 100644 index 00000000..e54b55f6 Binary files /dev/null and b/themes/30coffe/assets/img/home-one/review5.png differ diff --git a/themes/30coffe/assets/img/home-one/review6.png b/themes/30coffe/assets/img/home-one/review6.png new file mode 100644 index 00000000..4ea50626 Binary files /dev/null and b/themes/30coffe/assets/img/home-one/review6.png differ diff --git a/themes/30coffe/assets/img/home-one/review7.png b/themes/30coffe/assets/img/home-one/review7.png new file mode 100644 index 00000000..ee064da9 Binary files /dev/null and b/themes/30coffe/assets/img/home-one/review7.png differ diff --git a/themes/30coffe/assets/img/home-one/review8.png b/themes/30coffe/assets/img/home-one/review8.png new file mode 100644 index 00000000..a63c943f Binary files /dev/null and b/themes/30coffe/assets/img/home-one/review8.png differ diff --git a/themes/30coffe/assets/img/home-one/service-shape.png b/themes/30coffe/assets/img/home-one/service-shape.png new file mode 100644 index 00000000..795370d2 Binary files /dev/null and b/themes/30coffe/assets/img/home-one/service-shape.png differ diff --git a/themes/30coffe/assets/img/home-one/service-shape2.png b/themes/30coffe/assets/img/home-one/service-shape2.png new file mode 100644 index 00000000..db364450 Binary files /dev/null and b/themes/30coffe/assets/img/home-one/service-shape2.png differ diff --git a/themes/30coffe/assets/img/home-one/service1.png b/themes/30coffe/assets/img/home-one/service1.png new file mode 100644 index 00000000..f88c5740 Binary files /dev/null and b/themes/30coffe/assets/img/home-one/service1.png differ diff --git a/themes/30coffe/assets/img/home-one/service2.png b/themes/30coffe/assets/img/home-one/service2.png new file mode 100644 index 00000000..c01f72cb Binary files /dev/null and b/themes/30coffe/assets/img/home-one/service2.png differ diff --git a/themes/30coffe/assets/img/home-one/service3.png b/themes/30coffe/assets/img/home-one/service3.png new file mode 100644 index 00000000..c5c1ec75 Binary files /dev/null and b/themes/30coffe/assets/img/home-one/service3.png differ diff --git a/themes/30coffe/assets/img/home-one/service4.png b/themes/30coffe/assets/img/home-one/service4.png new file mode 100644 index 00000000..1ad24178 Binary files /dev/null and b/themes/30coffe/assets/img/home-one/service4.png differ diff --git a/themes/30coffe/assets/img/home-one/service5.png b/themes/30coffe/assets/img/home-one/service5.png new file mode 100644 index 00000000..22c71223 Binary files /dev/null and b/themes/30coffe/assets/img/home-one/service5.png differ diff --git a/themes/30coffe/assets/img/home-one/service6.png b/themes/30coffe/assets/img/home-one/service6.png new file mode 100644 index 00000000..c366c640 Binary files /dev/null and b/themes/30coffe/assets/img/home-one/service6.png differ diff --git a/themes/30coffe/assets/img/home-one/subscribe-main.png b/themes/30coffe/assets/img/home-one/subscribe-main.png new file mode 100644 index 00000000..4fc61d61 Binary files /dev/null and b/themes/30coffe/assets/img/home-one/subscribe-main.png differ diff --git a/themes/30coffe/assets/img/home-three/about1.jpg b/themes/30coffe/assets/img/home-three/about1.jpg new file mode 100644 index 00000000..012284c1 Binary files /dev/null and b/themes/30coffe/assets/img/home-three/about1.jpg differ diff --git a/themes/30coffe/assets/img/home-three/about2.png b/themes/30coffe/assets/img/home-three/about2.png new file mode 100644 index 00000000..3211bbe7 Binary files /dev/null and b/themes/30coffe/assets/img/home-three/about2.png differ diff --git a/themes/30coffe/assets/img/home-three/about3.png b/themes/30coffe/assets/img/home-three/about3.png new file mode 100644 index 00000000..964b0f54 Binary files /dev/null and b/themes/30coffe/assets/img/home-three/about3.png differ diff --git a/themes/30coffe/assets/img/home-three/banner-main.jpg b/themes/30coffe/assets/img/home-three/banner-main.jpg new file mode 100644 index 00000000..4818bbda Binary files /dev/null and b/themes/30coffe/assets/img/home-three/banner-main.jpg differ diff --git a/themes/30coffe/assets/img/home-three/banner1.jpg b/themes/30coffe/assets/img/home-three/banner1.jpg new file mode 100644 index 00000000..78f5025c Binary files /dev/null and b/themes/30coffe/assets/img/home-three/banner1.jpg differ diff --git a/themes/30coffe/assets/img/home-three/banner2.png b/themes/30coffe/assets/img/home-three/banner2.png new file mode 100644 index 00000000..aab92b4a Binary files /dev/null and b/themes/30coffe/assets/img/home-three/banner2.png differ diff --git a/themes/30coffe/assets/img/home-three/banner3.png b/themes/30coffe/assets/img/home-three/banner3.png new file mode 100644 index 00000000..8703a8ac Binary files /dev/null and b/themes/30coffe/assets/img/home-three/banner3.png differ diff --git a/themes/30coffe/assets/img/home-three/banner4.png b/themes/30coffe/assets/img/home-three/banner4.png new file mode 100644 index 00000000..303f5a6e Binary files /dev/null and b/themes/30coffe/assets/img/home-three/banner4.png differ diff --git a/themes/30coffe/assets/img/home-three/service1.png b/themes/30coffe/assets/img/home-three/service1.png new file mode 100644 index 00000000..77138602 Binary files /dev/null and b/themes/30coffe/assets/img/home-three/service1.png differ diff --git a/themes/30coffe/assets/img/home-two/about1.png b/themes/30coffe/assets/img/home-two/about1.png new file mode 100644 index 00000000..92b19680 Binary files /dev/null and b/themes/30coffe/assets/img/home-two/about1.png differ diff --git a/themes/30coffe/assets/img/home-two/about2.png b/themes/30coffe/assets/img/home-two/about2.png new file mode 100644 index 00000000..9dfbf8ca Binary files /dev/null and b/themes/30coffe/assets/img/home-two/about2.png differ diff --git a/themes/30coffe/assets/img/home-two/about3.png b/themes/30coffe/assets/img/home-two/about3.png new file mode 100644 index 00000000..d6395500 Binary files /dev/null and b/themes/30coffe/assets/img/home-two/about3.png differ diff --git a/themes/30coffe/assets/img/home-two/about4.png b/themes/30coffe/assets/img/home-two/about4.png new file mode 100644 index 00000000..7fade5c7 Binary files /dev/null and b/themes/30coffe/assets/img/home-two/about4.png differ diff --git a/themes/30coffe/assets/img/home-two/about5.png b/themes/30coffe/assets/img/home-two/about5.png new file mode 100644 index 00000000..3748fa96 Binary files /dev/null and b/themes/30coffe/assets/img/home-two/about5.png differ diff --git a/themes/30coffe/assets/img/home-two/about6.png b/themes/30coffe/assets/img/home-two/about6.png new file mode 100644 index 00000000..3323d6e1 Binary files /dev/null and b/themes/30coffe/assets/img/home-two/about6.png differ diff --git a/themes/30coffe/assets/img/home-two/about7.png b/themes/30coffe/assets/img/home-two/about7.png new file mode 100644 index 00000000..fc4788cb Binary files /dev/null and b/themes/30coffe/assets/img/home-two/about7.png differ diff --git a/themes/30coffe/assets/img/home-two/app-store.png b/themes/30coffe/assets/img/home-two/app-store.png new file mode 100644 index 00000000..d13e7898 Binary files /dev/null and b/themes/30coffe/assets/img/home-two/app-store.png differ diff --git a/themes/30coffe/assets/img/home-two/banner/1.jpg b/themes/30coffe/assets/img/home-two/banner/1.jpg new file mode 100644 index 00000000..9c32b90f Binary files /dev/null and b/themes/30coffe/assets/img/home-two/banner/1.jpg differ diff --git a/themes/30coffe/assets/img/home-two/banner/1.png b/themes/30coffe/assets/img/home-two/banner/1.png new file mode 100644 index 00000000..9a3416d5 Binary files /dev/null and b/themes/30coffe/assets/img/home-two/banner/1.png differ diff --git a/themes/30coffe/assets/img/home-two/banner/2.png b/themes/30coffe/assets/img/home-two/banner/2.png new file mode 100644 index 00000000..540d0705 Binary files /dev/null and b/themes/30coffe/assets/img/home-two/banner/2.png differ diff --git a/themes/30coffe/assets/img/home-two/banner/3.png b/themes/30coffe/assets/img/home-two/banner/3.png new file mode 100644 index 00000000..e803b909 Binary files /dev/null and b/themes/30coffe/assets/img/home-two/banner/3.png differ diff --git a/themes/30coffe/assets/img/home-two/banner/banner-main.png b/themes/30coffe/assets/img/home-two/banner/banner-main.png new file mode 100644 index 00000000..96a31b27 Binary files /dev/null and b/themes/30coffe/assets/img/home-two/banner/banner-main.png differ diff --git a/themes/30coffe/assets/img/home-two/banner/food1.png b/themes/30coffe/assets/img/home-two/banner/food1.png new file mode 100644 index 00000000..e25a53eb Binary files /dev/null and b/themes/30coffe/assets/img/home-two/banner/food1.png differ diff --git a/themes/30coffe/assets/img/home-two/banner/food2.png b/themes/30coffe/assets/img/home-two/banner/food2.png new file mode 100644 index 00000000..065efe30 Binary files /dev/null and b/themes/30coffe/assets/img/home-two/banner/food2.png differ diff --git a/themes/30coffe/assets/img/home-two/banner/food3.png b/themes/30coffe/assets/img/home-two/banner/food3.png new file mode 100644 index 00000000..8c5c407b Binary files /dev/null and b/themes/30coffe/assets/img/home-two/banner/food3.png differ diff --git a/themes/30coffe/assets/img/home-two/banner/food4.png b/themes/30coffe/assets/img/home-two/banner/food4.png new file mode 100644 index 00000000..490653d8 Binary files /dev/null and b/themes/30coffe/assets/img/home-two/banner/food4.png differ diff --git a/themes/30coffe/assets/img/home-two/banner/food5.png b/themes/30coffe/assets/img/home-two/banner/food5.png new file mode 100644 index 00000000..1b7fb963 Binary files /dev/null and b/themes/30coffe/assets/img/home-two/banner/food5.png differ diff --git a/themes/30coffe/assets/img/home-two/banner/food6.png b/themes/30coffe/assets/img/home-two/banner/food6.png new file mode 100644 index 00000000..f1438ee5 Binary files /dev/null and b/themes/30coffe/assets/img/home-two/banner/food6.png differ diff --git a/themes/30coffe/assets/img/home-two/download1.png b/themes/30coffe/assets/img/home-two/download1.png new file mode 100644 index 00000000..e0f63635 Binary files /dev/null and b/themes/30coffe/assets/img/home-two/download1.png differ diff --git a/themes/30coffe/assets/img/home-two/google-store.png b/themes/30coffe/assets/img/home-two/google-store.png new file mode 100644 index 00000000..62dc2d32 Binary files /dev/null and b/themes/30coffe/assets/img/home-two/google-store.png differ diff --git a/themes/30coffe/assets/img/home-two/join1.png b/themes/30coffe/assets/img/home-two/join1.png new file mode 100644 index 00000000..e43c1396 Binary files /dev/null and b/themes/30coffe/assets/img/home-two/join1.png differ diff --git a/themes/30coffe/assets/img/home-two/review1.jpg b/themes/30coffe/assets/img/home-two/review1.jpg new file mode 100644 index 00000000..eba90b12 Binary files /dev/null and b/themes/30coffe/assets/img/home-two/review1.jpg differ diff --git a/themes/30coffe/assets/img/home-two/review2.png b/themes/30coffe/assets/img/home-two/review2.png new file mode 100644 index 00000000..9fbc65ac Binary files /dev/null and b/themes/30coffe/assets/img/home-two/review2.png differ diff --git a/themes/30coffe/assets/img/logo-two.png b/themes/30coffe/assets/img/logo-two.png new file mode 100644 index 00000000..a95b7570 Binary files /dev/null and b/themes/30coffe/assets/img/logo-two.png differ diff --git a/themes/30coffe/assets/img/logo.png b/themes/30coffe/assets/img/logo.png new file mode 100644 index 00000000..ad24dc12 Binary files /dev/null and b/themes/30coffe/assets/img/logo.png differ diff --git a/themes/30coffe/assets/img/night.png b/themes/30coffe/assets/img/night.png new file mode 100644 index 00000000..dc378383 Binary files /dev/null and b/themes/30coffe/assets/img/night.png differ diff --git a/themes/30coffe/assets/img/service-details/2.jpg b/themes/30coffe/assets/img/service-details/2.jpg new file mode 100644 index 00000000..80bcd483 Binary files /dev/null and b/themes/30coffe/assets/img/service-details/2.jpg differ diff --git a/themes/30coffe/assets/img/service-details/food1.png b/themes/30coffe/assets/img/service-details/food1.png new file mode 100644 index 00000000..e25a53eb Binary files /dev/null and b/themes/30coffe/assets/img/service-details/food1.png differ diff --git a/themes/30coffe/assets/img/service-details/food2.png b/themes/30coffe/assets/img/service-details/food2.png new file mode 100644 index 00000000..d6c535f5 Binary files /dev/null and b/themes/30coffe/assets/img/service-details/food2.png differ diff --git a/themes/30coffe/assets/img/service-details/food3.png b/themes/30coffe/assets/img/service-details/food3.png new file mode 100644 index 00000000..f1438ee5 Binary files /dev/null and b/themes/30coffe/assets/img/service-details/food3.png differ diff --git a/themes/30coffe/assets/img/service-details/food4.png b/themes/30coffe/assets/img/service-details/food4.png new file mode 100644 index 00000000..1b7fb963 Binary files /dev/null and b/themes/30coffe/assets/img/service-details/food4.png differ diff --git a/themes/30coffe/assets/img/service-details/order.png b/themes/30coffe/assets/img/service-details/order.png new file mode 100644 index 00000000..b5ebaa59 Binary files /dev/null and b/themes/30coffe/assets/img/service-details/order.png differ diff --git a/themes/30coffe/assets/img/slider/1.jpg b/themes/30coffe/assets/img/slider/1.jpg new file mode 100644 index 00000000..2d73b2b3 Binary files /dev/null and b/themes/30coffe/assets/img/slider/1.jpg differ diff --git a/themes/30coffe/assets/img/slider/2.jpg b/themes/30coffe/assets/img/slider/2.jpg new file mode 100644 index 00000000..bea479b1 Binary files /dev/null and b/themes/30coffe/assets/img/slider/2.jpg differ diff --git a/themes/30coffe/assets/img/slider/3.jpg b/themes/30coffe/assets/img/slider/3.jpg new file mode 100644 index 00000000..6ee96280 Binary files /dev/null and b/themes/30coffe/assets/img/slider/3.jpg differ diff --git a/themes/30coffe/assets/img/slider/4.jpg b/themes/30coffe/assets/img/slider/4.jpg new file mode 100644 index 00000000..21c917e1 Binary files /dev/null and b/themes/30coffe/assets/img/slider/4.jpg differ diff --git a/themes/30coffe/assets/img/sunny.png b/themes/30coffe/assets/img/sunny.png new file mode 100644 index 00000000..89eabae6 Binary files /dev/null and b/themes/30coffe/assets/img/sunny.png differ diff --git a/themes/30coffe/assets/js/bootstrap.bundle.min.js b/themes/30coffe/assets/js/bootstrap.bundle.min.js new file mode 100644 index 00000000..dbfe7129 --- /dev/null +++ b/themes/30coffe/assets/js/bootstrap.bundle.min.js @@ -0,0 +1,6 @@ +/*! + * Bootstrap v5.1.3 (https://getbootstrap.com/) + * Copyright 2011-2021 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + */ +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).bootstrap=e()}(this,(function(){"use strict";const t="transitionend",e=t=>{let e=t.getAttribute("data-bs-target");if(!e||"#"===e){let i=t.getAttribute("href");if(!i||!i.includes("#")&&!i.startsWith("."))return null;i.includes("#")&&!i.startsWith("#")&&(i=`#${i.split("#")[1]}`),e=i&&"#"!==i?i.trim():null}return e},i=t=>{const i=e(t);return i&&document.querySelector(i)?i:null},n=t=>{const i=e(t);return i?document.querySelector(i):null},s=e=>{e.dispatchEvent(new Event(t))},o=t=>!(!t||"object"!=typeof t)&&(void 0!==t.jquery&&(t=t[0]),void 0!==t.nodeType),r=t=>o(t)?t.jquery?t[0]:t:"string"==typeof t&&t.length>0?document.querySelector(t):null,a=(t,e,i)=>{Object.keys(i).forEach((n=>{const s=i[n],r=e[n],a=r&&o(r)?"element":null==(l=r)?`${l}`:{}.toString.call(l).match(/\s([a-z]+)/i)[1].toLowerCase();var l;if(!new RegExp(s).test(a))throw new TypeError(`${t.toUpperCase()}: Option "${n}" provided type "${a}" but expected type "${s}".`)}))},l=t=>!(!o(t)||0===t.getClientRects().length)&&"visible"===getComputedStyle(t).getPropertyValue("visibility"),c=t=>!t||t.nodeType!==Node.ELEMENT_NODE||!!t.classList.contains("disabled")||(void 0!==t.disabled?t.disabled:t.hasAttribute("disabled")&&"false"!==t.getAttribute("disabled")),h=t=>{if(!document.documentElement.attachShadow)return null;if("function"==typeof t.getRootNode){const e=t.getRootNode();return e instanceof ShadowRoot?e:null}return t instanceof ShadowRoot?t:t.parentNode?h(t.parentNode):null},d=()=>{},u=t=>{t.offsetHeight},f=()=>{const{jQuery:t}=window;return t&&!document.body.hasAttribute("data-bs-no-jquery")?t:null},p=[],m=()=>"rtl"===document.documentElement.dir,g=t=>{var e;e=()=>{const e=f();if(e){const i=t.NAME,n=e.fn[i];e.fn[i]=t.jQueryInterface,e.fn[i].Constructor=t,e.fn[i].noConflict=()=>(e.fn[i]=n,t.jQueryInterface)}},"loading"===document.readyState?(p.length||document.addEventListener("DOMContentLoaded",(()=>{p.forEach((t=>t()))})),p.push(e)):e()},_=t=>{"function"==typeof t&&t()},b=(e,i,n=!0)=>{if(!n)return void _(e);const o=(t=>{if(!t)return 0;let{transitionDuration:e,transitionDelay:i}=window.getComputedStyle(t);const n=Number.parseFloat(e),s=Number.parseFloat(i);return n||s?(e=e.split(",")[0],i=i.split(",")[0],1e3*(Number.parseFloat(e)+Number.parseFloat(i))):0})(i)+5;let r=!1;const a=({target:n})=>{n===i&&(r=!0,i.removeEventListener(t,a),_(e))};i.addEventListener(t,a),setTimeout((()=>{r||s(i)}),o)},v=(t,e,i,n)=>{let s=t.indexOf(e);if(-1===s)return t[!i&&n?t.length-1:0];const o=t.length;return s+=i?1:-1,n&&(s=(s+o)%o),t[Math.max(0,Math.min(s,o-1))]},y=/[^.]*(?=\..*)\.|.*/,w=/\..*/,E=/::\d+$/,A={};let T=1;const O={mouseenter:"mouseover",mouseleave:"mouseout"},C=/^(mouseenter|mouseleave)/i,k=new Set(["click","dblclick","mouseup","mousedown","contextmenu","mousewheel","DOMMouseScroll","mouseover","mouseout","mousemove","selectstart","selectend","keydown","keypress","keyup","orientationchange","touchstart","touchmove","touchend","touchcancel","pointerdown","pointermove","pointerup","pointerleave","pointercancel","gesturestart","gesturechange","gestureend","focus","blur","change","reset","select","submit","focusin","focusout","load","unload","beforeunload","resize","move","DOMContentLoaded","readystatechange","error","abort","scroll"]);function L(t,e){return e&&`${e}::${T++}`||t.uidEvent||T++}function x(t){const e=L(t);return t.uidEvent=e,A[e]=A[e]||{},A[e]}function D(t,e,i=null){const n=Object.keys(t);for(let s=0,o=n.length;sfunction(e){if(!e.relatedTarget||e.relatedTarget!==e.delegateTarget&&!e.delegateTarget.contains(e.relatedTarget))return t.call(this,e)};n?n=t(n):i=t(i)}const[o,r,a]=S(e,i,n),l=x(t),c=l[a]||(l[a]={}),h=D(c,r,o?i:null);if(h)return void(h.oneOff=h.oneOff&&s);const d=L(r,e.replace(y,"")),u=o?function(t,e,i){return function n(s){const o=t.querySelectorAll(e);for(let{target:r}=s;r&&r!==this;r=r.parentNode)for(let a=o.length;a--;)if(o[a]===r)return s.delegateTarget=r,n.oneOff&&j.off(t,s.type,e,i),i.apply(r,[s]);return null}}(t,i,n):function(t,e){return function i(n){return n.delegateTarget=t,i.oneOff&&j.off(t,n.type,e),e.apply(t,[n])}}(t,i);u.delegationSelector=o?i:null,u.originalHandler=r,u.oneOff=s,u.uidEvent=d,c[d]=u,t.addEventListener(a,u,o)}function I(t,e,i,n,s){const o=D(e[i],n,s);o&&(t.removeEventListener(i,o,Boolean(s)),delete e[i][o.uidEvent])}function P(t){return t=t.replace(w,""),O[t]||t}const j={on(t,e,i,n){N(t,e,i,n,!1)},one(t,e,i,n){N(t,e,i,n,!0)},off(t,e,i,n){if("string"!=typeof e||!t)return;const[s,o,r]=S(e,i,n),a=r!==e,l=x(t),c=e.startsWith(".");if(void 0!==o){if(!l||!l[r])return;return void I(t,l,r,o,s?i:null)}c&&Object.keys(l).forEach((i=>{!function(t,e,i,n){const s=e[i]||{};Object.keys(s).forEach((o=>{if(o.includes(n)){const n=s[o];I(t,e,i,n.originalHandler,n.delegationSelector)}}))}(t,l,i,e.slice(1))}));const h=l[r]||{};Object.keys(h).forEach((i=>{const n=i.replace(E,"");if(!a||e.includes(n)){const e=h[i];I(t,l,r,e.originalHandler,e.delegationSelector)}}))},trigger(t,e,i){if("string"!=typeof e||!t)return null;const n=f(),s=P(e),o=e!==s,r=k.has(s);let a,l=!0,c=!0,h=!1,d=null;return o&&n&&(a=n.Event(e,i),n(t).trigger(a),l=!a.isPropagationStopped(),c=!a.isImmediatePropagationStopped(),h=a.isDefaultPrevented()),r?(d=document.createEvent("HTMLEvents"),d.initEvent(s,l,!0)):d=new CustomEvent(e,{bubbles:l,cancelable:!0}),void 0!==i&&Object.keys(i).forEach((t=>{Object.defineProperty(d,t,{get:()=>i[t]})})),h&&d.preventDefault(),c&&t.dispatchEvent(d),d.defaultPrevented&&void 0!==a&&a.preventDefault(),d}},M=new Map,H={set(t,e,i){M.has(t)||M.set(t,new Map);const n=M.get(t);n.has(e)||0===n.size?n.set(e,i):console.error(`Bootstrap doesn't allow more than one instance per element. Bound instance: ${Array.from(n.keys())[0]}.`)},get:(t,e)=>M.has(t)&&M.get(t).get(e)||null,remove(t,e){if(!M.has(t))return;const i=M.get(t);i.delete(e),0===i.size&&M.delete(t)}};class B{constructor(t){(t=r(t))&&(this._element=t,H.set(this._element,this.constructor.DATA_KEY,this))}dispose(){H.remove(this._element,this.constructor.DATA_KEY),j.off(this._element,this.constructor.EVENT_KEY),Object.getOwnPropertyNames(this).forEach((t=>{this[t]=null}))}_queueCallback(t,e,i=!0){b(t,e,i)}static getInstance(t){return H.get(r(t),this.DATA_KEY)}static getOrCreateInstance(t,e={}){return this.getInstance(t)||new this(t,"object"==typeof e?e:null)}static get VERSION(){return"5.1.3"}static get NAME(){throw new Error('You have to implement the static method "NAME", for each component!')}static get DATA_KEY(){return`bs.${this.NAME}`}static get EVENT_KEY(){return`.${this.DATA_KEY}`}}const R=(t,e="hide")=>{const i=`click.dismiss${t.EVENT_KEY}`,s=t.NAME;j.on(document,i,`[data-bs-dismiss="${s}"]`,(function(i){if(["A","AREA"].includes(this.tagName)&&i.preventDefault(),c(this))return;const o=n(this)||this.closest(`.${s}`);t.getOrCreateInstance(o)[e]()}))};class W extends B{static get NAME(){return"alert"}close(){if(j.trigger(this._element,"close.bs.alert").defaultPrevented)return;this._element.classList.remove("show");const t=this._element.classList.contains("fade");this._queueCallback((()=>this._destroyElement()),this._element,t)}_destroyElement(){this._element.remove(),j.trigger(this._element,"closed.bs.alert"),this.dispose()}static jQueryInterface(t){return this.each((function(){const e=W.getOrCreateInstance(this);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}R(W,"close"),g(W);const $='[data-bs-toggle="button"]';class z extends B{static get NAME(){return"button"}toggle(){this._element.setAttribute("aria-pressed",this._element.classList.toggle("active"))}static jQueryInterface(t){return this.each((function(){const e=z.getOrCreateInstance(this);"toggle"===t&&e[t]()}))}}function q(t){return"true"===t||"false"!==t&&(t===Number(t).toString()?Number(t):""===t||"null"===t?null:t)}function F(t){return t.replace(/[A-Z]/g,(t=>`-${t.toLowerCase()}`))}j.on(document,"click.bs.button.data-api",$,(t=>{t.preventDefault();const e=t.target.closest($);z.getOrCreateInstance(e).toggle()})),g(z);const U={setDataAttribute(t,e,i){t.setAttribute(`data-bs-${F(e)}`,i)},removeDataAttribute(t,e){t.removeAttribute(`data-bs-${F(e)}`)},getDataAttributes(t){if(!t)return{};const e={};return Object.keys(t.dataset).filter((t=>t.startsWith("bs"))).forEach((i=>{let n=i.replace(/^bs/,"");n=n.charAt(0).toLowerCase()+n.slice(1,n.length),e[n]=q(t.dataset[i])})),e},getDataAttribute:(t,e)=>q(t.getAttribute(`data-bs-${F(e)}`)),offset(t){const e=t.getBoundingClientRect();return{top:e.top+window.pageYOffset,left:e.left+window.pageXOffset}},position:t=>({top:t.offsetTop,left:t.offsetLeft})},V={find:(t,e=document.documentElement)=>[].concat(...Element.prototype.querySelectorAll.call(e,t)),findOne:(t,e=document.documentElement)=>Element.prototype.querySelector.call(e,t),children:(t,e)=>[].concat(...t.children).filter((t=>t.matches(e))),parents(t,e){const i=[];let n=t.parentNode;for(;n&&n.nodeType===Node.ELEMENT_NODE&&3!==n.nodeType;)n.matches(e)&&i.push(n),n=n.parentNode;return i},prev(t,e){let i=t.previousElementSibling;for(;i;){if(i.matches(e))return[i];i=i.previousElementSibling}return[]},next(t,e){let i=t.nextElementSibling;for(;i;){if(i.matches(e))return[i];i=i.nextElementSibling}return[]},focusableChildren(t){const e=["a","button","input","textarea","select","details","[tabindex]",'[contenteditable="true"]'].map((t=>`${t}:not([tabindex^="-"])`)).join(", ");return this.find(e,t).filter((t=>!c(t)&&l(t)))}},K="carousel",X={interval:5e3,keyboard:!0,slide:!1,pause:"hover",wrap:!0,touch:!0},Y={interval:"(number|boolean)",keyboard:"boolean",slide:"(boolean|string)",pause:"(string|boolean)",wrap:"boolean",touch:"boolean"},Q="next",G="prev",Z="left",J="right",tt={ArrowLeft:J,ArrowRight:Z},et="slid.bs.carousel",it="active",nt=".active.carousel-item";class st extends B{constructor(t,e){super(t),this._items=null,this._interval=null,this._activeElement=null,this._isPaused=!1,this._isSliding=!1,this.touchTimeout=null,this.touchStartX=0,this.touchDeltaX=0,this._config=this._getConfig(e),this._indicatorsElement=V.findOne(".carousel-indicators",this._element),this._touchSupported="ontouchstart"in document.documentElement||navigator.maxTouchPoints>0,this._pointerEvent=Boolean(window.PointerEvent),this._addEventListeners()}static get Default(){return X}static get NAME(){return K}next(){this._slide(Q)}nextWhenVisible(){!document.hidden&&l(this._element)&&this.next()}prev(){this._slide(G)}pause(t){t||(this._isPaused=!0),V.findOne(".carousel-item-next, .carousel-item-prev",this._element)&&(s(this._element),this.cycle(!0)),clearInterval(this._interval),this._interval=null}cycle(t){t||(this._isPaused=!1),this._interval&&(clearInterval(this._interval),this._interval=null),this._config&&this._config.interval&&!this._isPaused&&(this._updateInterval(),this._interval=setInterval((document.visibilityState?this.nextWhenVisible:this.next).bind(this),this._config.interval))}to(t){this._activeElement=V.findOne(nt,this._element);const e=this._getItemIndex(this._activeElement);if(t>this._items.length-1||t<0)return;if(this._isSliding)return void j.one(this._element,et,(()=>this.to(t)));if(e===t)return this.pause(),void this.cycle();const i=t>e?Q:G;this._slide(i,this._items[t])}_getConfig(t){return t={...X,...U.getDataAttributes(this._element),..."object"==typeof t?t:{}},a(K,t,Y),t}_handleSwipe(){const t=Math.abs(this.touchDeltaX);if(t<=40)return;const e=t/this.touchDeltaX;this.touchDeltaX=0,e&&this._slide(e>0?J:Z)}_addEventListeners(){this._config.keyboard&&j.on(this._element,"keydown.bs.carousel",(t=>this._keydown(t))),"hover"===this._config.pause&&(j.on(this._element,"mouseenter.bs.carousel",(t=>this.pause(t))),j.on(this._element,"mouseleave.bs.carousel",(t=>this.cycle(t)))),this._config.touch&&this._touchSupported&&this._addTouchEventListeners()}_addTouchEventListeners(){const t=t=>this._pointerEvent&&("pen"===t.pointerType||"touch"===t.pointerType),e=e=>{t(e)?this.touchStartX=e.clientX:this._pointerEvent||(this.touchStartX=e.touches[0].clientX)},i=t=>{this.touchDeltaX=t.touches&&t.touches.length>1?0:t.touches[0].clientX-this.touchStartX},n=e=>{t(e)&&(this.touchDeltaX=e.clientX-this.touchStartX),this._handleSwipe(),"hover"===this._config.pause&&(this.pause(),this.touchTimeout&&clearTimeout(this.touchTimeout),this.touchTimeout=setTimeout((t=>this.cycle(t)),500+this._config.interval))};V.find(".carousel-item img",this._element).forEach((t=>{j.on(t,"dragstart.bs.carousel",(t=>t.preventDefault()))})),this._pointerEvent?(j.on(this._element,"pointerdown.bs.carousel",(t=>e(t))),j.on(this._element,"pointerup.bs.carousel",(t=>n(t))),this._element.classList.add("pointer-event")):(j.on(this._element,"touchstart.bs.carousel",(t=>e(t))),j.on(this._element,"touchmove.bs.carousel",(t=>i(t))),j.on(this._element,"touchend.bs.carousel",(t=>n(t))))}_keydown(t){if(/input|textarea/i.test(t.target.tagName))return;const e=tt[t.key];e&&(t.preventDefault(),this._slide(e))}_getItemIndex(t){return this._items=t&&t.parentNode?V.find(".carousel-item",t.parentNode):[],this._items.indexOf(t)}_getItemByOrder(t,e){const i=t===Q;return v(this._items,e,i,this._config.wrap)}_triggerSlideEvent(t,e){const i=this._getItemIndex(t),n=this._getItemIndex(V.findOne(nt,this._element));return j.trigger(this._element,"slide.bs.carousel",{relatedTarget:t,direction:e,from:n,to:i})}_setActiveIndicatorElement(t){if(this._indicatorsElement){const e=V.findOne(".active",this._indicatorsElement);e.classList.remove(it),e.removeAttribute("aria-current");const i=V.find("[data-bs-target]",this._indicatorsElement);for(let e=0;e{j.trigger(this._element,et,{relatedTarget:o,direction:d,from:s,to:r})};if(this._element.classList.contains("slide")){o.classList.add(h),u(o),n.classList.add(c),o.classList.add(c);const t=()=>{o.classList.remove(c,h),o.classList.add(it),n.classList.remove(it,h,c),this._isSliding=!1,setTimeout(f,0)};this._queueCallback(t,n,!0)}else n.classList.remove(it),o.classList.add(it),this._isSliding=!1,f();a&&this.cycle()}_directionToOrder(t){return[J,Z].includes(t)?m()?t===Z?G:Q:t===Z?Q:G:t}_orderToDirection(t){return[Q,G].includes(t)?m()?t===G?Z:J:t===G?J:Z:t}static carouselInterface(t,e){const i=st.getOrCreateInstance(t,e);let{_config:n}=i;"object"==typeof e&&(n={...n,...e});const s="string"==typeof e?e:n.slide;if("number"==typeof e)i.to(e);else if("string"==typeof s){if(void 0===i[s])throw new TypeError(`No method named "${s}"`);i[s]()}else n.interval&&n.ride&&(i.pause(),i.cycle())}static jQueryInterface(t){return this.each((function(){st.carouselInterface(this,t)}))}static dataApiClickHandler(t){const e=n(this);if(!e||!e.classList.contains("carousel"))return;const i={...U.getDataAttributes(e),...U.getDataAttributes(this)},s=this.getAttribute("data-bs-slide-to");s&&(i.interval=!1),st.carouselInterface(e,i),s&&st.getInstance(e).to(s),t.preventDefault()}}j.on(document,"click.bs.carousel.data-api","[data-bs-slide], [data-bs-slide-to]",st.dataApiClickHandler),j.on(window,"load.bs.carousel.data-api",(()=>{const t=V.find('[data-bs-ride="carousel"]');for(let e=0,i=t.length;et===this._element));null!==s&&o.length&&(this._selector=s,this._triggerArray.push(e))}this._initializeChildren(),this._config.parent||this._addAriaAndCollapsedClass(this._triggerArray,this._isShown()),this._config.toggle&&this.toggle()}static get Default(){return rt}static get NAME(){return ot}toggle(){this._isShown()?this.hide():this.show()}show(){if(this._isTransitioning||this._isShown())return;let t,e=[];if(this._config.parent){const t=V.find(ut,this._config.parent);e=V.find(".collapse.show, .collapse.collapsing",this._config.parent).filter((e=>!t.includes(e)))}const i=V.findOne(this._selector);if(e.length){const n=e.find((t=>i!==t));if(t=n?pt.getInstance(n):null,t&&t._isTransitioning)return}if(j.trigger(this._element,"show.bs.collapse").defaultPrevented)return;e.forEach((e=>{i!==e&&pt.getOrCreateInstance(e,{toggle:!1}).hide(),t||H.set(e,"bs.collapse",null)}));const n=this._getDimension();this._element.classList.remove(ct),this._element.classList.add(ht),this._element.style[n]=0,this._addAriaAndCollapsedClass(this._triggerArray,!0),this._isTransitioning=!0;const s=`scroll${n[0].toUpperCase()+n.slice(1)}`;this._queueCallback((()=>{this._isTransitioning=!1,this._element.classList.remove(ht),this._element.classList.add(ct,lt),this._element.style[n]="",j.trigger(this._element,"shown.bs.collapse")}),this._element,!0),this._element.style[n]=`${this._element[s]}px`}hide(){if(this._isTransitioning||!this._isShown())return;if(j.trigger(this._element,"hide.bs.collapse").defaultPrevented)return;const t=this._getDimension();this._element.style[t]=`${this._element.getBoundingClientRect()[t]}px`,u(this._element),this._element.classList.add(ht),this._element.classList.remove(ct,lt);const e=this._triggerArray.length;for(let t=0;t{this._isTransitioning=!1,this._element.classList.remove(ht),this._element.classList.add(ct),j.trigger(this._element,"hidden.bs.collapse")}),this._element,!0)}_isShown(t=this._element){return t.classList.contains(lt)}_getConfig(t){return(t={...rt,...U.getDataAttributes(this._element),...t}).toggle=Boolean(t.toggle),t.parent=r(t.parent),a(ot,t,at),t}_getDimension(){return this._element.classList.contains("collapse-horizontal")?"width":"height"}_initializeChildren(){if(!this._config.parent)return;const t=V.find(ut,this._config.parent);V.find(ft,this._config.parent).filter((e=>!t.includes(e))).forEach((t=>{const e=n(t);e&&this._addAriaAndCollapsedClass([t],this._isShown(e))}))}_addAriaAndCollapsedClass(t,e){t.length&&t.forEach((t=>{e?t.classList.remove(dt):t.classList.add(dt),t.setAttribute("aria-expanded",e)}))}static jQueryInterface(t){return this.each((function(){const e={};"string"==typeof t&&/show|hide/.test(t)&&(e.toggle=!1);const i=pt.getOrCreateInstance(this,e);if("string"==typeof t){if(void 0===i[t])throw new TypeError(`No method named "${t}"`);i[t]()}}))}}j.on(document,"click.bs.collapse.data-api",ft,(function(t){("A"===t.target.tagName||t.delegateTarget&&"A"===t.delegateTarget.tagName)&&t.preventDefault();const e=i(this);V.find(e).forEach((t=>{pt.getOrCreateInstance(t,{toggle:!1}).toggle()}))})),g(pt);var mt="top",gt="bottom",_t="right",bt="left",vt="auto",yt=[mt,gt,_t,bt],wt="start",Et="end",At="clippingParents",Tt="viewport",Ot="popper",Ct="reference",kt=yt.reduce((function(t,e){return t.concat([e+"-"+wt,e+"-"+Et])}),[]),Lt=[].concat(yt,[vt]).reduce((function(t,e){return t.concat([e,e+"-"+wt,e+"-"+Et])}),[]),xt="beforeRead",Dt="read",St="afterRead",Nt="beforeMain",It="main",Pt="afterMain",jt="beforeWrite",Mt="write",Ht="afterWrite",Bt=[xt,Dt,St,Nt,It,Pt,jt,Mt,Ht];function Rt(t){return t?(t.nodeName||"").toLowerCase():null}function Wt(t){if(null==t)return window;if("[object Window]"!==t.toString()){var e=t.ownerDocument;return e&&e.defaultView||window}return t}function $t(t){return t instanceof Wt(t).Element||t instanceof Element}function zt(t){return t instanceof Wt(t).HTMLElement||t instanceof HTMLElement}function qt(t){return"undefined"!=typeof ShadowRoot&&(t instanceof Wt(t).ShadowRoot||t instanceof ShadowRoot)}const Ft={name:"applyStyles",enabled:!0,phase:"write",fn:function(t){var e=t.state;Object.keys(e.elements).forEach((function(t){var i=e.styles[t]||{},n=e.attributes[t]||{},s=e.elements[t];zt(s)&&Rt(s)&&(Object.assign(s.style,i),Object.keys(n).forEach((function(t){var e=n[t];!1===e?s.removeAttribute(t):s.setAttribute(t,!0===e?"":e)})))}))},effect:function(t){var e=t.state,i={popper:{position:e.options.strategy,left:"0",top:"0",margin:"0"},arrow:{position:"absolute"},reference:{}};return Object.assign(e.elements.popper.style,i.popper),e.styles=i,e.elements.arrow&&Object.assign(e.elements.arrow.style,i.arrow),function(){Object.keys(e.elements).forEach((function(t){var n=e.elements[t],s=e.attributes[t]||{},o=Object.keys(e.styles.hasOwnProperty(t)?e.styles[t]:i[t]).reduce((function(t,e){return t[e]="",t}),{});zt(n)&&Rt(n)&&(Object.assign(n.style,o),Object.keys(s).forEach((function(t){n.removeAttribute(t)})))}))}},requires:["computeStyles"]};function Ut(t){return t.split("-")[0]}function Vt(t,e){var i=t.getBoundingClientRect();return{width:i.width/1,height:i.height/1,top:i.top/1,right:i.right/1,bottom:i.bottom/1,left:i.left/1,x:i.left/1,y:i.top/1}}function Kt(t){var e=Vt(t),i=t.offsetWidth,n=t.offsetHeight;return Math.abs(e.width-i)<=1&&(i=e.width),Math.abs(e.height-n)<=1&&(n=e.height),{x:t.offsetLeft,y:t.offsetTop,width:i,height:n}}function Xt(t,e){var i=e.getRootNode&&e.getRootNode();if(t.contains(e))return!0;if(i&&qt(i)){var n=e;do{if(n&&t.isSameNode(n))return!0;n=n.parentNode||n.host}while(n)}return!1}function Yt(t){return Wt(t).getComputedStyle(t)}function Qt(t){return["table","td","th"].indexOf(Rt(t))>=0}function Gt(t){return(($t(t)?t.ownerDocument:t.document)||window.document).documentElement}function Zt(t){return"html"===Rt(t)?t:t.assignedSlot||t.parentNode||(qt(t)?t.host:null)||Gt(t)}function Jt(t){return zt(t)&&"fixed"!==Yt(t).position?t.offsetParent:null}function te(t){for(var e=Wt(t),i=Jt(t);i&&Qt(i)&&"static"===Yt(i).position;)i=Jt(i);return i&&("html"===Rt(i)||"body"===Rt(i)&&"static"===Yt(i).position)?e:i||function(t){var e=-1!==navigator.userAgent.toLowerCase().indexOf("firefox");if(-1!==navigator.userAgent.indexOf("Trident")&&zt(t)&&"fixed"===Yt(t).position)return null;for(var i=Zt(t);zt(i)&&["html","body"].indexOf(Rt(i))<0;){var n=Yt(i);if("none"!==n.transform||"none"!==n.perspective||"paint"===n.contain||-1!==["transform","perspective"].indexOf(n.willChange)||e&&"filter"===n.willChange||e&&n.filter&&"none"!==n.filter)return i;i=i.parentNode}return null}(t)||e}function ee(t){return["top","bottom"].indexOf(t)>=0?"x":"y"}var ie=Math.max,ne=Math.min,se=Math.round;function oe(t,e,i){return ie(t,ne(e,i))}function re(t){return Object.assign({},{top:0,right:0,bottom:0,left:0},t)}function ae(t,e){return e.reduce((function(e,i){return e[i]=t,e}),{})}const le={name:"arrow",enabled:!0,phase:"main",fn:function(t){var e,i=t.state,n=t.name,s=t.options,o=i.elements.arrow,r=i.modifiersData.popperOffsets,a=Ut(i.placement),l=ee(a),c=[bt,_t].indexOf(a)>=0?"height":"width";if(o&&r){var h=function(t,e){return re("number"!=typeof(t="function"==typeof t?t(Object.assign({},e.rects,{placement:e.placement})):t)?t:ae(t,yt))}(s.padding,i),d=Kt(o),u="y"===l?mt:bt,f="y"===l?gt:_t,p=i.rects.reference[c]+i.rects.reference[l]-r[l]-i.rects.popper[c],m=r[l]-i.rects.reference[l],g=te(o),_=g?"y"===l?g.clientHeight||0:g.clientWidth||0:0,b=p/2-m/2,v=h[u],y=_-d[c]-h[f],w=_/2-d[c]/2+b,E=oe(v,w,y),A=l;i.modifiersData[n]=((e={})[A]=E,e.centerOffset=E-w,e)}},effect:function(t){var e=t.state,i=t.options.element,n=void 0===i?"[data-popper-arrow]":i;null!=n&&("string"!=typeof n||(n=e.elements.popper.querySelector(n)))&&Xt(e.elements.popper,n)&&(e.elements.arrow=n)},requires:["popperOffsets"],requiresIfExists:["preventOverflow"]};function ce(t){return t.split("-")[1]}var he={top:"auto",right:"auto",bottom:"auto",left:"auto"};function de(t){var e,i=t.popper,n=t.popperRect,s=t.placement,o=t.variation,r=t.offsets,a=t.position,l=t.gpuAcceleration,c=t.adaptive,h=t.roundOffsets,d=!0===h?function(t){var e=t.x,i=t.y,n=window.devicePixelRatio||1;return{x:se(se(e*n)/n)||0,y:se(se(i*n)/n)||0}}(r):"function"==typeof h?h(r):r,u=d.x,f=void 0===u?0:u,p=d.y,m=void 0===p?0:p,g=r.hasOwnProperty("x"),_=r.hasOwnProperty("y"),b=bt,v=mt,y=window;if(c){var w=te(i),E="clientHeight",A="clientWidth";w===Wt(i)&&"static"!==Yt(w=Gt(i)).position&&"absolute"===a&&(E="scrollHeight",A="scrollWidth"),w=w,s!==mt&&(s!==bt&&s!==_t||o!==Et)||(v=gt,m-=w[E]-n.height,m*=l?1:-1),s!==bt&&(s!==mt&&s!==gt||o!==Et)||(b=_t,f-=w[A]-n.width,f*=l?1:-1)}var T,O=Object.assign({position:a},c&&he);return l?Object.assign({},O,((T={})[v]=_?"0":"",T[b]=g?"0":"",T.transform=(y.devicePixelRatio||1)<=1?"translate("+f+"px, "+m+"px)":"translate3d("+f+"px, "+m+"px, 0)",T)):Object.assign({},O,((e={})[v]=_?m+"px":"",e[b]=g?f+"px":"",e.transform="",e))}const ue={name:"computeStyles",enabled:!0,phase:"beforeWrite",fn:function(t){var e=t.state,i=t.options,n=i.gpuAcceleration,s=void 0===n||n,o=i.adaptive,r=void 0===o||o,a=i.roundOffsets,l=void 0===a||a,c={placement:Ut(e.placement),variation:ce(e.placement),popper:e.elements.popper,popperRect:e.rects.popper,gpuAcceleration:s};null!=e.modifiersData.popperOffsets&&(e.styles.popper=Object.assign({},e.styles.popper,de(Object.assign({},c,{offsets:e.modifiersData.popperOffsets,position:e.options.strategy,adaptive:r,roundOffsets:l})))),null!=e.modifiersData.arrow&&(e.styles.arrow=Object.assign({},e.styles.arrow,de(Object.assign({},c,{offsets:e.modifiersData.arrow,position:"absolute",adaptive:!1,roundOffsets:l})))),e.attributes.popper=Object.assign({},e.attributes.popper,{"data-popper-placement":e.placement})},data:{}};var fe={passive:!0};const pe={name:"eventListeners",enabled:!0,phase:"write",fn:function(){},effect:function(t){var e=t.state,i=t.instance,n=t.options,s=n.scroll,o=void 0===s||s,r=n.resize,a=void 0===r||r,l=Wt(e.elements.popper),c=[].concat(e.scrollParents.reference,e.scrollParents.popper);return o&&c.forEach((function(t){t.addEventListener("scroll",i.update,fe)})),a&&l.addEventListener("resize",i.update,fe),function(){o&&c.forEach((function(t){t.removeEventListener("scroll",i.update,fe)})),a&&l.removeEventListener("resize",i.update,fe)}},data:{}};var me={left:"right",right:"left",bottom:"top",top:"bottom"};function ge(t){return t.replace(/left|right|bottom|top/g,(function(t){return me[t]}))}var _e={start:"end",end:"start"};function be(t){return t.replace(/start|end/g,(function(t){return _e[t]}))}function ve(t){var e=Wt(t);return{scrollLeft:e.pageXOffset,scrollTop:e.pageYOffset}}function ye(t){return Vt(Gt(t)).left+ve(t).scrollLeft}function we(t){var e=Yt(t),i=e.overflow,n=e.overflowX,s=e.overflowY;return/auto|scroll|overlay|hidden/.test(i+s+n)}function Ee(t){return["html","body","#document"].indexOf(Rt(t))>=0?t.ownerDocument.body:zt(t)&&we(t)?t:Ee(Zt(t))}function Ae(t,e){var i;void 0===e&&(e=[]);var n=Ee(t),s=n===(null==(i=t.ownerDocument)?void 0:i.body),o=Wt(n),r=s?[o].concat(o.visualViewport||[],we(n)?n:[]):n,a=e.concat(r);return s?a:a.concat(Ae(Zt(r)))}function Te(t){return Object.assign({},t,{left:t.x,top:t.y,right:t.x+t.width,bottom:t.y+t.height})}function Oe(t,e){return e===Tt?Te(function(t){var e=Wt(t),i=Gt(t),n=e.visualViewport,s=i.clientWidth,o=i.clientHeight,r=0,a=0;return n&&(s=n.width,o=n.height,/^((?!chrome|android).)*safari/i.test(navigator.userAgent)||(r=n.offsetLeft,a=n.offsetTop)),{width:s,height:o,x:r+ye(t),y:a}}(t)):zt(e)?function(t){var e=Vt(t);return e.top=e.top+t.clientTop,e.left=e.left+t.clientLeft,e.bottom=e.top+t.clientHeight,e.right=e.left+t.clientWidth,e.width=t.clientWidth,e.height=t.clientHeight,e.x=e.left,e.y=e.top,e}(e):Te(function(t){var e,i=Gt(t),n=ve(t),s=null==(e=t.ownerDocument)?void 0:e.body,o=ie(i.scrollWidth,i.clientWidth,s?s.scrollWidth:0,s?s.clientWidth:0),r=ie(i.scrollHeight,i.clientHeight,s?s.scrollHeight:0,s?s.clientHeight:0),a=-n.scrollLeft+ye(t),l=-n.scrollTop;return"rtl"===Yt(s||i).direction&&(a+=ie(i.clientWidth,s?s.clientWidth:0)-o),{width:o,height:r,x:a,y:l}}(Gt(t)))}function Ce(t){var e,i=t.reference,n=t.element,s=t.placement,o=s?Ut(s):null,r=s?ce(s):null,a=i.x+i.width/2-n.width/2,l=i.y+i.height/2-n.height/2;switch(o){case mt:e={x:a,y:i.y-n.height};break;case gt:e={x:a,y:i.y+i.height};break;case _t:e={x:i.x+i.width,y:l};break;case bt:e={x:i.x-n.width,y:l};break;default:e={x:i.x,y:i.y}}var c=o?ee(o):null;if(null!=c){var h="y"===c?"height":"width";switch(r){case wt:e[c]=e[c]-(i[h]/2-n[h]/2);break;case Et:e[c]=e[c]+(i[h]/2-n[h]/2)}}return e}function ke(t,e){void 0===e&&(e={});var i=e,n=i.placement,s=void 0===n?t.placement:n,o=i.boundary,r=void 0===o?At:o,a=i.rootBoundary,l=void 0===a?Tt:a,c=i.elementContext,h=void 0===c?Ot:c,d=i.altBoundary,u=void 0!==d&&d,f=i.padding,p=void 0===f?0:f,m=re("number"!=typeof p?p:ae(p,yt)),g=h===Ot?Ct:Ot,_=t.rects.popper,b=t.elements[u?g:h],v=function(t,e,i){var n="clippingParents"===e?function(t){var e=Ae(Zt(t)),i=["absolute","fixed"].indexOf(Yt(t).position)>=0&&zt(t)?te(t):t;return $t(i)?e.filter((function(t){return $t(t)&&Xt(t,i)&&"body"!==Rt(t)})):[]}(t):[].concat(e),s=[].concat(n,[i]),o=s[0],r=s.reduce((function(e,i){var n=Oe(t,i);return e.top=ie(n.top,e.top),e.right=ne(n.right,e.right),e.bottom=ne(n.bottom,e.bottom),e.left=ie(n.left,e.left),e}),Oe(t,o));return r.width=r.right-r.left,r.height=r.bottom-r.top,r.x=r.left,r.y=r.top,r}($t(b)?b:b.contextElement||Gt(t.elements.popper),r,l),y=Vt(t.elements.reference),w=Ce({reference:y,element:_,strategy:"absolute",placement:s}),E=Te(Object.assign({},_,w)),A=h===Ot?E:y,T={top:v.top-A.top+m.top,bottom:A.bottom-v.bottom+m.bottom,left:v.left-A.left+m.left,right:A.right-v.right+m.right},O=t.modifiersData.offset;if(h===Ot&&O){var C=O[s];Object.keys(T).forEach((function(t){var e=[_t,gt].indexOf(t)>=0?1:-1,i=[mt,gt].indexOf(t)>=0?"y":"x";T[t]+=C[i]*e}))}return T}function Le(t,e){void 0===e&&(e={});var i=e,n=i.placement,s=i.boundary,o=i.rootBoundary,r=i.padding,a=i.flipVariations,l=i.allowedAutoPlacements,c=void 0===l?Lt:l,h=ce(n),d=h?a?kt:kt.filter((function(t){return ce(t)===h})):yt,u=d.filter((function(t){return c.indexOf(t)>=0}));0===u.length&&(u=d);var f=u.reduce((function(e,i){return e[i]=ke(t,{placement:i,boundary:s,rootBoundary:o,padding:r})[Ut(i)],e}),{});return Object.keys(f).sort((function(t,e){return f[t]-f[e]}))}const xe={name:"flip",enabled:!0,phase:"main",fn:function(t){var e=t.state,i=t.options,n=t.name;if(!e.modifiersData[n]._skip){for(var s=i.mainAxis,o=void 0===s||s,r=i.altAxis,a=void 0===r||r,l=i.fallbackPlacements,c=i.padding,h=i.boundary,d=i.rootBoundary,u=i.altBoundary,f=i.flipVariations,p=void 0===f||f,m=i.allowedAutoPlacements,g=e.options.placement,_=Ut(g),b=l||(_!==g&&p?function(t){if(Ut(t)===vt)return[];var e=ge(t);return[be(t),e,be(e)]}(g):[ge(g)]),v=[g].concat(b).reduce((function(t,i){return t.concat(Ut(i)===vt?Le(e,{placement:i,boundary:h,rootBoundary:d,padding:c,flipVariations:p,allowedAutoPlacements:m}):i)}),[]),y=e.rects.reference,w=e.rects.popper,E=new Map,A=!0,T=v[0],O=0;O=0,D=x?"width":"height",S=ke(e,{placement:C,boundary:h,rootBoundary:d,altBoundary:u,padding:c}),N=x?L?_t:bt:L?gt:mt;y[D]>w[D]&&(N=ge(N));var I=ge(N),P=[];if(o&&P.push(S[k]<=0),a&&P.push(S[N]<=0,S[I]<=0),P.every((function(t){return t}))){T=C,A=!1;break}E.set(C,P)}if(A)for(var j=function(t){var e=v.find((function(e){var i=E.get(e);if(i)return i.slice(0,t).every((function(t){return t}))}));if(e)return T=e,"break"},M=p?3:1;M>0&&"break"!==j(M);M--);e.placement!==T&&(e.modifiersData[n]._skip=!0,e.placement=T,e.reset=!0)}},requiresIfExists:["offset"],data:{_skip:!1}};function De(t,e,i){return void 0===i&&(i={x:0,y:0}),{top:t.top-e.height-i.y,right:t.right-e.width+i.x,bottom:t.bottom-e.height+i.y,left:t.left-e.width-i.x}}function Se(t){return[mt,_t,gt,bt].some((function(e){return t[e]>=0}))}const Ne={name:"hide",enabled:!0,phase:"main",requiresIfExists:["preventOverflow"],fn:function(t){var e=t.state,i=t.name,n=e.rects.reference,s=e.rects.popper,o=e.modifiersData.preventOverflow,r=ke(e,{elementContext:"reference"}),a=ke(e,{altBoundary:!0}),l=De(r,n),c=De(a,s,o),h=Se(l),d=Se(c);e.modifiersData[i]={referenceClippingOffsets:l,popperEscapeOffsets:c,isReferenceHidden:h,hasPopperEscaped:d},e.attributes.popper=Object.assign({},e.attributes.popper,{"data-popper-reference-hidden":h,"data-popper-escaped":d})}},Ie={name:"offset",enabled:!0,phase:"main",requires:["popperOffsets"],fn:function(t){var e=t.state,i=t.options,n=t.name,s=i.offset,o=void 0===s?[0,0]:s,r=Lt.reduce((function(t,i){return t[i]=function(t,e,i){var n=Ut(t),s=[bt,mt].indexOf(n)>=0?-1:1,o="function"==typeof i?i(Object.assign({},e,{placement:t})):i,r=o[0],a=o[1];return r=r||0,a=(a||0)*s,[bt,_t].indexOf(n)>=0?{x:a,y:r}:{x:r,y:a}}(i,e.rects,o),t}),{}),a=r[e.placement],l=a.x,c=a.y;null!=e.modifiersData.popperOffsets&&(e.modifiersData.popperOffsets.x+=l,e.modifiersData.popperOffsets.y+=c),e.modifiersData[n]=r}},Pe={name:"popperOffsets",enabled:!0,phase:"read",fn:function(t){var e=t.state,i=t.name;e.modifiersData[i]=Ce({reference:e.rects.reference,element:e.rects.popper,strategy:"absolute",placement:e.placement})},data:{}},je={name:"preventOverflow",enabled:!0,phase:"main",fn:function(t){var e=t.state,i=t.options,n=t.name,s=i.mainAxis,o=void 0===s||s,r=i.altAxis,a=void 0!==r&&r,l=i.boundary,c=i.rootBoundary,h=i.altBoundary,d=i.padding,u=i.tether,f=void 0===u||u,p=i.tetherOffset,m=void 0===p?0:p,g=ke(e,{boundary:l,rootBoundary:c,padding:d,altBoundary:h}),_=Ut(e.placement),b=ce(e.placement),v=!b,y=ee(_),w="x"===y?"y":"x",E=e.modifiersData.popperOffsets,A=e.rects.reference,T=e.rects.popper,O="function"==typeof m?m(Object.assign({},e.rects,{placement:e.placement})):m,C={x:0,y:0};if(E){if(o||a){var k="y"===y?mt:bt,L="y"===y?gt:_t,x="y"===y?"height":"width",D=E[y],S=E[y]+g[k],N=E[y]-g[L],I=f?-T[x]/2:0,P=b===wt?A[x]:T[x],j=b===wt?-T[x]:-A[x],M=e.elements.arrow,H=f&&M?Kt(M):{width:0,height:0},B=e.modifiersData["arrow#persistent"]?e.modifiersData["arrow#persistent"].padding:{top:0,right:0,bottom:0,left:0},R=B[k],W=B[L],$=oe(0,A[x],H[x]),z=v?A[x]/2-I-$-R-O:P-$-R-O,q=v?-A[x]/2+I+$+W+O:j+$+W+O,F=e.elements.arrow&&te(e.elements.arrow),U=F?"y"===y?F.clientTop||0:F.clientLeft||0:0,V=e.modifiersData.offset?e.modifiersData.offset[e.placement][y]:0,K=E[y]+z-V-U,X=E[y]+q-V;if(o){var Y=oe(f?ne(S,K):S,D,f?ie(N,X):N);E[y]=Y,C[y]=Y-D}if(a){var Q="x"===y?mt:bt,G="x"===y?gt:_t,Z=E[w],J=Z+g[Q],tt=Z-g[G],et=oe(f?ne(J,K):J,Z,f?ie(tt,X):tt);E[w]=et,C[w]=et-Z}}e.modifiersData[n]=C}},requiresIfExists:["offset"]};function Me(t,e,i){void 0===i&&(i=!1);var n=zt(e);zt(e)&&function(t){var e=t.getBoundingClientRect();e.width,t.offsetWidth,e.height,t.offsetHeight}(e);var s,o,r=Gt(e),a=Vt(t),l={scrollLeft:0,scrollTop:0},c={x:0,y:0};return(n||!n&&!i)&&(("body"!==Rt(e)||we(r))&&(l=(s=e)!==Wt(s)&&zt(s)?{scrollLeft:(o=s).scrollLeft,scrollTop:o.scrollTop}:ve(s)),zt(e)?((c=Vt(e)).x+=e.clientLeft,c.y+=e.clientTop):r&&(c.x=ye(r))),{x:a.left+l.scrollLeft-c.x,y:a.top+l.scrollTop-c.y,width:a.width,height:a.height}}function He(t){var e=new Map,i=new Set,n=[];function s(t){i.add(t.name),[].concat(t.requires||[],t.requiresIfExists||[]).forEach((function(t){if(!i.has(t)){var n=e.get(t);n&&s(n)}})),n.push(t)}return t.forEach((function(t){e.set(t.name,t)})),t.forEach((function(t){i.has(t.name)||s(t)})),n}var Be={placement:"bottom",modifiers:[],strategy:"absolute"};function Re(){for(var t=arguments.length,e=new Array(t),i=0;ij.on(t,"mouseover",d))),this._element.focus(),this._element.setAttribute("aria-expanded",!0),this._menu.classList.add(Je),this._element.classList.add(Je),j.trigger(this._element,"shown.bs.dropdown",t)}hide(){if(c(this._element)||!this._isShown(this._menu))return;const t={relatedTarget:this._element};this._completeHide(t)}dispose(){this._popper&&this._popper.destroy(),super.dispose()}update(){this._inNavbar=this._detectNavbar(),this._popper&&this._popper.update()}_completeHide(t){j.trigger(this._element,"hide.bs.dropdown",t).defaultPrevented||("ontouchstart"in document.documentElement&&[].concat(...document.body.children).forEach((t=>j.off(t,"mouseover",d))),this._popper&&this._popper.destroy(),this._menu.classList.remove(Je),this._element.classList.remove(Je),this._element.setAttribute("aria-expanded","false"),U.removeDataAttribute(this._menu,"popper"),j.trigger(this._element,"hidden.bs.dropdown",t))}_getConfig(t){if(t={...this.constructor.Default,...U.getDataAttributes(this._element),...t},a(Ue,t,this.constructor.DefaultType),"object"==typeof t.reference&&!o(t.reference)&&"function"!=typeof t.reference.getBoundingClientRect)throw new TypeError(`${Ue.toUpperCase()}: Option "reference" provided type "object" without a required "getBoundingClientRect" method.`);return t}_createPopper(t){if(void 0===Fe)throw new TypeError("Bootstrap's dropdowns require Popper (https://popper.js.org)");let e=this._element;"parent"===this._config.reference?e=t:o(this._config.reference)?e=r(this._config.reference):"object"==typeof this._config.reference&&(e=this._config.reference);const i=this._getPopperConfig(),n=i.modifiers.find((t=>"applyStyles"===t.name&&!1===t.enabled));this._popper=qe(e,this._menu,i),n&&U.setDataAttribute(this._menu,"popper","static")}_isShown(t=this._element){return t.classList.contains(Je)}_getMenuElement(){return V.next(this._element,ei)[0]}_getPlacement(){const t=this._element.parentNode;if(t.classList.contains("dropend"))return ri;if(t.classList.contains("dropstart"))return ai;const e="end"===getComputedStyle(this._menu).getPropertyValue("--bs-position").trim();return t.classList.contains("dropup")?e?ni:ii:e?oi:si}_detectNavbar(){return null!==this._element.closest(".navbar")}_getOffset(){const{offset:t}=this._config;return"string"==typeof t?t.split(",").map((t=>Number.parseInt(t,10))):"function"==typeof t?e=>t(e,this._element):t}_getPopperConfig(){const t={placement:this._getPlacement(),modifiers:[{name:"preventOverflow",options:{boundary:this._config.boundary}},{name:"offset",options:{offset:this._getOffset()}}]};return"static"===this._config.display&&(t.modifiers=[{name:"applyStyles",enabled:!1}]),{...t,..."function"==typeof this._config.popperConfig?this._config.popperConfig(t):this._config.popperConfig}}_selectMenuItem({key:t,target:e}){const i=V.find(".dropdown-menu .dropdown-item:not(.disabled):not(:disabled)",this._menu).filter(l);i.length&&v(i,e,t===Ye,!i.includes(e)).focus()}static jQueryInterface(t){return this.each((function(){const e=hi.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}static clearMenus(t){if(t&&(2===t.button||"keyup"===t.type&&"Tab"!==t.key))return;const e=V.find(ti);for(let i=0,n=e.length;ie+t)),this._setElementAttributes(di,"paddingRight",(e=>e+t)),this._setElementAttributes(ui,"marginRight",(e=>e-t))}_disableOverFlow(){this._saveInitialAttribute(this._element,"overflow"),this._element.style.overflow="hidden"}_setElementAttributes(t,e,i){const n=this.getWidth();this._applyManipulationCallback(t,(t=>{if(t!==this._element&&window.innerWidth>t.clientWidth+n)return;this._saveInitialAttribute(t,e);const s=window.getComputedStyle(t)[e];t.style[e]=`${i(Number.parseFloat(s))}px`}))}reset(){this._resetElementAttributes(this._element,"overflow"),this._resetElementAttributes(this._element,"paddingRight"),this._resetElementAttributes(di,"paddingRight"),this._resetElementAttributes(ui,"marginRight")}_saveInitialAttribute(t,e){const i=t.style[e];i&&U.setDataAttribute(t,e,i)}_resetElementAttributes(t,e){this._applyManipulationCallback(t,(t=>{const i=U.getDataAttribute(t,e);void 0===i?t.style.removeProperty(e):(U.removeDataAttribute(t,e),t.style[e]=i)}))}_applyManipulationCallback(t,e){o(t)?e(t):V.find(t,this._element).forEach(e)}isOverflowing(){return this.getWidth()>0}}const pi={className:"modal-backdrop",isVisible:!0,isAnimated:!1,rootElement:"body",clickCallback:null},mi={className:"string",isVisible:"boolean",isAnimated:"boolean",rootElement:"(element|string)",clickCallback:"(function|null)"},gi="show",_i="mousedown.bs.backdrop";class bi{constructor(t){this._config=this._getConfig(t),this._isAppended=!1,this._element=null}show(t){this._config.isVisible?(this._append(),this._config.isAnimated&&u(this._getElement()),this._getElement().classList.add(gi),this._emulateAnimation((()=>{_(t)}))):_(t)}hide(t){this._config.isVisible?(this._getElement().classList.remove(gi),this._emulateAnimation((()=>{this.dispose(),_(t)}))):_(t)}_getElement(){if(!this._element){const t=document.createElement("div");t.className=this._config.className,this._config.isAnimated&&t.classList.add("fade"),this._element=t}return this._element}_getConfig(t){return(t={...pi,..."object"==typeof t?t:{}}).rootElement=r(t.rootElement),a("backdrop",t,mi),t}_append(){this._isAppended||(this._config.rootElement.append(this._getElement()),j.on(this._getElement(),_i,(()=>{_(this._config.clickCallback)})),this._isAppended=!0)}dispose(){this._isAppended&&(j.off(this._element,_i),this._element.remove(),this._isAppended=!1)}_emulateAnimation(t){b(t,this._getElement(),this._config.isAnimated)}}const vi={trapElement:null,autofocus:!0},yi={trapElement:"element",autofocus:"boolean"},wi=".bs.focustrap",Ei="backward";class Ai{constructor(t){this._config=this._getConfig(t),this._isActive=!1,this._lastTabNavDirection=null}activate(){const{trapElement:t,autofocus:e}=this._config;this._isActive||(e&&t.focus(),j.off(document,wi),j.on(document,"focusin.bs.focustrap",(t=>this._handleFocusin(t))),j.on(document,"keydown.tab.bs.focustrap",(t=>this._handleKeydown(t))),this._isActive=!0)}deactivate(){this._isActive&&(this._isActive=!1,j.off(document,wi))}_handleFocusin(t){const{target:e}=t,{trapElement:i}=this._config;if(e===document||e===i||i.contains(e))return;const n=V.focusableChildren(i);0===n.length?i.focus():this._lastTabNavDirection===Ei?n[n.length-1].focus():n[0].focus()}_handleKeydown(t){"Tab"===t.key&&(this._lastTabNavDirection=t.shiftKey?Ei:"forward")}_getConfig(t){return t={...vi,..."object"==typeof t?t:{}},a("focustrap",t,yi),t}}const Ti="modal",Oi="Escape",Ci={backdrop:!0,keyboard:!0,focus:!0},ki={backdrop:"(boolean|string)",keyboard:"boolean",focus:"boolean"},Li="hidden.bs.modal",xi="show.bs.modal",Di="resize.bs.modal",Si="click.dismiss.bs.modal",Ni="keydown.dismiss.bs.modal",Ii="mousedown.dismiss.bs.modal",Pi="modal-open",ji="show",Mi="modal-static";class Hi extends B{constructor(t,e){super(t),this._config=this._getConfig(e),this._dialog=V.findOne(".modal-dialog",this._element),this._backdrop=this._initializeBackDrop(),this._focustrap=this._initializeFocusTrap(),this._isShown=!1,this._ignoreBackdropClick=!1,this._isTransitioning=!1,this._scrollBar=new fi}static get Default(){return Ci}static get NAME(){return Ti}toggle(t){return this._isShown?this.hide():this.show(t)}show(t){this._isShown||this._isTransitioning||j.trigger(this._element,xi,{relatedTarget:t}).defaultPrevented||(this._isShown=!0,this._isAnimated()&&(this._isTransitioning=!0),this._scrollBar.hide(),document.body.classList.add(Pi),this._adjustDialog(),this._setEscapeEvent(),this._setResizeEvent(),j.on(this._dialog,Ii,(()=>{j.one(this._element,"mouseup.dismiss.bs.modal",(t=>{t.target===this._element&&(this._ignoreBackdropClick=!0)}))})),this._showBackdrop((()=>this._showElement(t))))}hide(){if(!this._isShown||this._isTransitioning)return;if(j.trigger(this._element,"hide.bs.modal").defaultPrevented)return;this._isShown=!1;const t=this._isAnimated();t&&(this._isTransitioning=!0),this._setEscapeEvent(),this._setResizeEvent(),this._focustrap.deactivate(),this._element.classList.remove(ji),j.off(this._element,Si),j.off(this._dialog,Ii),this._queueCallback((()=>this._hideModal()),this._element,t)}dispose(){[window,this._dialog].forEach((t=>j.off(t,".bs.modal"))),this._backdrop.dispose(),this._focustrap.deactivate(),super.dispose()}handleUpdate(){this._adjustDialog()}_initializeBackDrop(){return new bi({isVisible:Boolean(this._config.backdrop),isAnimated:this._isAnimated()})}_initializeFocusTrap(){return new Ai({trapElement:this._element})}_getConfig(t){return t={...Ci,...U.getDataAttributes(this._element),..."object"==typeof t?t:{}},a(Ti,t,ki),t}_showElement(t){const e=this._isAnimated(),i=V.findOne(".modal-body",this._dialog);this._element.parentNode&&this._element.parentNode.nodeType===Node.ELEMENT_NODE||document.body.append(this._element),this._element.style.display="block",this._element.removeAttribute("aria-hidden"),this._element.setAttribute("aria-modal",!0),this._element.setAttribute("role","dialog"),this._element.scrollTop=0,i&&(i.scrollTop=0),e&&u(this._element),this._element.classList.add(ji),this._queueCallback((()=>{this._config.focus&&this._focustrap.activate(),this._isTransitioning=!1,j.trigger(this._element,"shown.bs.modal",{relatedTarget:t})}),this._dialog,e)}_setEscapeEvent(){this._isShown?j.on(this._element,Ni,(t=>{this._config.keyboard&&t.key===Oi?(t.preventDefault(),this.hide()):this._config.keyboard||t.key!==Oi||this._triggerBackdropTransition()})):j.off(this._element,Ni)}_setResizeEvent(){this._isShown?j.on(window,Di,(()=>this._adjustDialog())):j.off(window,Di)}_hideModal(){this._element.style.display="none",this._element.setAttribute("aria-hidden",!0),this._element.removeAttribute("aria-modal"),this._element.removeAttribute("role"),this._isTransitioning=!1,this._backdrop.hide((()=>{document.body.classList.remove(Pi),this._resetAdjustments(),this._scrollBar.reset(),j.trigger(this._element,Li)}))}_showBackdrop(t){j.on(this._element,Si,(t=>{this._ignoreBackdropClick?this._ignoreBackdropClick=!1:t.target===t.currentTarget&&(!0===this._config.backdrop?this.hide():"static"===this._config.backdrop&&this._triggerBackdropTransition())})),this._backdrop.show(t)}_isAnimated(){return this._element.classList.contains("fade")}_triggerBackdropTransition(){if(j.trigger(this._element,"hidePrevented.bs.modal").defaultPrevented)return;const{classList:t,scrollHeight:e,style:i}=this._element,n=e>document.documentElement.clientHeight;!n&&"hidden"===i.overflowY||t.contains(Mi)||(n||(i.overflowY="hidden"),t.add(Mi),this._queueCallback((()=>{t.remove(Mi),n||this._queueCallback((()=>{i.overflowY=""}),this._dialog)}),this._dialog),this._element.focus())}_adjustDialog(){const t=this._element.scrollHeight>document.documentElement.clientHeight,e=this._scrollBar.getWidth(),i=e>0;(!i&&t&&!m()||i&&!t&&m())&&(this._element.style.paddingLeft=`${e}px`),(i&&!t&&!m()||!i&&t&&m())&&(this._element.style.paddingRight=`${e}px`)}_resetAdjustments(){this._element.style.paddingLeft="",this._element.style.paddingRight=""}static jQueryInterface(t,e){return this.each((function(){const i=Hi.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===i[t])throw new TypeError(`No method named "${t}"`);i[t](e)}}))}}j.on(document,"click.bs.modal.data-api",'[data-bs-toggle="modal"]',(function(t){const e=n(this);["A","AREA"].includes(this.tagName)&&t.preventDefault(),j.one(e,xi,(t=>{t.defaultPrevented||j.one(e,Li,(()=>{l(this)&&this.focus()}))}));const i=V.findOne(".modal.show");i&&Hi.getInstance(i).hide(),Hi.getOrCreateInstance(e).toggle(this)})),R(Hi),g(Hi);const Bi="offcanvas",Ri={backdrop:!0,keyboard:!0,scroll:!1},Wi={backdrop:"boolean",keyboard:"boolean",scroll:"boolean"},$i="show",zi=".offcanvas.show",qi="hidden.bs.offcanvas";class Fi extends B{constructor(t,e){super(t),this._config=this._getConfig(e),this._isShown=!1,this._backdrop=this._initializeBackDrop(),this._focustrap=this._initializeFocusTrap(),this._addEventListeners()}static get NAME(){return Bi}static get Default(){return Ri}toggle(t){return this._isShown?this.hide():this.show(t)}show(t){this._isShown||j.trigger(this._element,"show.bs.offcanvas",{relatedTarget:t}).defaultPrevented||(this._isShown=!0,this._element.style.visibility="visible",this._backdrop.show(),this._config.scroll||(new fi).hide(),this._element.removeAttribute("aria-hidden"),this._element.setAttribute("aria-modal",!0),this._element.setAttribute("role","dialog"),this._element.classList.add($i),this._queueCallback((()=>{this._config.scroll||this._focustrap.activate(),j.trigger(this._element,"shown.bs.offcanvas",{relatedTarget:t})}),this._element,!0))}hide(){this._isShown&&(j.trigger(this._element,"hide.bs.offcanvas").defaultPrevented||(this._focustrap.deactivate(),this._element.blur(),this._isShown=!1,this._element.classList.remove($i),this._backdrop.hide(),this._queueCallback((()=>{this._element.setAttribute("aria-hidden",!0),this._element.removeAttribute("aria-modal"),this._element.removeAttribute("role"),this._element.style.visibility="hidden",this._config.scroll||(new fi).reset(),j.trigger(this._element,qi)}),this._element,!0)))}dispose(){this._backdrop.dispose(),this._focustrap.deactivate(),super.dispose()}_getConfig(t){return t={...Ri,...U.getDataAttributes(this._element),..."object"==typeof t?t:{}},a(Bi,t,Wi),t}_initializeBackDrop(){return new bi({className:"offcanvas-backdrop",isVisible:this._config.backdrop,isAnimated:!0,rootElement:this._element.parentNode,clickCallback:()=>this.hide()})}_initializeFocusTrap(){return new Ai({trapElement:this._element})}_addEventListeners(){j.on(this._element,"keydown.dismiss.bs.offcanvas",(t=>{this._config.keyboard&&"Escape"===t.key&&this.hide()}))}static jQueryInterface(t){return this.each((function(){const e=Fi.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}j.on(document,"click.bs.offcanvas.data-api",'[data-bs-toggle="offcanvas"]',(function(t){const e=n(this);if(["A","AREA"].includes(this.tagName)&&t.preventDefault(),c(this))return;j.one(e,qi,(()=>{l(this)&&this.focus()}));const i=V.findOne(zi);i&&i!==e&&Fi.getInstance(i).hide(),Fi.getOrCreateInstance(e).toggle(this)})),j.on(window,"load.bs.offcanvas.data-api",(()=>V.find(zi).forEach((t=>Fi.getOrCreateInstance(t).show())))),R(Fi),g(Fi);const Ui=new Set(["background","cite","href","itemtype","longdesc","poster","src","xlink:href"]),Vi=/^(?:(?:https?|mailto|ftp|tel|file|sms):|[^#&/:?]*(?:[#/?]|$))/i,Ki=/^data:(?:image\/(?:bmp|gif|jpeg|jpg|png|tiff|webp)|video\/(?:mpeg|mp4|ogg|webm)|audio\/(?:mp3|oga|ogg|opus));base64,[\d+/a-z]+=*$/i,Xi=(t,e)=>{const i=t.nodeName.toLowerCase();if(e.includes(i))return!Ui.has(i)||Boolean(Vi.test(t.nodeValue)||Ki.test(t.nodeValue));const n=e.filter((t=>t instanceof RegExp));for(let t=0,e=n.length;t{Xi(t,r)||i.removeAttribute(t.nodeName)}))}return n.body.innerHTML}const Qi="tooltip",Gi=new Set(["sanitize","allowList","sanitizeFn"]),Zi={animation:"boolean",template:"string",title:"(string|element|function)",trigger:"string",delay:"(number|object)",html:"boolean",selector:"(string|boolean)",placement:"(string|function)",offset:"(array|string|function)",container:"(string|element|boolean)",fallbackPlacements:"array",boundary:"(string|element)",customClass:"(string|function)",sanitize:"boolean",sanitizeFn:"(null|function)",allowList:"object",popperConfig:"(null|object|function)"},Ji={AUTO:"auto",TOP:"top",RIGHT:m()?"left":"right",BOTTOM:"bottom",LEFT:m()?"right":"left"},tn={animation:!0,template:'',trigger:"hover focus",title:"",delay:0,html:!1,selector:!1,placement:"top",offset:[0,0],container:!1,fallbackPlacements:["top","right","bottom","left"],boundary:"clippingParents",customClass:"",sanitize:!0,sanitizeFn:null,allowList:{"*":["class","dir","id","lang","role",/^aria-[\w-]*$/i],a:["target","href","title","rel"],area:[],b:[],br:[],col:[],code:[],div:[],em:[],hr:[],h1:[],h2:[],h3:[],h4:[],h5:[],h6:[],i:[],img:["src","srcset","alt","title","width","height"],li:[],ol:[],p:[],pre:[],s:[],small:[],span:[],sub:[],sup:[],strong:[],u:[],ul:[]},popperConfig:null},en={HIDE:"hide.bs.tooltip",HIDDEN:"hidden.bs.tooltip",SHOW:"show.bs.tooltip",SHOWN:"shown.bs.tooltip",INSERTED:"inserted.bs.tooltip",CLICK:"click.bs.tooltip",FOCUSIN:"focusin.bs.tooltip",FOCUSOUT:"focusout.bs.tooltip",MOUSEENTER:"mouseenter.bs.tooltip",MOUSELEAVE:"mouseleave.bs.tooltip"},nn="fade",sn="show",on="show",rn="out",an=".tooltip-inner",ln=".modal",cn="hide.bs.modal",hn="hover",dn="focus";class un extends B{constructor(t,e){if(void 0===Fe)throw new TypeError("Bootstrap's tooltips require Popper (https://popper.js.org)");super(t),this._isEnabled=!0,this._timeout=0,this._hoverState="",this._activeTrigger={},this._popper=null,this._config=this._getConfig(e),this.tip=null,this._setListeners()}static get Default(){return tn}static get NAME(){return Qi}static get Event(){return en}static get DefaultType(){return Zi}enable(){this._isEnabled=!0}disable(){this._isEnabled=!1}toggleEnabled(){this._isEnabled=!this._isEnabled}toggle(t){if(this._isEnabled)if(t){const e=this._initializeOnDelegatedTarget(t);e._activeTrigger.click=!e._activeTrigger.click,e._isWithActiveTrigger()?e._enter(null,e):e._leave(null,e)}else{if(this.getTipElement().classList.contains(sn))return void this._leave(null,this);this._enter(null,this)}}dispose(){clearTimeout(this._timeout),j.off(this._element.closest(ln),cn,this._hideModalHandler),this.tip&&this.tip.remove(),this._disposePopper(),super.dispose()}show(){if("none"===this._element.style.display)throw new Error("Please use show on visible elements");if(!this.isWithContent()||!this._isEnabled)return;const t=j.trigger(this._element,this.constructor.Event.SHOW),e=h(this._element),i=null===e?this._element.ownerDocument.documentElement.contains(this._element):e.contains(this._element);if(t.defaultPrevented||!i)return;"tooltip"===this.constructor.NAME&&this.tip&&this.getTitle()!==this.tip.querySelector(an).innerHTML&&(this._disposePopper(),this.tip.remove(),this.tip=null);const n=this.getTipElement(),s=(t=>{do{t+=Math.floor(1e6*Math.random())}while(document.getElementById(t));return t})(this.constructor.NAME);n.setAttribute("id",s),this._element.setAttribute("aria-describedby",s),this._config.animation&&n.classList.add(nn);const o="function"==typeof this._config.placement?this._config.placement.call(this,n,this._element):this._config.placement,r=this._getAttachment(o);this._addAttachmentClass(r);const{container:a}=this._config;H.set(n,this.constructor.DATA_KEY,this),this._element.ownerDocument.documentElement.contains(this.tip)||(a.append(n),j.trigger(this._element,this.constructor.Event.INSERTED)),this._popper?this._popper.update():this._popper=qe(this._element,n,this._getPopperConfig(r)),n.classList.add(sn);const l=this._resolvePossibleFunction(this._config.customClass);l&&n.classList.add(...l.split(" ")),"ontouchstart"in document.documentElement&&[].concat(...document.body.children).forEach((t=>{j.on(t,"mouseover",d)}));const c=this.tip.classList.contains(nn);this._queueCallback((()=>{const t=this._hoverState;this._hoverState=null,j.trigger(this._element,this.constructor.Event.SHOWN),t===rn&&this._leave(null,this)}),this.tip,c)}hide(){if(!this._popper)return;const t=this.getTipElement();if(j.trigger(this._element,this.constructor.Event.HIDE).defaultPrevented)return;t.classList.remove(sn),"ontouchstart"in document.documentElement&&[].concat(...document.body.children).forEach((t=>j.off(t,"mouseover",d))),this._activeTrigger.click=!1,this._activeTrigger.focus=!1,this._activeTrigger.hover=!1;const e=this.tip.classList.contains(nn);this._queueCallback((()=>{this._isWithActiveTrigger()||(this._hoverState!==on&&t.remove(),this._cleanTipClass(),this._element.removeAttribute("aria-describedby"),j.trigger(this._element,this.constructor.Event.HIDDEN),this._disposePopper())}),this.tip,e),this._hoverState=""}update(){null!==this._popper&&this._popper.update()}isWithContent(){return Boolean(this.getTitle())}getTipElement(){if(this.tip)return this.tip;const t=document.createElement("div");t.innerHTML=this._config.template;const e=t.children[0];return this.setContent(e),e.classList.remove(nn,sn),this.tip=e,this.tip}setContent(t){this._sanitizeAndSetContent(t,this.getTitle(),an)}_sanitizeAndSetContent(t,e,i){const n=V.findOne(i,t);e||!n?this.setElementContent(n,e):n.remove()}setElementContent(t,e){if(null!==t)return o(e)?(e=r(e),void(this._config.html?e.parentNode!==t&&(t.innerHTML="",t.append(e)):t.textContent=e.textContent)):void(this._config.html?(this._config.sanitize&&(e=Yi(e,this._config.allowList,this._config.sanitizeFn)),t.innerHTML=e):t.textContent=e)}getTitle(){const t=this._element.getAttribute("data-bs-original-title")||this._config.title;return this._resolvePossibleFunction(t)}updateAttachment(t){return"right"===t?"end":"left"===t?"start":t}_initializeOnDelegatedTarget(t,e){return e||this.constructor.getOrCreateInstance(t.delegateTarget,this._getDelegateConfig())}_getOffset(){const{offset:t}=this._config;return"string"==typeof t?t.split(",").map((t=>Number.parseInt(t,10))):"function"==typeof t?e=>t(e,this._element):t}_resolvePossibleFunction(t){return"function"==typeof t?t.call(this._element):t}_getPopperConfig(t){const e={placement:t,modifiers:[{name:"flip",options:{fallbackPlacements:this._config.fallbackPlacements}},{name:"offset",options:{offset:this._getOffset()}},{name:"preventOverflow",options:{boundary:this._config.boundary}},{name:"arrow",options:{element:`.${this.constructor.NAME}-arrow`}},{name:"onChange",enabled:!0,phase:"afterWrite",fn:t=>this._handlePopperPlacementChange(t)}],onFirstUpdate:t=>{t.options.placement!==t.placement&&this._handlePopperPlacementChange(t)}};return{...e,..."function"==typeof this._config.popperConfig?this._config.popperConfig(e):this._config.popperConfig}}_addAttachmentClass(t){this.getTipElement().classList.add(`${this._getBasicClassPrefix()}-${this.updateAttachment(t)}`)}_getAttachment(t){return Ji[t.toUpperCase()]}_setListeners(){this._config.trigger.split(" ").forEach((t=>{if("click"===t)j.on(this._element,this.constructor.Event.CLICK,this._config.selector,(t=>this.toggle(t)));else if("manual"!==t){const e=t===hn?this.constructor.Event.MOUSEENTER:this.constructor.Event.FOCUSIN,i=t===hn?this.constructor.Event.MOUSELEAVE:this.constructor.Event.FOCUSOUT;j.on(this._element,e,this._config.selector,(t=>this._enter(t))),j.on(this._element,i,this._config.selector,(t=>this._leave(t)))}})),this._hideModalHandler=()=>{this._element&&this.hide()},j.on(this._element.closest(ln),cn,this._hideModalHandler),this._config.selector?this._config={...this._config,trigger:"manual",selector:""}:this._fixTitle()}_fixTitle(){const t=this._element.getAttribute("title"),e=typeof this._element.getAttribute("data-bs-original-title");(t||"string"!==e)&&(this._element.setAttribute("data-bs-original-title",t||""),!t||this._element.getAttribute("aria-label")||this._element.textContent||this._element.setAttribute("aria-label",t),this._element.setAttribute("title",""))}_enter(t,e){e=this._initializeOnDelegatedTarget(t,e),t&&(e._activeTrigger["focusin"===t.type?dn:hn]=!0),e.getTipElement().classList.contains(sn)||e._hoverState===on?e._hoverState=on:(clearTimeout(e._timeout),e._hoverState=on,e._config.delay&&e._config.delay.show?e._timeout=setTimeout((()=>{e._hoverState===on&&e.show()}),e._config.delay.show):e.show())}_leave(t,e){e=this._initializeOnDelegatedTarget(t,e),t&&(e._activeTrigger["focusout"===t.type?dn:hn]=e._element.contains(t.relatedTarget)),e._isWithActiveTrigger()||(clearTimeout(e._timeout),e._hoverState=rn,e._config.delay&&e._config.delay.hide?e._timeout=setTimeout((()=>{e._hoverState===rn&&e.hide()}),e._config.delay.hide):e.hide())}_isWithActiveTrigger(){for(const t in this._activeTrigger)if(this._activeTrigger[t])return!0;return!1}_getConfig(t){const e=U.getDataAttributes(this._element);return Object.keys(e).forEach((t=>{Gi.has(t)&&delete e[t]})),(t={...this.constructor.Default,...e,..."object"==typeof t&&t?t:{}}).container=!1===t.container?document.body:r(t.container),"number"==typeof t.delay&&(t.delay={show:t.delay,hide:t.delay}),"number"==typeof t.title&&(t.title=t.title.toString()),"number"==typeof t.content&&(t.content=t.content.toString()),a(Qi,t,this.constructor.DefaultType),t.sanitize&&(t.template=Yi(t.template,t.allowList,t.sanitizeFn)),t}_getDelegateConfig(){const t={};for(const e in this._config)this.constructor.Default[e]!==this._config[e]&&(t[e]=this._config[e]);return t}_cleanTipClass(){const t=this.getTipElement(),e=new RegExp(`(^|\\s)${this._getBasicClassPrefix()}\\S+`,"g"),i=t.getAttribute("class").match(e);null!==i&&i.length>0&&i.map((t=>t.trim())).forEach((e=>t.classList.remove(e)))}_getBasicClassPrefix(){return"bs-tooltip"}_handlePopperPlacementChange(t){const{state:e}=t;e&&(this.tip=e.elements.popper,this._cleanTipClass(),this._addAttachmentClass(this._getAttachment(e.placement)))}_disposePopper(){this._popper&&(this._popper.destroy(),this._popper=null)}static jQueryInterface(t){return this.each((function(){const e=un.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}g(un);const fn={...un.Default,placement:"right",offset:[0,8],trigger:"click",content:"",template:''},pn={...un.DefaultType,content:"(string|element|function)"},mn={HIDE:"hide.bs.popover",HIDDEN:"hidden.bs.popover",SHOW:"show.bs.popover",SHOWN:"shown.bs.popover",INSERTED:"inserted.bs.popover",CLICK:"click.bs.popover",FOCUSIN:"focusin.bs.popover",FOCUSOUT:"focusout.bs.popover",MOUSEENTER:"mouseenter.bs.popover",MOUSELEAVE:"mouseleave.bs.popover"};class gn extends un{static get Default(){return fn}static get NAME(){return"popover"}static get Event(){return mn}static get DefaultType(){return pn}isWithContent(){return this.getTitle()||this._getContent()}setContent(t){this._sanitizeAndSetContent(t,this.getTitle(),".popover-header"),this._sanitizeAndSetContent(t,this._getContent(),".popover-body")}_getContent(){return this._resolvePossibleFunction(this._config.content)}_getBasicClassPrefix(){return"bs-popover"}static jQueryInterface(t){return this.each((function(){const e=gn.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}g(gn);const _n="scrollspy",bn={offset:10,method:"auto",target:""},vn={offset:"number",method:"string",target:"(string|element)"},yn="active",wn=".nav-link, .list-group-item, .dropdown-item",En="position";class An extends B{constructor(t,e){super(t),this._scrollElement="BODY"===this._element.tagName?window:this._element,this._config=this._getConfig(e),this._offsets=[],this._targets=[],this._activeTarget=null,this._scrollHeight=0,j.on(this._scrollElement,"scroll.bs.scrollspy",(()=>this._process())),this.refresh(),this._process()}static get Default(){return bn}static get NAME(){return _n}refresh(){const t=this._scrollElement===this._scrollElement.window?"offset":En,e="auto"===this._config.method?t:this._config.method,n=e===En?this._getScrollTop():0;this._offsets=[],this._targets=[],this._scrollHeight=this._getScrollHeight(),V.find(wn,this._config.target).map((t=>{const s=i(t),o=s?V.findOne(s):null;if(o){const t=o.getBoundingClientRect();if(t.width||t.height)return[U[e](o).top+n,s]}return null})).filter((t=>t)).sort(((t,e)=>t[0]-e[0])).forEach((t=>{this._offsets.push(t[0]),this._targets.push(t[1])}))}dispose(){j.off(this._scrollElement,".bs.scrollspy"),super.dispose()}_getConfig(t){return(t={...bn,...U.getDataAttributes(this._element),..."object"==typeof t&&t?t:{}}).target=r(t.target)||document.documentElement,a(_n,t,vn),t}_getScrollTop(){return this._scrollElement===window?this._scrollElement.pageYOffset:this._scrollElement.scrollTop}_getScrollHeight(){return this._scrollElement.scrollHeight||Math.max(document.body.scrollHeight,document.documentElement.scrollHeight)}_getOffsetHeight(){return this._scrollElement===window?window.innerHeight:this._scrollElement.getBoundingClientRect().height}_process(){const t=this._getScrollTop()+this._config.offset,e=this._getScrollHeight(),i=this._config.offset+e-this._getOffsetHeight();if(this._scrollHeight!==e&&this.refresh(),t>=i){const t=this._targets[this._targets.length-1];this._activeTarget!==t&&this._activate(t)}else{if(this._activeTarget&&t0)return this._activeTarget=null,void this._clear();for(let e=this._offsets.length;e--;)this._activeTarget!==this._targets[e]&&t>=this._offsets[e]&&(void 0===this._offsets[e+1]||t`${e}[data-bs-target="${t}"],${e}[href="${t}"]`)),i=V.findOne(e.join(","),this._config.target);i.classList.add(yn),i.classList.contains("dropdown-item")?V.findOne(".dropdown-toggle",i.closest(".dropdown")).classList.add(yn):V.parents(i,".nav, .list-group").forEach((t=>{V.prev(t,".nav-link, .list-group-item").forEach((t=>t.classList.add(yn))),V.prev(t,".nav-item").forEach((t=>{V.children(t,".nav-link").forEach((t=>t.classList.add(yn)))}))})),j.trigger(this._scrollElement,"activate.bs.scrollspy",{relatedTarget:t})}_clear(){V.find(wn,this._config.target).filter((t=>t.classList.contains(yn))).forEach((t=>t.classList.remove(yn)))}static jQueryInterface(t){return this.each((function(){const e=An.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}j.on(window,"load.bs.scrollspy.data-api",(()=>{V.find('[data-bs-spy="scroll"]').forEach((t=>new An(t)))})),g(An);const Tn="active",On="fade",Cn="show",kn=".active",Ln=":scope > li > .active";class xn extends B{static get NAME(){return"tab"}show(){if(this._element.parentNode&&this._element.parentNode.nodeType===Node.ELEMENT_NODE&&this._element.classList.contains(Tn))return;let t;const e=n(this._element),i=this._element.closest(".nav, .list-group");if(i){const e="UL"===i.nodeName||"OL"===i.nodeName?Ln:kn;t=V.find(e,i),t=t[t.length-1]}const s=t?j.trigger(t,"hide.bs.tab",{relatedTarget:this._element}):null;if(j.trigger(this._element,"show.bs.tab",{relatedTarget:t}).defaultPrevented||null!==s&&s.defaultPrevented)return;this._activate(this._element,i);const o=()=>{j.trigger(t,"hidden.bs.tab",{relatedTarget:this._element}),j.trigger(this._element,"shown.bs.tab",{relatedTarget:t})};e?this._activate(e,e.parentNode,o):o()}_activate(t,e,i){const n=(!e||"UL"!==e.nodeName&&"OL"!==e.nodeName?V.children(e,kn):V.find(Ln,e))[0],s=i&&n&&n.classList.contains(On),o=()=>this._transitionComplete(t,n,i);n&&s?(n.classList.remove(Cn),this._queueCallback(o,t,!0)):o()}_transitionComplete(t,e,i){if(e){e.classList.remove(Tn);const t=V.findOne(":scope > .dropdown-menu .active",e.parentNode);t&&t.classList.remove(Tn),"tab"===e.getAttribute("role")&&e.setAttribute("aria-selected",!1)}t.classList.add(Tn),"tab"===t.getAttribute("role")&&t.setAttribute("aria-selected",!0),u(t),t.classList.contains(On)&&t.classList.add(Cn);let n=t.parentNode;if(n&&"LI"===n.nodeName&&(n=n.parentNode),n&&n.classList.contains("dropdown-menu")){const e=t.closest(".dropdown");e&&V.find(".dropdown-toggle",e).forEach((t=>t.classList.add(Tn))),t.setAttribute("aria-expanded",!0)}i&&i()}static jQueryInterface(t){return this.each((function(){const e=xn.getOrCreateInstance(this);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}j.on(document,"click.bs.tab.data-api",'[data-bs-toggle="tab"], [data-bs-toggle="pill"], [data-bs-toggle="list"]',(function(t){["A","AREA"].includes(this.tagName)&&t.preventDefault(),c(this)||xn.getOrCreateInstance(this).show()})),g(xn);const Dn="toast",Sn="hide",Nn="show",In="showing",Pn={animation:"boolean",autohide:"boolean",delay:"number"},jn={animation:!0,autohide:!0,delay:5e3};class Mn extends B{constructor(t,e){super(t),this._config=this._getConfig(e),this._timeout=null,this._hasMouseInteraction=!1,this._hasKeyboardInteraction=!1,this._setListeners()}static get DefaultType(){return Pn}static get Default(){return jn}static get NAME(){return Dn}show(){j.trigger(this._element,"show.bs.toast").defaultPrevented||(this._clearTimeout(),this._config.animation&&this._element.classList.add("fade"),this._element.classList.remove(Sn),u(this._element),this._element.classList.add(Nn),this._element.classList.add(In),this._queueCallback((()=>{this._element.classList.remove(In),j.trigger(this._element,"shown.bs.toast"),this._maybeScheduleHide()}),this._element,this._config.animation))}hide(){this._element.classList.contains(Nn)&&(j.trigger(this._element,"hide.bs.toast").defaultPrevented||(this._element.classList.add(In),this._queueCallback((()=>{this._element.classList.add(Sn),this._element.classList.remove(In),this._element.classList.remove(Nn),j.trigger(this._element,"hidden.bs.toast")}),this._element,this._config.animation)))}dispose(){this._clearTimeout(),this._element.classList.contains(Nn)&&this._element.classList.remove(Nn),super.dispose()}_getConfig(t){return t={...jn,...U.getDataAttributes(this._element),..."object"==typeof t&&t?t:{}},a(Dn,t,this.constructor.DefaultType),t}_maybeScheduleHide(){this._config.autohide&&(this._hasMouseInteraction||this._hasKeyboardInteraction||(this._timeout=setTimeout((()=>{this.hide()}),this._config.delay)))}_onInteraction(t,e){switch(t.type){case"mouseover":case"mouseout":this._hasMouseInteraction=e;break;case"focusin":case"focusout":this._hasKeyboardInteraction=e}if(e)return void this._clearTimeout();const i=t.relatedTarget;this._element===i||this._element.contains(i)||this._maybeScheduleHide()}_setListeners(){j.on(this._element,"mouseover.bs.toast",(t=>this._onInteraction(t,!0))),j.on(this._element,"mouseout.bs.toast",(t=>this._onInteraction(t,!1))),j.on(this._element,"focusin.bs.toast",(t=>this._onInteraction(t,!0))),j.on(this._element,"focusout.bs.toast",(t=>this._onInteraction(t,!1)))}_clearTimeout(){clearTimeout(this._timeout),this._timeout=null}static jQueryInterface(t){return this.each((function(){const e=Mn.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}return R(Mn),g(Mn),{Alert:W,Button:z,Carousel:st,Collapse:pt,Dropdown:hi,Modal:Hi,Offcanvas:Fi,Popover:gn,ScrollSpy:An,Tab:xn,Toast:Mn,Tooltip:un}})); diff --git a/themes/30coffe/assets/js/contact-form-script.js b/themes/30coffe/assets/js/contact-form-script.js new file mode 100644 index 00000000..fa78e80b --- /dev/null +++ b/themes/30coffe/assets/js/contact-form-script.js @@ -0,0 +1,8 @@ +(function($){"use strict";$("#contactForm").validator().on("submit",function(event){if(event.isDefaultPrevented()){formError();submitMSG(false,"Did you fill in the form properly?");} +else{event.preventDefault();submitForm();}});function submitForm(){var name=$("#name").val();var email=$("#email").val();var msg_subject=$("#msg_subject").val();var phone_number=$("#phone_number").val();var message=$("#message").val();var gridCheck=$("#gridCheck").val();$.ajax({type:"POST",url:"assets/php/form-process.php",data:"name="+name+"&email="+email+"&msg_subject="+msg_subject+"&phone_number="+phone_number+"&message="+message+"&gridCheck="+gridCheck,success:function(text){if(text=="success"){formSuccess();} +else{formError();submitMSG(false,text);}}});} +function formSuccess(){$("#contactForm")[0].reset();submitMSG(true,"Message Submitted!")} +function formError(){$("#contactForm").removeClass().addClass('shake animated').one('webkitAnimationEnd mozAnimationEnd MSAnimationEnd oanimationend animationend',function(){$(this).removeClass();});} +function submitMSG(valid,msg){if(valid){var msgClasses="h4 tada animated text-success";} +else{var msgClasses="h4 text-danger";} +$("#msgSubmit").removeClass().addClass(msgClasses).text(msg);}}(jQuery)); \ No newline at end of file diff --git a/themes/30coffe/assets/js/custom.js b/themes/30coffe/assets/js/custom.js new file mode 100644 index 00000000..c2d55427 --- /dev/null +++ b/themes/30coffe/assets/js/custom.js @@ -0,0 +1,11 @@ +jQuery(function($){'use strict';$(window).on('scroll',function(){if($(this).scrollTop()>50){$('.main-nav').addClass('menu-shrink');}else{$('.main-nav').removeClass('menu-shrink');}});jQuery('.mean-menu').meanmenu({meanScreenWidth:"991"});$('.banner-slider').owlCarousel({items:1,loop:true,margin:0,nav:true,dots:false,smartSpeed:1000,autoplay:true,autoplayTimeout:4000,autoplayHoverPause:true,navText:["",""],});$('.service-slider').owlCarousel({center:true,loop:true,margin:15,nav:true,dots:false,smartSpeed:1000,autoplay:true,autoplayTimeout:4000,autoplayHoverPause:true,navText:["",""],responsive:{0:{items:1,},600:{items:2,},1000:{items:3,}}});$(document).ready(function(){$('.minus').on('click',function(){var $input=$(this).parent().find('input');var count=parseInt($input.val())-1;count=count<1?1:count;$input.val(count);$input.change();return false;});$('.plus').on('click',function(){var $input=$(this).parent().find('input');$input.val(parseInt($input.val())+1);$input.change();return false;});});$('.popup-youtube').magnificPopup({disableOn:320,type:'iframe',mainClass:'mfp-fade',removalDelay:160,preloader:false,fixedContentPos:false});try{var mixer=mixitup('#Container',{controls:{toggleDefault:'none'}});}catch(err){} +$('.slider-for').slick({slidesToShow:1,slidesToScroll:1,arrows:true,fade:true,asNavFor:'.slider-nav',autoplay:true,centerPadding:'0px',prevArrow:"",nextArrow:""});$('.slider-nav').slick({slidesToShow:3,slidesToScroll:1,asNavFor:'.slider-for',dots:false,centerMode:true,focusOnSelect:true,arrows:false,centerPadding:'0px',});$(".newsletter-form").validator().on("submit",function(event){if(event.isDefaultPrevented()){formErrorSub();submitMSGSub(false,"Please enter your email correctly.");}else{event.preventDefault();}});function callbackFunction(resp){if(resp.result==="success"){formSuccessSub();} +else{formErrorSub();}} +function formSuccessSub(){$(".newsletter-form")[0].reset();submitMSGSub(true,"Thank you for subscribing!");setTimeout(function(){$("#validator-newsletter").addClass('hide');},4000)} +function formErrorSub(){$(".newsletter-form").addClass("animated shake");setTimeout(function(){$(".newsletter-form").removeClass("animated shake");},1000)} +function submitMSGSub(valid,msg){if(valid){var msgClasses="validation-success";}else{var msgClasses="validation-danger";} +$("#validator-newsletter").removeClass().addClass(msgClasses).text(msg);} +$(".newsletter-form").ajaxChimp({url:"https://envytheme.us20.list-manage.com/subscribe/post?u=60e1ffe2e8a68ce1204cd39a5&id=42d6d188d9",callback:callbackFunction});$(".modal a").not(".dropdown-toggle").on('click',function(){$(".modal").modal("hide");});$('.accordion > li:eq(0) a').addClass('active').next().slideDown();$('.accordion a').on('click',function(j){var dropDown=$(this).closest('li').find('p');$(this).closest('.accordion').find('p').not(dropDown).slideUp();if($(this).hasClass('active')){$(this).removeClass('active');}else{$(this).closest('.accordion').find('a.active').removeClass('active');$(this).addClass('active');} +dropDown.stop(false,true).slideToggle();j.preventDefault();});let getDaysId=document.getElementById('days');if(getDaysId!==null){const second=1000;const minute=second*60;const hour=minute*60;const day=hour*24;let countDown=new Date('November 30, 2022 00:00:00').getTime();setInterval(function(){let now=new Date().getTime();let distance=countDown-now;document.getElementById('days').innerText=Math.floor(distance/(day)),document.getElementById('hours').innerText=Math.floor((distance%(day))/(hour)),document.getElementById('minutes').innerText=Math.floor((distance%(hour))/(minute)),document.getElementById('seconds').innerText=Math.floor((distance%(minute))/second);},second);};jQuery(window).on('load',function(){jQuery(".loader").fadeOut(500);});$('body').append('
    ');$(window).scroll(function(){if($(this).scrollTop()!=0){$('#toTop').fadeIn();}else{$('#toTop').fadeOut();}});$('#toTop').on('click',function(){$("html, body").animate({scrollTop:0},900);return false;});$('body').append("
    ");}(jQuery));function setTheme(themeName){localStorage.setItem('restant_theme',themeName);document.documentElement.className=themeName;} +function toggleTheme(){if(localStorage.getItem('restant_theme')==='theme-dark'){setTheme('theme-light');}else{setTheme('theme-dark');}} +(function(){if(localStorage.getItem('restant_theme')==='theme-dark'){setTheme('theme-dark');document.getElementById('slider').checked=false;}else{setTheme('theme-light');document.getElementById('slider').checked=true;}})(); \ No newline at end of file diff --git a/themes/30coffe/assets/js/form-validator.min.js b/themes/30coffe/assets/js/form-validator.min.js new file mode 100644 index 00000000..ffb43154 --- /dev/null +++ b/themes/30coffe/assets/js/form-validator.min.js @@ -0,0 +1,9 @@ +/*! + * Validator v0.8.1 for Bootstrap 3, by @1000hz + * Copyright 2015 Cina Saffary + * Licensed under http://opensource.org/licenses/MIT + * + * https://github.com/1000hz/bootstrap-validator + */ + ++function(a){"use strict";function b(b){return this.each(function(){var c=a(this),e=a.extend({},d.DEFAULTS,c.data(),"object"==typeof b&&b),f=c.data("bs.validator");(f||"destroy"!=b)&&(f||c.data("bs.validator",f=new d(this,e)),"string"==typeof b&&f[b]())})}var c=':input:not([type="submit"], button):enabled:visible',d=function(b,c){this.$element=a(b),this.options=c,c.errors=a.extend({},d.DEFAULTS.errors,c.errors);for(var e in c.custom)if(!c.errors[e])throw new Error("Missing default error message for custom validator: "+e);a.extend(d.VALIDATORS,c.custom),this.$element.attr("novalidate",!0),this.toggleSubmit(),this.$element.on("input.bs.validator change.bs.validator focusout.bs.validator",a.proxy(this.validateInput,this)),this.$element.on("submit.bs.validator",a.proxy(this.onSubmit,this)),this.$element.find("[data-match]").each(function(){var b=a(this),c=b.data("match");a(c).on("input.bs.validator",function(){b.val()&&b.trigger("input.bs.validator")})})};d.DEFAULTS={delay:500,html:!1,disable:!0,custom:{},errors:{match:"Does not match",minlength:"Not long enough"},feedback:{success:"glyphicon-ok",error:"glyphicon-warning-sign"}},d.VALIDATORS={"native":function(a){var b=a[0];return b.checkValidity?b.checkValidity():!0},match:function(b){var c=b.data("match");return!b.val()||b.val()===a(c).val()},minlength:function(a){var b=a.data("minlength");return!a.val()||a.val().length>=b}},d.prototype.validateInput=function(b){var c=a(b.target),d=c.data("bs.validator.errors");if(c.is('[type="radio"]')&&(c=this.$element.find('input[name="'+c.attr("name")+'"]')),this.$element.trigger(b=a.Event("validate.bs.validator",{relatedTarget:c[0]})),!b.isDefaultPrevented()){var e=this;this.runValidators(c).done(function(f){c.data("bs.validator.errors",f),f.length?e.showErrors(c):e.clearErrors(c),d&&f.toString()===d.toString()||(b=f.length?a.Event("invalid.bs.validator",{relatedTarget:c[0],detail:f}):a.Event("valid.bs.validator",{relatedTarget:c[0],detail:d}),e.$element.trigger(b)),e.toggleSubmit(),e.$element.trigger(a.Event("validated.bs.validator",{relatedTarget:c[0]}))})}},d.prototype.runValidators=function(b){function c(a){return b.data(a+"-error")||b.data("error")||"native"==a&&b[0].validationMessage||g.errors[a]}var e=[],f=a.Deferred(),g=this.options;return b.data("bs.validator.deferred")&&b.data("bs.validator.deferred").reject(),b.data("bs.validator.deferred",f),a.each(d.VALIDATORS,a.proxy(function(a,d){if((b.data(a)||"native"==a)&&!d.call(this,b)){var f=c(a);!~e.indexOf(f)&&e.push(f)}},this)),!e.length&&b.val()&&b.data("remote")?this.defer(b,function(){var d={};d[b.attr("name")]=b.val(),a.get(b.data("remote"),d).fail(function(a,b,d){e.push(c("remote")||d)}).always(function(){f.resolve(e)})}):f.resolve(e),f.promise()},d.prototype.validate=function(){var a=this.options.delay;return this.options.delay=0,this.$element.find(c).trigger("input.bs.validator"),this.options.delay=a,this},d.prototype.showErrors=function(b){var c=this.options.html?"html":"text";this.defer(b,function(){var d=b.closest(".form-group"),e=d.find(".help-block.with-errors"),f=d.find(".form-control-feedback"),g=b.data("bs.validator.errors");g.length&&(g=a("
      ").addClass("list-unstyled").append(a.map(g,function(b){return a("
    • ")[c](b)})),void 0===e.data("bs.validator.originalContent")&&e.data("bs.validator.originalContent",e.html()),e.empty().append(g),d.addClass("has-error"),f.length&&f.removeClass(this.options.feedback.success)&&f.addClass(this.options.feedback.error)&&d.removeClass("has-success"))})},d.prototype.clearErrors=function(a){var b=a.closest(".form-group"),c=b.find(".help-block.with-errors"),d=b.find(".form-control-feedback");c.html(c.data("bs.validator.originalContent")),b.removeClass("has-error"),d.length&&d.removeClass(this.options.feedback.error)&&d.addClass(this.options.feedback.success)&&b.addClass("has-success")},d.prototype.hasErrors=function(){function b(){return!!(a(this).data("bs.validator.errors")||[]).length}return!!this.$element.find(c).filter(b).length},d.prototype.isIncomplete=function(){function b(){return"checkbox"===this.type?!this.checked:"radio"===this.type?!a('[name="'+this.name+'"]:checked').length:""===a.trim(this.value)}return!!this.$element.find(c).filter("[required]").filter(b).length},d.prototype.onSubmit=function(a){this.validate(),(this.isIncomplete()||this.hasErrors())&&a.preventDefault()},d.prototype.toggleSubmit=function(){if(this.options.disable){var b=a('button[type="submit"], input[type="submit"]').filter('[form="'+this.$element.attr("id")+'"]').add(this.$element.find('input[type="submit"], button[type="submit"]'));b.toggleClass("disabled",this.isIncomplete()||this.hasErrors()).css({"pointer-events":"all",cursor:"pointer"})}},d.prototype.defer=function(b,c){return c=a.proxy(c,this),this.options.delay?(window.clearTimeout(b.data("bs.validator.timeout")),void b.data("bs.validator.timeout",window.setTimeout(c,this.options.delay))):c()},d.prototype.destroy=function(){return this.$element.removeAttr("novalidate").removeData("bs.validator").off(".bs.validator"),this.$element.find(c).off(".bs.validator").removeData(["bs.validator.errors","bs.validator.deferred"]).each(function(){var b=a(this),c=b.data("bs.validator.timeout");window.clearTimeout(c)&&b.removeData("bs.validator.timeout")}),this.$element.find(".help-block.with-errors").each(function(){var b=a(this),c=b.data("bs.validator.originalContent");b.removeData("bs.validator.originalContent").html(c)}),this.$element.find('input[type="submit"], button[type="submit"]').removeClass("disabled"),this.$element.find(".has-error").removeClass("has-error"),this};var e=a.fn.validator;a.fn.validator=b,a.fn.validator.Constructor=d,a.fn.validator.noConflict=function(){return a.fn.validator=e,this},a(window).on("load",function(){a('form[data-toggle="validator"]').each(function(){var c=a(this);b.call(c,c.data())})})}(jQuery); \ No newline at end of file diff --git a/themes/30coffe/assets/js/jquery.ajaxchimp.min.js b/themes/30coffe/assets/js/jquery.ajaxchimp.min.js new file mode 100644 index 00000000..83b637d3 --- /dev/null +++ b/themes/30coffe/assets/js/jquery.ajaxchimp.min.js @@ -0,0 +1 @@ +(function($){"use strict";$.ajaxChimp={responses:{"We have sent you a confirmation email":0,"Please enter a value":1,"An email address must contain a single @":2,"The domain portion of the email address is invalid (the portion after the @: )":3,"The username portion of the email address is invalid (the portion before the @: )":4,"This email address looks fake or invalid. Please enter a real email address":5},translations:{en:null},init:function(selector,options){$(selector).ajaxChimp(options)}};$.fn.ajaxChimp=function(options){$(this).each(function(i,elem){var form=$(elem);var email=form.find("input[type=email]");var label=form.find("label[for="+email.attr("id")+"]");var settings=$.extend({url:form.attr("action"),language:"en"},options);var url=settings.url.replace("/post?","/post-json?").concat("&c=?");form.attr("novalidate","true");email.attr("name","EMAIL");form.submit(function(){var msg;function successCallback(resp){if(resp.result==="success"){msg="We have sent you a confirmation email";label.removeClass("error").addClass("valid");email.removeClass("error").addClass("valid")}else{email.removeClass("valid").addClass("error");label.removeClass("valid").addClass("error");var index=-1;try{var parts=resp.msg.split(" - ",2);if(parts[1]===undefined){msg=resp.msg}else{var i=parseInt(parts[0],10);if(i.toString()===parts[0]){index=parts[0];msg=parts[1]}else{index=-1;msg=resp.msg}}}catch(e){index=-1;msg=resp.msg}}if(settings.language!=="en"&&$.ajaxChimp.responses[msg]!==undefined&&$.ajaxChimp.translations&&$.ajaxChimp.translations[settings.language]&&$.ajaxChimp.translations[settings.language][$.ajaxChimp.responses[msg]]){msg=$.ajaxChimp.translations[settings.language][$.ajaxChimp.responses[msg]]}label.html(msg);label.show(2e3);if(settings.callback){settings.callback(resp)}}var data={};var dataArray=form.serializeArray();$.each(dataArray,function(index,item){data[item.name]=item.value});$.ajax({url:url,data:data,success:successCallback,dataType:"jsonp",error:function(resp,text){console.log("mailchimp ajax submit error: "+text)}});var submitMsg="Submitting...";if(settings.language!=="en"&&$.ajaxChimp.translations&&$.ajaxChimp.translations[settings.language]&&$.ajaxChimp.translations[settings.language]["submit"]){submitMsg=$.ajaxChimp.translations[settings.language]["submit"]}label.html(submitMsg).show(2e3);return false})});return this}})(jQuery); \ No newline at end of file diff --git a/themes/30coffe/assets/js/jquery.magnific-popup.min.js b/themes/30coffe/assets/js/jquery.magnific-popup.min.js new file mode 100644 index 00000000..6ee3a3bd --- /dev/null +++ b/themes/30coffe/assets/js/jquery.magnific-popup.min.js @@ -0,0 +1,4 @@ +/*! Magnific Popup - v1.1.0 - 2016-02-20 +* http://dimsemenov.com/plugins/magnific-popup/ +* Copyright (c) 2016 Dmitry Semenov; */ +!function(a){"function"==typeof define&&define.amd?define(["jquery"],a):a("object"==typeof exports?require("jquery"):window.jQuery||window.Zepto)}(function(a){var b,c,d,e,f,g,h="Close",i="BeforeClose",j="AfterClose",k="BeforeAppend",l="MarkupParse",m="Open",n="Change",o="mfp",p="."+o,q="mfp-ready",r="mfp-removing",s="mfp-prevent-close",t=function(){},u=!!window.jQuery,v=a(window),w=function(a,c){b.ev.on(o+a+p,c)},x=function(b,c,d,e){var f=document.createElement("div");return f.className="mfp-"+b,d&&(f.innerHTML=d),e?c&&c.appendChild(f):(f=a(f),c&&f.appendTo(c)),f},y=function(c,d){b.ev.triggerHandler(o+c,d),b.st.callbacks&&(c=c.charAt(0).toLowerCase()+c.slice(1),b.st.callbacks[c]&&b.st.callbacks[c].apply(b,a.isArray(d)?d:[d]))},z=function(c){return c===g&&b.currTemplate.closeBtn||(b.currTemplate.closeBtn=a(b.st.closeMarkup.replace("%title%",b.st.tClose)),g=c),b.currTemplate.closeBtn},A=function(){a.magnificPopup.instance||(b=new t,b.init(),a.magnificPopup.instance=b)},B=function(){var a=document.createElement("p").style,b=["ms","O","Moz","Webkit"];if(void 0!==a.transition)return!0;for(;b.length;)if(b.pop()+"Transition"in a)return!0;return!1};t.prototype={constructor:t,init:function(){var c=navigator.appVersion;b.isLowIE=b.isIE8=document.all&&!document.addEventListener,b.isAndroid=/android/gi.test(c),b.isIOS=/iphone|ipad|ipod/gi.test(c),b.supportsTransition=B(),b.probablyMobile=b.isAndroid||b.isIOS||/(Opera Mini)|Kindle|webOS|BlackBerry|(Opera Mobi)|(Windows Phone)|IEMobile/i.test(navigator.userAgent),d=a(document),b.popupsCache={}},open:function(c){var e;if(c.isObj===!1){b.items=c.items.toArray(),b.index=0;var g,h=c.items;for(e=0;e(a||v.height())},_setFocus:function(){(b.st.focus?b.content.find(b.st.focus).eq(0):b.wrap).focus()},_onFocusIn:function(c){return c.target===b.wrap[0]||a.contains(b.wrap[0],c.target)?void 0:(b._setFocus(),!1)},_parseMarkup:function(b,c,d){var e;d.data&&(c=a.extend(d.data,c)),y(l,[b,c,d]),a.each(c,function(c,d){if(void 0===d||d===!1)return!0;if(e=c.split("_"),e.length>1){var f=b.find(p+"-"+e[0]);if(f.length>0){var g=e[1];"replaceWith"===g?f[0]!==d[0]&&f.replaceWith(d):"img"===g?f.is("img")?f.attr("src",d):f.replaceWith(a("").attr("src",d).attr("class",f.attr("class"))):f.attr(e[1],d)}}else b.find(p+"-"+c).html(d)})},_getScrollbarSize:function(){if(void 0===b.scrollbarSize){var a=document.createElement("div");a.style.cssText="width: 99px; height: 99px; overflow: scroll; position: absolute; top: -9999px;",document.body.appendChild(a),b.scrollbarSize=a.offsetWidth-a.clientWidth,document.body.removeChild(a)}return b.scrollbarSize}},a.magnificPopup={instance:null,proto:t.prototype,modules:[],open:function(b,c){return A(),b=b?a.extend(!0,{},b):{},b.isObj=!0,b.index=c||0,this.instance.open(b)},close:function(){return a.magnificPopup.instance&&a.magnificPopup.instance.close()},registerModule:function(b,c){c.options&&(a.magnificPopup.defaults[b]=c.options),a.extend(this.proto,c.proto),this.modules.push(b)},defaults:{disableOn:0,key:null,midClick:!1,mainClass:"",preloader:!0,focus:"",closeOnContentClick:!1,closeOnBgClick:!0,closeBtnInside:!0,showCloseBtn:!0,enableEscapeKey:!0,modal:!1,alignTop:!1,removalDelay:0,prependTo:null,fixedContentPos:"auto",fixedBgPos:"auto",overflowY:"auto",closeMarkup:'',tClose:"Close (Esc)",tLoading:"Loading...",autoFocusLast:!0}},a.fn.magnificPopup=function(c){A();var d=a(this);if("string"==typeof c)if("open"===c){var e,f=u?d.data("magnificPopup"):d[0].magnificPopup,g=parseInt(arguments[1],10)||0;f.items?e=f.items[g]:(e=d,f.delegate&&(e=e.find(f.delegate)),e=e.eq(g)),b._openClick({mfpEl:e},d,f)}else b.isOpen&&b[c].apply(b,Array.prototype.slice.call(arguments,1));else c=a.extend(!0,{},c),u?d.data("magnificPopup",c):d[0].magnificPopup=c,b.addGroup(d,c);return d};var C,D,E,F="inline",G=function(){E&&(D.after(E.addClass(C)).detach(),E=null)};a.magnificPopup.registerModule(F,{options:{hiddenClass:"hide",markup:"",tNotFound:"Content not found"},proto:{initInline:function(){b.types.push(F),w(h+"."+F,function(){G()})},getInline:function(c,d){if(G(),c.src){var e=b.st.inline,f=a(c.src);if(f.length){var g=f[0].parentNode;g&&g.tagName&&(D||(C=e.hiddenClass,D=x(C),C="mfp-"+C),E=f.after(D).detach().removeClass(C)),b.updateStatus("ready")}else b.updateStatus("error",e.tNotFound),f=a("
      ");return c.inlineElement=f,f}return b.updateStatus("ready"),b._parseMarkup(d,{},c),d}}});var H,I="ajax",J=function(){H&&a(document.body).removeClass(H)},K=function(){J(),b.req&&b.req.abort()};a.magnificPopup.registerModule(I,{options:{settings:null,cursor:"mfp-ajax-cur",tError:'The content could not be loaded.'},proto:{initAjax:function(){b.types.push(I),H=b.st.ajax.cursor,w(h+"."+I,K),w("BeforeChange."+I,K)},getAjax:function(c){H&&a(document.body).addClass(H),b.updateStatus("loading");var d=a.extend({url:c.src,success:function(d,e,f){var g={data:d,xhr:f};y("ParseAjax",g),b.appendContent(a(g.data),I),c.finished=!0,J(),b._setFocus(),setTimeout(function(){b.wrap.addClass(q)},16),b.updateStatus("ready"),y("AjaxContentAdded")},error:function(){J(),c.finished=c.loadError=!0,b.updateStatus("error",b.st.ajax.tError.replace("%url%",c.src))}},b.st.ajax.settings);return b.req=a.ajax(d),""}}});var L,M=function(c){if(c.data&&void 0!==c.data.title)return c.data.title;var d=b.st.image.titleSrc;if(d){if(a.isFunction(d))return d.call(b,c);if(c.el)return c.el.attr(d)||""}return""};a.magnificPopup.registerModule("image",{options:{markup:'
      ',cursor:"mfp-zoom-out-cur",titleSrc:"title",verticalFit:!0,tError:'The image could not be loaded.'},proto:{initImage:function(){var c=b.st.image,d=".image";b.types.push("image"),w(m+d,function(){"image"===b.currItem.type&&c.cursor&&a(document.body).addClass(c.cursor)}),w(h+d,function(){c.cursor&&a(document.body).removeClass(c.cursor),v.off("resize"+p)}),w("Resize"+d,b.resizeImage),b.isLowIE&&w("AfterChange",b.resizeImage)},resizeImage:function(){var a=b.currItem;if(a&&a.img&&b.st.image.verticalFit){var c=0;b.isLowIE&&(c=parseInt(a.img.css("padding-top"),10)+parseInt(a.img.css("padding-bottom"),10)),a.img.css("max-height",b.wH-c)}},_onImageHasSize:function(a){a.img&&(a.hasSize=!0,L&&clearInterval(L),a.isCheckingImgSize=!1,y("ImageHasSize",a),a.imgHidden&&(b.content&&b.content.removeClass("mfp-loading"),a.imgHidden=!1))},findImageSize:function(a){var c=0,d=a.img[0],e=function(f){L&&clearInterval(L),L=setInterval(function(){return d.naturalWidth>0?void b._onImageHasSize(a):(c>200&&clearInterval(L),c++,void(3===c?e(10):40===c?e(50):100===c&&e(500)))},f)};e(1)},getImage:function(c,d){var e=0,f=function(){c&&(c.img[0].complete?(c.img.off(".mfploader"),c===b.currItem&&(b._onImageHasSize(c),b.updateStatus("ready")),c.hasSize=!0,c.loaded=!0,y("ImageLoadComplete")):(e++,200>e?setTimeout(f,100):g()))},g=function(){c&&(c.img.off(".mfploader"),c===b.currItem&&(b._onImageHasSize(c),b.updateStatus("error",h.tError.replace("%url%",c.src))),c.hasSize=!0,c.loaded=!0,c.loadError=!0)},h=b.st.image,i=d.find(".mfp-img");if(i.length){var j=document.createElement("img");j.className="mfp-img",c.el&&c.el.find("img").length&&(j.alt=c.el.find("img").attr("alt")),c.img=a(j).on("load.mfploader",f).on("error.mfploader",g),j.src=c.src,i.is("img")&&(c.img=c.img.clone()),j=c.img[0],j.naturalWidth>0?c.hasSize=!0:j.width||(c.hasSize=!1)}return b._parseMarkup(d,{title:M(c),img_replaceWith:c.img},c),b.resizeImage(),c.hasSize?(L&&clearInterval(L),c.loadError?(d.addClass("mfp-loading"),b.updateStatus("error",h.tError.replace("%url%",c.src))):(d.removeClass("mfp-loading"),b.updateStatus("ready")),d):(b.updateStatus("loading"),c.loading=!0,c.hasSize||(c.imgHidden=!0,d.addClass("mfp-loading"),b.findImageSize(c)),d)}}});var N,O=function(){return void 0===N&&(N=void 0!==document.createElement("p").style.MozTransform),N};a.magnificPopup.registerModule("zoom",{options:{enabled:!1,easing:"ease-in-out",duration:300,opener:function(a){return a.is("img")?a:a.find("img")}},proto:{initZoom:function(){var a,c=b.st.zoom,d=".zoom";if(c.enabled&&b.supportsTransition){var e,f,g=c.duration,j=function(a){var b=a.clone().removeAttr("style").removeAttr("class").addClass("mfp-animated-image"),d="all "+c.duration/1e3+"s "+c.easing,e={position:"fixed",zIndex:9999,left:0,top:0,"-webkit-backface-visibility":"hidden"},f="transition";return e["-webkit-"+f]=e["-moz-"+f]=e["-o-"+f]=e[f]=d,b.css(e),b},k=function(){b.content.css("visibility","visible")};w("BuildControls"+d,function(){if(b._allowZoom()){if(clearTimeout(e),b.content.css("visibility","hidden"),a=b._getItemToZoom(),!a)return void k();f=j(a),f.css(b._getOffset()),b.wrap.append(f),e=setTimeout(function(){f.css(b._getOffset(!0)),e=setTimeout(function(){k(),setTimeout(function(){f.remove(),a=f=null,y("ZoomAnimationEnded")},16)},g)},16)}}),w(i+d,function(){if(b._allowZoom()){if(clearTimeout(e),b.st.removalDelay=g,!a){if(a=b._getItemToZoom(),!a)return;f=j(a)}f.css(b._getOffset(!0)),b.wrap.append(f),b.content.css("visibility","hidden"),setTimeout(function(){f.css(b._getOffset())},16)}}),w(h+d,function(){b._allowZoom()&&(k(),f&&f.remove(),a=null)})}},_allowZoom:function(){return"image"===b.currItem.type},_getItemToZoom:function(){return b.currItem.hasSize?b.currItem.img:!1},_getOffset:function(c){var d;d=c?b.currItem.img:b.st.zoom.opener(b.currItem.el||b.currItem);var e=d.offset(),f=parseInt(d.css("padding-top"),10),g=parseInt(d.css("padding-bottom"),10);e.top-=a(window).scrollTop()-f;var h={width:d.width(),height:(u?d.innerHeight():d[0].offsetHeight)-g-f};return O()?h["-moz-transform"]=h.transform="translate("+e.left+"px,"+e.top+"px)":(h.left=e.left,h.top=e.top),h}}});var P="iframe",Q="//about:blank",R=function(a){if(b.currTemplate[P]){var c=b.currTemplate[P].find("iframe");c.length&&(a||(c[0].src=Q),b.isIE8&&c.css("display",a?"block":"none"))}};a.magnificPopup.registerModule(P,{options:{markup:'
      ',srcAction:"iframe_src",patterns:{youtube:{index:"youtube.com",id:"v=",src:"//www.youtube.com/embed/%id%?autoplay=1"},vimeo:{index:"vimeo.com/",id:"/",src:"//player.vimeo.com/video/%id%?autoplay=1"},gmaps:{index:"//maps.google.",src:"%id%&output=embed"}}},proto:{initIframe:function(){b.types.push(P),w("BeforeChange",function(a,b,c){b!==c&&(b===P?R():c===P&&R(!0))}),w(h+"."+P,function(){R()})},getIframe:function(c,d){var e=c.src,f=b.st.iframe;a.each(f.patterns,function(){return e.indexOf(this.index)>-1?(this.id&&(e="string"==typeof this.id?e.substr(e.lastIndexOf(this.id)+this.id.length,e.length):this.id.call(this,e)),e=this.src.replace("%id%",e),!1):void 0});var g={};return f.srcAction&&(g[f.srcAction]=e),b._parseMarkup(d,g,c),b.updateStatus("ready"),d}}});var S=function(a){var c=b.items.length;return a>c-1?a-c:0>a?c+a:a},T=function(a,b,c){return a.replace(/%curr%/gi,b+1).replace(/%total%/gi,c)};a.magnificPopup.registerModule("gallery",{options:{enabled:!1,arrowMarkup:'',preload:[0,2],navigateByImgClick:!0,arrows:!0,tPrev:"Previous (Left arrow key)",tNext:"Next (Right arrow key)",tCounter:"%curr% of %total%"},proto:{initGallery:function(){var c=b.st.gallery,e=".mfp-gallery";return b.direction=!0,c&&c.enabled?(f+=" mfp-gallery",w(m+e,function(){c.navigateByImgClick&&b.wrap.on("click"+e,".mfp-img",function(){return b.items.length>1?(b.next(),!1):void 0}),d.on("keydown"+e,function(a){37===a.keyCode?b.prev():39===a.keyCode&&b.next()})}),w("UpdateStatus"+e,function(a,c){c.text&&(c.text=T(c.text,b.currItem.index,b.items.length))}),w(l+e,function(a,d,e,f){var g=b.items.length;e.counter=g>1?T(c.tCounter,f.index,g):""}),w("BuildControls"+e,function(){if(b.items.length>1&&c.arrows&&!b.arrowLeft){var d=c.arrowMarkup,e=b.arrowLeft=a(d.replace(/%title%/gi,c.tPrev).replace(/%dir%/gi,"left")).addClass(s),f=b.arrowRight=a(d.replace(/%title%/gi,c.tNext).replace(/%dir%/gi,"right")).addClass(s);e.click(function(){b.prev()}),f.click(function(){b.next()}),b.container.append(e.add(f))}}),w(n+e,function(){b._preloadTimeout&&clearTimeout(b._preloadTimeout),b._preloadTimeout=setTimeout(function(){b.preloadNearbyImages(),b._preloadTimeout=null},16)}),void w(h+e,function(){d.off(e),b.wrap.off("click"+e),b.arrowRight=b.arrowLeft=null})):!1},next:function(){b.direction=!0,b.index=S(b.index+1),b.updateItemHTML()},prev:function(){b.direction=!1,b.index=S(b.index-1),b.updateItemHTML()},goTo:function(a){b.direction=a>=b.index,b.index=a,b.updateItemHTML()},preloadNearbyImages:function(){var a,c=b.st.gallery.preload,d=Math.min(c[0],b.items.length),e=Math.min(c[1],b.items.length);for(a=1;a<=(b.direction?e:d);a++)b._preloadItem(b.index+a);for(a=1;a<=(b.direction?d:e);a++)b._preloadItem(b.index-a)},_preloadItem:function(c){if(c=S(c),!b.items[c].preloaded){var d=b.items[c];d.parsed||(d=b.parseEl(c)),y("LazyLoad",d),"image"===d.type&&(d.img=a('').on("load.mfploader",function(){d.hasSize=!0}).on("error.mfploader",function(){d.hasSize=!0,d.loadError=!0,y("LazyLoadError",d)}).attr("src",d.src)),d.preloaded=!0}}}});var U="retina";a.magnificPopup.registerModule(U,{options:{replaceSrc:function(a){return a.src.replace(/\.\w+$/,function(a){return"@2x"+a})},ratio:1},proto:{initRetina:function(){if(window.devicePixelRatio>1){var a=b.st.retina,c=a.ratio;c=isNaN(c)?c():c,c>1&&(w("ImageHasSize."+U,function(a,b){b.img.css({"max-width":b.img[0].naturalWidth/c,width:"100%"})}),w("ElementParse."+U,function(b,d){d.src=a.replaceSrc(d,c)}))}}}}),A()}); \ No newline at end of file diff --git a/themes/30coffe/assets/js/jquery.meanmenu.js b/themes/30coffe/assets/js/jquery.meanmenu.js new file mode 100644 index 00000000..0dc6dd26 --- /dev/null +++ b/themes/30coffe/assets/js/jquery.meanmenu.js @@ -0,0 +1 @@ +!function($){"use strict";$.fn.meanmenu=function(e){var n={meanMenuTarget:jQuery(this),meanMenuContainer:".mobile-nav",meanMenuClose:"X",meanMenuCloseSize:"18px",meanMenuOpen:"",meanRevealPosition:"right",meanRevealPositionDistance:"0",meanRevealColour:"",meanScreenWidth:"480",meanNavPush:"",meanShowChildren:!0,meanExpandableChildren:!0,meanExpand:"+",meanContract:"-",meanRemoveAttrs:!1,onePage:!1,meanDisplay:"block",removeElements:""};e=$.extend(n,e);var a=window.innerWidth||document.documentElement.clientWidth;return this.each(function(){var n=e.meanMenuTarget,t=e.meanMenuContainer,r=e.meanMenuClose,i=e.meanMenuCloseSize,s=e.meanMenuOpen,u=e.meanRevealPosition,m=e.meanRevealPositionDistance,l=e.meanRevealColour,o=e.meanScreenWidth,c=e.meanNavPush,v=".meanmenu-reveal",h=e.meanShowChildren,d=e.meanExpandableChildren,y=e.meanExpand,j=e.meanContract,Q=e.meanRemoveAttrs,f=e.onePage,g=e.meanDisplay,p=e.removeElements,C=!1;(navigator.userAgent.match(/iPhone/i)||navigator.userAgent.match(/iPod/i)||navigator.userAgent.match(/iPad/i)||navigator.userAgent.match(/Android/i)||navigator.userAgent.match(/Blackberry/i)||navigator.userAgent.match(/Windows Phone/i))&&(C=!0),(navigator.userAgent.match(/MSIE 8/i)||navigator.userAgent.match(/MSIE 7/i))&&jQuery("html").css("overflow-y","scroll");var w="",x=function(){if("center"===u){var e=window.innerWidth||document.documentElement.clientWidth,n=e/2-22+"px";w="left:"+n+";right:auto;",C?jQuery(".meanmenu-reveal").animate({left:n}):jQuery(".meanmenu-reveal").css("left",n)}},A=!1,E=!1;"right"===u&&(w="right:"+m+";left:auto;"),"left"===u&&(w="left:"+m+";right:auto;"),x();var M="",P=function(){M.html(jQuery(M).is(".meanmenu-reveal.meanclose")?r:s)},W=function(){jQuery(".mean-bar,.mean-push").remove(),jQuery(t).removeClass("mean-container"),jQuery(n).css("display",g),A=!1,E=!1,jQuery(p).removeClass("mean-remove")},b=function(){var e="background:"+l+";color:"+l+";"+w;if(o>=a){jQuery(p).addClass("mean-remove"),E=!0,jQuery(t).addClass("mean-container"),jQuery(".mean-container").prepend('');var r=jQuery(n).html();jQuery(".mean-nav").html(r),Q&&jQuery("nav.mean-nav ul, nav.mean-nav ul *").each(function(){jQuery(this).is(".mean-remove")?jQuery(this).attr("class","mean-remove"):jQuery(this).removeAttr("class"),jQuery(this).removeAttr("id")}),jQuery(n).before('
      '),jQuery(".mean-push").css("margin-top",c),jQuery(n).hide(),jQuery(".meanmenu-reveal").show(),jQuery(v).html(s),M=jQuery(v),jQuery(".mean-nav ul").hide(),h?d?(jQuery(".mean-nav ul ul").each(function(){jQuery(this).children().length&&jQuery(this,"li:first").parent().append(''+y+"")}),jQuery(".mean-expand").on("click",function(e){e.preventDefault(),jQuery(this).hasClass("mean-clicked")?(jQuery(this).text(y),jQuery(this).prev("ul").slideUp(300,function(){})):(jQuery(this).text(j),jQuery(this).prev("ul").slideDown(300,function(){})),jQuery(this).toggleClass("mean-clicked")})):jQuery(".mean-nav ul ul").show():jQuery(".mean-nav ul ul").hide(),jQuery(".mean-nav ul li").last().addClass("mean-last"),M.removeClass("meanclose"),jQuery(M).click(function(e){e.preventDefault(),A===!1?(M.css("text-align","center"),M.css("text-indent","0"),M.css("font-size",i),jQuery(".mean-nav ul:first").slideDown(),A=!0):(jQuery(".mean-nav ul:first").slideUp(),A=!1),M.toggleClass("meanclose"),P(),jQuery(p).addClass("mean-remove")}),f&&jQuery(".mean-nav ul > li > a:first-child").on("click",function(){jQuery(".mean-nav ul:first").slideUp(),A=!1,jQuery(M).toggleClass("meanclose").html(s)})}else W()};C||jQuery(window).resize(function(){a=window.innerWidth||document.documentElement.clientWidth,a>o,W(),o>=a?(b(),x()):W()}),jQuery(window).resize(function(){a=window.innerWidth||document.documentElement.clientWidth,C?(x(),o>=a?E===!1&&b():W()):(W(),o>=a&&(b(),x()))}),b()})}}(jQuery); \ No newline at end of file diff --git a/themes/30coffe/assets/js/jquery.min.js b/themes/30coffe/assets/js/jquery.min.js new file mode 100644 index 00000000..200b54e4 --- /dev/null +++ b/themes/30coffe/assets/js/jquery.min.js @@ -0,0 +1,2 @@ +/*! jQuery v3.6.0 | (c) OpenJS Foundation and other contributors | jquery.org/license */ +!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(C,e){"use strict";var t=[],r=Object.getPrototypeOf,s=t.slice,g=t.flat?function(e){return t.flat.call(e)}:function(e){return t.concat.apply([],e)},u=t.push,i=t.indexOf,n={},o=n.toString,v=n.hasOwnProperty,a=v.toString,l=a.call(Object),y={},m=function(e){return"function"==typeof e&&"number"!=typeof e.nodeType&&"function"!=typeof e.item},x=function(e){return null!=e&&e===e.window},E=C.document,c={type:!0,src:!0,nonce:!0,noModule:!0};function b(e,t,n){var r,i,o=(n=n||E).createElement("script");if(o.text=e,t)for(r in c)(i=t[r]||t.getAttribute&&t.getAttribute(r))&&o.setAttribute(r,i);n.head.appendChild(o).parentNode.removeChild(o)}function w(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?n[o.call(e)]||"object":typeof e}var f="3.6.0",S=function(e,t){return new S.fn.init(e,t)};function p(e){var t=!!e&&"length"in e&&e.length,n=w(e);return!m(e)&&!x(e)&&("array"===n||0===t||"number"==typeof t&&0+~]|"+M+")"+M+"*"),U=new RegExp(M+"|>"),X=new RegExp(F),V=new RegExp("^"+I+"$"),G={ID:new RegExp("^#("+I+")"),CLASS:new RegExp("^\\.("+I+")"),TAG:new RegExp("^("+I+"|[*])"),ATTR:new RegExp("^"+W),PSEUDO:new RegExp("^"+F),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+R+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Y=/HTML$/i,Q=/^(?:input|select|textarea|button)$/i,J=/^h\d$/i,K=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ee=/[+~]/,te=new RegExp("\\\\[\\da-fA-F]{1,6}"+M+"?|\\\\([^\\r\\n\\f])","g"),ne=function(e,t){var n="0x"+e.slice(1)-65536;return t||(n<0?String.fromCharCode(n+65536):String.fromCharCode(n>>10|55296,1023&n|56320))},re=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ie=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},oe=function(){T()},ae=be(function(e){return!0===e.disabled&&"fieldset"===e.nodeName.toLowerCase()},{dir:"parentNode",next:"legend"});try{H.apply(t=O.call(p.childNodes),p.childNodes),t[p.childNodes.length].nodeType}catch(e){H={apply:t.length?function(e,t){L.apply(e,O.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function se(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,p=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==p&&9!==p&&11!==p)return n;if(!r&&(T(e),e=e||C,E)){if(11!==p&&(u=Z.exec(t)))if(i=u[1]){if(9===p){if(!(a=e.getElementById(i)))return n;if(a.id===i)return n.push(a),n}else if(f&&(a=f.getElementById(i))&&y(e,a)&&a.id===i)return n.push(a),n}else{if(u[2])return H.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&d.getElementsByClassName&&e.getElementsByClassName)return H.apply(n,e.getElementsByClassName(i)),n}if(d.qsa&&!N[t+" "]&&(!v||!v.test(t))&&(1!==p||"object"!==e.nodeName.toLowerCase())){if(c=t,f=e,1===p&&(U.test(t)||z.test(t))){(f=ee.test(t)&&ye(e.parentNode)||e)===e&&d.scope||((s=e.getAttribute("id"))?s=s.replace(re,ie):e.setAttribute("id",s=S)),o=(l=h(t)).length;while(o--)l[o]=(s?"#"+s:":scope")+" "+xe(l[o]);c=l.join(",")}try{return H.apply(n,f.querySelectorAll(c)),n}catch(e){N(t,!0)}finally{s===S&&e.removeAttribute("id")}}}return g(t.replace($,"$1"),e,n,r)}function ue(){var r=[];return function e(t,n){return r.push(t+" ")>b.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function le(e){return e[S]=!0,e}function ce(e){var t=C.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function fe(e,t){var n=e.split("|"),r=n.length;while(r--)b.attrHandle[n[r]]=t}function pe(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function de(t){return function(e){return"input"===e.nodeName.toLowerCase()&&e.type===t}}function he(n){return function(e){var t=e.nodeName.toLowerCase();return("input"===t||"button"===t)&&e.type===n}}function ge(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&ae(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function ve(a){return le(function(o){return o=+o,le(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function ye(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}for(e in d=se.support={},i=se.isXML=function(e){var t=e&&e.namespaceURI,n=e&&(e.ownerDocument||e).documentElement;return!Y.test(t||n&&n.nodeName||"HTML")},T=se.setDocument=function(e){var t,n,r=e?e.ownerDocument||e:p;return r!=C&&9===r.nodeType&&r.documentElement&&(a=(C=r).documentElement,E=!i(C),p!=C&&(n=C.defaultView)&&n.top!==n&&(n.addEventListener?n.addEventListener("unload",oe,!1):n.attachEvent&&n.attachEvent("onunload",oe)),d.scope=ce(function(e){return a.appendChild(e).appendChild(C.createElement("div")),"undefined"!=typeof e.querySelectorAll&&!e.querySelectorAll(":scope fieldset div").length}),d.attributes=ce(function(e){return e.className="i",!e.getAttribute("className")}),d.getElementsByTagName=ce(function(e){return e.appendChild(C.createComment("")),!e.getElementsByTagName("*").length}),d.getElementsByClassName=K.test(C.getElementsByClassName),d.getById=ce(function(e){return a.appendChild(e).id=S,!C.getElementsByName||!C.getElementsByName(S).length}),d.getById?(b.filter.ID=function(e){var t=e.replace(te,ne);return function(e){return e.getAttribute("id")===t}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n=t.getElementById(e);return n?[n]:[]}}):(b.filter.ID=function(e){var n=e.replace(te,ne);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),b.find.TAG=d.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):d.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},b.find.CLASS=d.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&E)return t.getElementsByClassName(e)},s=[],v=[],(d.qsa=K.test(C.querySelectorAll))&&(ce(function(e){var t;a.appendChild(e).innerHTML="",e.querySelectorAll("[msallowcapture^='']").length&&v.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||v.push("\\["+M+"*(?:value|"+R+")"),e.querySelectorAll("[id~="+S+"-]").length||v.push("~="),(t=C.createElement("input")).setAttribute("name",""),e.appendChild(t),e.querySelectorAll("[name='']").length||v.push("\\["+M+"*name"+M+"*="+M+"*(?:''|\"\")"),e.querySelectorAll(":checked").length||v.push(":checked"),e.querySelectorAll("a#"+S+"+*").length||v.push(".#.+[+~]"),e.querySelectorAll("\\\f"),v.push("[\\r\\n\\f]")}),ce(function(e){e.innerHTML="";var t=C.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&v.push("name"+M+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&v.push(":enabled",":disabled"),a.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&v.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),v.push(",.*:")})),(d.matchesSelector=K.test(c=a.matches||a.webkitMatchesSelector||a.mozMatchesSelector||a.oMatchesSelector||a.msMatchesSelector))&&ce(function(e){d.disconnectedMatch=c.call(e,"*"),c.call(e,"[s!='']:x"),s.push("!=",F)}),v=v.length&&new RegExp(v.join("|")),s=s.length&&new RegExp(s.join("|")),t=K.test(a.compareDocumentPosition),y=t||K.test(a.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},j=t?function(e,t){if(e===t)return l=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!d.sortDetached&&t.compareDocumentPosition(e)===n?e==C||e.ownerDocument==p&&y(p,e)?-1:t==C||t.ownerDocument==p&&y(p,t)?1:u?P(u,e)-P(u,t):0:4&n?-1:1)}:function(e,t){if(e===t)return l=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e==C?-1:t==C?1:i?-1:o?1:u?P(u,e)-P(u,t):0;if(i===o)return pe(e,t);n=e;while(n=n.parentNode)a.unshift(n);n=t;while(n=n.parentNode)s.unshift(n);while(a[r]===s[r])r++;return r?pe(a[r],s[r]):a[r]==p?-1:s[r]==p?1:0}),C},se.matches=function(e,t){return se(e,null,null,t)},se.matchesSelector=function(e,t){if(T(e),d.matchesSelector&&E&&!N[t+" "]&&(!s||!s.test(t))&&(!v||!v.test(t)))try{var n=c.call(e,t);if(n||d.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){N(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(te,ne),e[3]=(e[3]||e[4]||e[5]||"").replace(te,ne),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||se.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&se.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return G.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=h(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(te,ne).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=m[e+" "];return t||(t=new RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&m(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=se.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function j(e,n,r){return m(n)?S.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?S.grep(e,function(e){return e===n!==r}):"string"!=typeof n?S.grep(e,function(e){return-1)[^>]*|#([\w-]+))$/;(S.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||D,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:q.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof S?t[0]:t,S.merge(this,S.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:E,!0)),N.test(r[1])&&S.isPlainObject(t))for(r in t)m(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=E.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):m(e)?void 0!==n.ready?n.ready(e):e(S):S.makeArray(e,this)}).prototype=S.fn,D=S(E);var L=/^(?:parents|prev(?:Until|All))/,H={children:!0,contents:!0,next:!0,prev:!0};function O(e,t){while((e=e[t])&&1!==e.nodeType);return e}S.fn.extend({has:function(e){var t=S(e,this),n=t.length;return this.filter(function(){for(var e=0;e\x20\t\r\n\f]*)/i,he=/^$|^module$|\/(?:java|ecma)script/i;ce=E.createDocumentFragment().appendChild(E.createElement("div")),(fe=E.createElement("input")).setAttribute("type","radio"),fe.setAttribute("checked","checked"),fe.setAttribute("name","t"),ce.appendChild(fe),y.checkClone=ce.cloneNode(!0).cloneNode(!0).lastChild.checked,ce.innerHTML="",y.noCloneChecked=!!ce.cloneNode(!0).lastChild.defaultValue,ce.innerHTML="",y.option=!!ce.lastChild;var ge={thead:[1,"","
      "],col:[2,"","
      "],tr:[2,"","
      "],td:[3,"","
      "],_default:[0,"",""]};function ve(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&A(e,t)?S.merge([e],n):n}function ye(e,t){for(var n=0,r=e.length;n",""]);var me=/<|&#?\w+;/;function xe(e,t,n,r,i){for(var o,a,s,u,l,c,f=t.createDocumentFragment(),p=[],d=0,h=e.length;d\s*$/g;function je(e,t){return A(e,"table")&&A(11!==t.nodeType?t:t.firstChild,"tr")&&S(e).children("tbody")[0]||e}function De(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function qe(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Le(e,t){var n,r,i,o,a,s;if(1===t.nodeType){if(Y.hasData(e)&&(s=Y.get(e).events))for(i in Y.remove(t,"handle events"),s)for(n=0,r=s[i].length;n").attr(n.scriptAttrs||{}).prop({charset:n.scriptCharset,src:n.url}).on("load error",i=function(e){r.remove(),i=null,e&&t("error"===e.type?404:200,e.type)}),E.head.appendChild(r[0])},abort:function(){i&&i()}}});var _t,zt=[],Ut=/(=)\?(?=&|$)|\?\?/;S.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=zt.pop()||S.expando+"_"+wt.guid++;return this[e]=!0,e}}),S.ajaxPrefilter("json jsonp",function(e,t,n){var r,i,o,a=!1!==e.jsonp&&(Ut.test(e.url)?"url":"string"==typeof e.data&&0===(e.contentType||"").indexOf("application/x-www-form-urlencoded")&&Ut.test(e.data)&&"data");if(a||"jsonp"===e.dataTypes[0])return r=e.jsonpCallback=m(e.jsonpCallback)?e.jsonpCallback():e.jsonpCallback,a?e[a]=e[a].replace(Ut,"$1"+r):!1!==e.jsonp&&(e.url+=(Tt.test(e.url)?"&":"?")+e.jsonp+"="+r),e.converters["script json"]=function(){return o||S.error(r+" was not called"),o[0]},e.dataTypes[0]="json",i=C[r],C[r]=function(){o=arguments},n.always(function(){void 0===i?S(C).removeProp(r):C[r]=i,e[r]&&(e.jsonpCallback=t.jsonpCallback,zt.push(r)),o&&m(i)&&i(o[0]),o=i=void 0}),"script"}),y.createHTMLDocument=((_t=E.implementation.createHTMLDocument("").body).innerHTML="
      ",2===_t.childNodes.length),S.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(y.createHTMLDocument?((r=(t=E.implementation.createHTMLDocument("")).createElement("base")).href=E.location.href,t.head.appendChild(r)):t=E),o=!n&&[],(i=N.exec(e))?[t.createElement(i[1])]:(i=xe([e],t,o),o&&o.length&&S(o).remove(),S.merge([],i.childNodes)));var r,i,o},S.fn.load=function(e,t,n){var r,i,o,a=this,s=e.indexOf(" ");return-1").append(S.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},S.expr.pseudos.animated=function(t){return S.grep(S.timers,function(e){return t===e.elem}).length},S.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=S.css(e,"position"),c=S(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=S.css(e,"top"),u=S.css(e,"left"),("absolute"===l||"fixed"===l)&&-1<(o+u).indexOf("auto")?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),m(t)&&(t=t.call(e,n,S.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):c.css(f)}},S.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){S.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===S.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===S.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=S(e).offset()).top+=S.css(e,"borderTopWidth",!0),i.left+=S.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-S.css(r,"marginTop",!0),left:t.left-i.left-S.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===S.css(e,"position"))e=e.offsetParent;return e||re})}}),S.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;S.fn[t]=function(e){return $(this,function(e,t,n){var r;if(x(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[i]:e[t];r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),S.each(["top","left"],function(e,n){S.cssHooks[n]=Fe(y.pixelPosition,function(e,t){if(t)return t=We(e,n),Pe.test(t)?S(e).position()[n]+"px":t})}),S.each({Height:"height",Width:"width"},function(a,s){S.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){S.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return $(this,function(e,t,n){var r;return x(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?S.css(e,t,i):S.style(e,t,n,i)},s,n?e:void 0,n)}})}),S.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){S.fn[t]=function(e){return this.on(t,e)}}),S.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)},hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)}}),S.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,n){S.fn[n]=function(e,t){return 0-1}}(t.Element.prototype),Object.keys||(Object.keys=function(){var t=Object.prototype.hasOwnProperty,e=!1,n=[],a=-1;return e=!{toString:null}.propertyIsEnumerable("toString"),n=["toString","toLocaleString","valueOf","hasOwnProperty","isPrototypeOf","propertyIsEnumerable","constructor"],a=n.length,function(i){var o=[],r="",s=-1;if("object"!=typeof i&&("function"!=typeof i||null===i))throw new TypeError("Object.keys called on non-object");for(r in i)t.call(i,r)&&o.push(r);if(e)for(s=0;s>>0,0===i)return-1;if(e=0,arguments.length>1&&(e=Number(arguments[1]),e!==e?e=0:0!==e&&e!==1/0&&e!==-(1/0)&&(e=(e>0||-1)*Math.floor(Math.abs(e)))),e>=i)return-1;for(n=e>=0?e:Math.max(i-Math.abs(e),0);n0)||s);g++)r.id?d=r.id:(d="MixItUp"+n.randomHex(),r.id=d),e.instances[d]instanceof e.Mixer?(l=e.instances[d],(!i||i&&i.debug&&i.debug.showWarnings!==!1)&&console.warn(e.messages.warningFactoryPreexistingInstance())):(l=new e.Mixer,l.attach(r,u,d,i),e.instances[d]=l),c=new e.Facade(l),i&&i.debug&&i.debug.enable?h.push(l):h.push(c);return f=s?new e.Collection(h):h[0]},e.use=function(t){e.Base.prototype.callActions.call(e,"beforeUse",arguments),"function"==typeof t&&"mixitup-extension"===t.TYPE?"undefined"==typeof e.extensions[t.NAME]&&(t(e),e.extensions[t.NAME]=t):t.fn&&t.fn.jquery&&(e.libraries.$=t),e.Base.prototype.callActions.call(e,"afterUse",arguments)},e.instances={},e.extensions={},e.libraries={},n={hasClass:function(t,e){return!!t.className.match(new RegExp("(\\s|^)"+e+"(\\s|$)"))},addClass:function(t,e){this.hasClass(t,e)||(t.className+=t.className?" "+e:e)},removeClass:function(t,e){if(this.hasClass(t,e)){var n=new RegExp("(\\s|^)"+e+"(\\s|$)");t.className=t.className.replace(n," ").trim()}},extend:function(t,e,n,a){var i=[],o="",r=-1;n=n||!1,a=a||!1;try{if(Array.isArray(e))for(r=0;ru&&(u=f,l=c)}throw u>1&&(s=e.messages.errorConfigInvalidPropertySuggestion({probableMatch:l})),r=e.messages.errorConfigInvalidProperty({erroneous:o,suggestion:s}),new TypeError(r)}throw t},template:function(t){for(var e=/\${([\w]*)}/g,n={},a=null;a=e.exec(t);)n[a[1]]=new RegExp("\\${"+a[1]+"}","g");return function(e){var a="",i=t;e=e||{};for(a in n)i=i.replace(n[a],"undefined"!=typeof e[a]?e[a]:"");return i}},on:function(e,n,a,i){e&&(e.addEventListener?e.addEventListener(n,a,i):e.attachEvent&&(e["e"+n+a]=a,e[n+a]=function(){e["e"+n+a](t.event)},e.attachEvent("on"+n,e[n+a])))},off:function(t,e,n){t&&(t.removeEventListener?t.removeEventListener(e,n,!1):t.detachEvent&&(t.detachEvent("on"+e,t[e+n]),t[e+n]=null))},getCustomEvent:function(e,n,a){var i=null;return a=a||t.document,"function"==typeof t.CustomEvent?i=new t.CustomEvent(e,{detail:n,bubbles:!0,cancelable:!0}):"function"==typeof a.createEvent?(i=a.createEvent("CustomEvent"),i.initCustomEvent(e,!0,!0,n)):(i=a.createEventObject(),i.type=e,i.returnValue=!1,i.cancelBubble=!1,i.detail=n),i},getOriginalEvent:function(t){return t.touches&&t.touches.length?t.touches[0]:t.changedTouches&&t.changedTouches.length?t.changedTouches[0]:t},index:function(t,e){for(var n=0;null!==(t=t.previousElementSibling);)e&&!t.matches(e)||++n;return n},camelCase:function(t){return t.toLowerCase().replace(/([_-][a-z])/g,function(t){return t.toUpperCase().replace(/[_-]/,"")})},pascalCase:function(t){return(t=this.camelCase(t)).charAt(0).toUpperCase()+t.slice(1)},dashCase:function(t){return t.replace(/([A-Z])/g,"-$1").replace(/^-/,"").toLowerCase()},isElement:function(e,n){return n=n||t.document,!!(t.HTMLElement&&e instanceof t.HTMLElement)||(!!(n.defaultView&&n.defaultView.HTMLElement&&e instanceof n.defaultView.HTMLElement)||null!==e&&1===e.nodeType&&"string"==typeof e.nodeName)},createElement:function(e,n){var a=null,i=null;for(n=n||t.document,a=n.createDocumentFragment(),i=n.createElement("div"),i.innerHTML=e.trim();i.firstChild;)a.appendChild(i.firstChild);return a},removeWhitespace:function(t){for(var e;t&&"#text"===t.nodeName;)e=t,t=t.previousSibling,e.parentElement&&e.parentElement.removeChild(e)},isEqualArray:function(t,e){var n=t.length;if(n!==e.length)return!1;for(;n--;)if(t[n]!==e[n])return!1;return!0},deepEquals:function(t,e){var n;if("object"==typeof t&&t&&"object"==typeof e&&e){if(Object.keys(t).length!==Object.keys(e).length)return!1;for(n in t)if(!e.hasOwnProperty(n)||!this.deepEquals(t[n],e[n]))return!1}else if(t!==e)return!1;return!0},arrayShuffle:function(t){for(var e=t.slice(),n=e.length,a=n,i=-1,o=[];a--;)i=~~(Math.random()*n),o=e[a],e[a]=e[i],e[i]=o;return e},arrayFromList:function(t){var e,n;try{return Array.prototype.slice.call(t)}catch(a){for(e=[],n=0;n "+n),o&&e.removeAttribute("id")),i},clean:function(t){var e=[],n=-1;for(n=0;ni)return!0}return!0},Deferred:function(){this.promise=null,this.resolve=null,this.reject=null,this.id=n.randomHex()},isEmptyObject:function(t){var e="";if("function"==typeof Object.keys)return 0===Object.keys(t).length;for(e in t)if(t.hasOwnProperty(e))return!1;return!0},getClassname:function(t,e,n){var a="";return a+=t.block,a.length&&(a+=t.delineatorElement),a+=t["element"+this.pascalCase(e)],n?(a.length&&(a+=t.delineatorModifier),a+=n):a},getProperty:function(t,e){var n=e.split("."),a=null,i="",o=0;if(!e)return t;for(a=function(t){return t?t[i]:null};o-1,e.callFilters("afterIsBound",n,arguments)},addBinding:function(t){var e=this;this.callActions("beforeAddBinding",arguments),e.isBound()||e.bound.push(t),this.callActions("afterAddBinding",arguments)},removeBinding:function(t){var n=this,a=-1;this.callActions("beforeRemoveBinding",arguments),(a=n.bound.indexOf(t))>-1&&n.bound.splice(a,1),n.bound.length<1&&(n.unbindClick(),a=e.controls.indexOf(n),e.controls.splice(a,1),"active"===n.status&&n.renderStatus(n.el,"inactive")),this.callActions("afterRemoveBinding",arguments)},bindClick:function(){var t=this;this.callActions("beforeBindClick",arguments),t.handler=function(e){t.handleClick(e)},n.on(t.el,"click",t.handler),this.callActions("afterBindClick",arguments)},unbindClick:function(){var t=this;this.callActions("beforeUnbindClick",arguments),n.off(t.el,"click",t.handler),t.handler=null,this.callActions("afterUnbindClick",arguments)},handleClick:function(t){var a=this,i=null,o=null,r=!1,s=void 0,l={},c=null,u=[],f=-1;if(this.callActions("beforeHandleClick",arguments),this.pending=0,o=a.bound[0],i=a.selector?n.closestParent(t.target,o.config.selectors.control+a.selector,!0,o.dom.document):a.el,!i)return void a.callActions("afterHandleClick",arguments);switch(a.type){case"filter":l.filter=a.filter||i.getAttribute("data-filter");break;case"sort":l.sort=a.sort||i.getAttribute("data-sort");break;case"multimix":l.filter=a.filter||i.getAttribute("data-filter"),l.sort=a.sort||i.getAttribute("data-sort");break;case"toggle":l.filter=a.filter||i.getAttribute("data-toggle"),r="live"===a.status?n.hasClass(i,a.classNames.active):"active"===a.status}for(f=0;f0||("live"===a.status?a.updateLive(t,n):(i.sort=a.sort,i.filter=a.filter,a.callFilters("actionsUpdate",i,arguments),a.parseStatusChange(a.el,t,i,n)),a.callActions("afterUpdate",arguments))},updateLive:function(t,n){var a=this,i=null,o=null,r=null,s=-1;if(a.callActions("beforeUpdateLive",arguments),a.el){for(i=a.el.querySelectorAll(a.selector),s=0;r=i[s];s++){switch(o=new e.CommandMultimix,a.type){case"filter":o.filter=r.getAttribute("data-filter");break;case"sort":o.sort=r.getAttribute("data-sort");break;case"multimix":o.filter=r.getAttribute("data-filter"),o.sort=r.getAttribute("data-sort");break;case"toggle":o.filter=r.getAttribute("data-toggle")}o=a.callFilters("actionsUpdateLive",o,arguments),a.parseStatusChange(r,t,o,n)}a.callActions("afterUpdateLive",arguments)}},parseStatusChange:function(t,e,n,a){var i=this,o="",r="",s=-1;switch(i.callActions("beforeParseStatusChange",arguments),i.type){case"filter":e.filter===n.filter?i.renderStatus(t,"active"):i.renderStatus(t,"inactive");break;case"multimix":e.sort===n.sort&&e.filter===n.filter?i.renderStatus(t,"active"):i.renderStatus(t,"inactive");break;case"sort":e.sort.match(/:asc/g)&&(o=e.sort.replace(/:asc/g,"")),e.sort===n.sort||o===n.sort?i.renderStatus(t,"active"):i.renderStatus(t,"inactive");break;case"toggle":for(a.length<1&&i.renderStatus(t,"inactive"),e.filter===n.filter&&i.renderStatus(t,"active"),s=0;s-1)throw new Error(e.messages.errorInsertPreexistingElement());c.style.display="none",s.appendChild(c),s.appendChild(i.dom.document.createTextNode(" ")),n.isElement(c,i.dom.document)&&c.matches(i.config.selectors.target)&&(l=new e.Target,l.init(c,i),l.isInDom=!0,i.targets.splice(r,0,l),r++)}i.dom.parent.insertBefore(s,o)}a.startOrder=i.origOrder=i.targets,i.callActions("afterInsertTargets",arguments)},getNextSibling:function(t,e,n){var a=this,i=null;return t=Math.max(t,0),e&&"before"===n?i=e:e&&"after"===n?i=e.nextElementSibling||null:a.targets.length>0&&"undefined"!=typeof t?i=t0&&(a.config.layout.siblingAfter?i=a.config.layout.siblingAfter:a.config.layout.siblingBefore?i=a.config.layout.siblingBefore.nextElementSibling:a.dom.parent.children[0]),a.callFilters("elementGetNextSibling",i,arguments)},filterOperation:function(t){var e=this,n=!1,a=-1,i="",o=null,r=-1;for(e.callActions("beforeFilterOperation",arguments),i=t.newFilter.action,r=0;o=t.newOrder[r];r++)n=t.newFilter.collection?t.newFilter.collection.indexOf(o.dom.el)>-1:""!==t.newFilter.selector&&o.dom.el.matches(t.newFilter.selector),e.evaluateHideShow(n,o,i,t);if(t.toRemove.length)for(r=0;o=t.show[r];r++)t.toRemove.indexOf(o)>-1&&(t.show.splice(r,1),(a=t.toShow.indexOf(o))>-1&&t.toShow.splice(a,1),t.toHide.push(o),t.hide.push(o),r--);t.matching=t.show.slice(),0===t.show.length&&""!==t.newFilter.selector&&0!==e.targets.length&&(t.hasFailed=!0),e.callActions("afterFilterOperation",arguments)},evaluateHideShow:function(t,e,n,a){var i=this,o=!1,r=Array.prototype.slice.call(arguments,1);o=i.callFilters("testResultEvaluateHideShow",t,r),i.callActions("beforeEvaluateHideShow",arguments),o===!0&&"show"===n||o===!1&&"hide"===n?(a.show.push(e),!e.isShown&&a.toShow.push(e)):(a.hide.push(e),e.isShown&&a.toHide.push(e)),i.callActions("afterEvaluateHideShow",arguments)},sortOperation:function(t){var a=this,i=[],o=null,r=null,s=-1;if(a.callActions("beforeSortOperation",arguments),t.startOrder=a.targets,t.newSort.collection){for(i=[],s=0;r=t.newSort.collection[s];s++){if(a.dom.targets.indexOf(r)<0)throw new Error(e.messages.errorSortNonExistentElement());o=new e.Target,o.init(r,a),o.isInDom=!0,i.push(o)}t.newOrder=i}else"random"===t.newSort.order?t.newOrder=n.arrayShuffle(t.startOrder):""===t.newSort.attribute?(t.newOrder=a.origOrder.slice(),"desc"===t.newSort.order&&t.newOrder.reverse()):(t.newOrder=t.startOrder.slice(),t.newOrder.sort(function(e,n){return a.compare(e,n,t.newSort)}));n.isEqualArray(t.newOrder,t.startOrder)&&(t.willSort=!1),a.callActions("afterSortOperation",arguments)},compare:function(t,e,n){var a=this,i=n.order,o=a.getAttributeValue(t,n.attribute),r=a.getAttributeValue(e,n.attribute);return isNaN(1*o)||isNaN(1*r)?(o=o.toLowerCase(),r=r.toLowerCase()):(o=1*o,r=1*r),or?"asc"===i?1:-1:o===r&&n.next?a.compare(t,e,n.next):0},getAttributeValue:function(t,n){var a=this,i="";return i=t.dom.el.getAttribute("data-"+n),null===i&&a.config.debug.showWarnings&&console.warn(e.messages.warningInconsistentSortingAttributes({attribute:"data-"+n})),a.callFilters("valueGetAttributeValue",i||0,arguments)},printSort:function(e,a){var i=this,o=e?a.newOrder:a.startOrder,r=e?a.startOrder:a.newOrder,s=o.length?o[o.length-1].dom.el.nextElementSibling:null,l=t.document.createDocumentFragment(),c=null,u=null,f=null,h=-1;for(i.callActions("beforePrintSort",arguments),h=0;u=o[h];h++)f=u.dom.el,"absolute"!==f.style.position&&(n.removeWhitespace(f.previousSibling),f.parentElement.removeChild(f));for(c=s?s.previousSibling:i.dom.parent.lastChild,c&&"#text"===c.nodeName&&n.removeWhitespace(c),h=0;u=r[h];h++)f=u.dom.el,n.isElement(l.lastChild)&&l.appendChild(t.document.createTextNode(" ")),l.appendChild(f);i.dom.parent.firstChild&&i.dom.parent.firstChild!==s&&l.insertBefore(t.document.createTextNode(" "),l.childNodes[0]),s?(l.appendChild(t.document.createTextNode(" ")),i.dom.parent.insertBefore(l,s)):i.dom.parent.appendChild(l),i.callActions("afterPrintSort",arguments)},parseSortString:function(t,a){var i=this,o=t.split(" "),r=a,s=[],l=-1;for(l=0;l-1&&(c=n.substring(l),u=s.exec(c),f=u[1]),t){case"fade":a.opacity=f?parseFloat(f):0;break;case"stagger":r.staggerDuration=f?parseFloat(f):100;break;default:if(o&&r.config.animation.reverseOut&&"scale"!==t?a[t].value=(f?parseFloat(f):e.transformDefaults[t].value)*-1:a[t].value=f?parseFloat(f):e.transformDefaults[t].value,f){for(m=0;d=h[m];m++)if(f.indexOf(d)>-1){a[t].unit=d;break}}else a[t].unit=e.transformDefaults[t].unit;i.push(t+"("+a[t].value+a[t].unit+")")}r.callActions("afterParseEffect",arguments)},buildState:function(t){var n=this,a=new e.State,i=null,o=-1;for(n.callActions("beforeBuildState",arguments),o=0;i=n.targets[o];o++)(!t.toRemove.length||t.toRemove.indexOf(i)<0)&&a.targets.push(i.dom.el);for(o=0;i=t.matching[o];o++)a.matching.push(i.dom.el);for(o=0;i=t.show[o];o++)a.show.push(i.dom.el);for(o=0;i=t.hide[o];o++)(!t.toRemove.length||t.toRemove.indexOf(i)<0)&&a.hide.push(i.dom.el);return a.id=n.id,a.container=n.dom.container,a.activeFilter=t.newFilter,a.activeSort=t.newSort,a.activeDataset=t.newDataset,a.activeContainerClassName=t.newContainerClassName,a.hasFailed=t.hasFailed,a.totalTargets=n.targets.length,a.totalShow=t.show.length,a.totalHide=t.hide.length,a.totalMatching=t.matching.length,a.triggerElement=t.triggerElement,n.callFilters("stateBuildState",a,arguments)},goMix:function(a,i){var o=this,r=null;return o.callActions("beforeGoMix",arguments),o.config.animation.duration&&o.config.animation.effects&&n.isVisible(o.dom.container)||(a=!1),i.toShow.length||i.toHide.length||i.willSort||i.willChangeLayout||(a=!1),i.startState.show.length||i.show.length||(a=!1),e.events.fire("mixStart",o.dom.container,{state:i.startState,futureState:i.newState,instance:o},o.dom.document),"function"==typeof o.config.callbacks.onMixStart&&o.config.callbacks.onMixStart.call(o.dom.container,i.startState,i.newState,o),n.removeClass(o.dom.container,n.getClassname(o.config.classNames,"container",o.config.classNames.modifierFailed)),r=o.userDeferred?o.userDeferred:o.userDeferred=n.defer(e.libraries),o.isBusy=!0,a&&e.features.has.transitions?(t.pageYOffset!==i.docState.scrollTop&&t.scrollTo(i.docState.scrollLeft,i.docState.scrollTop),o.config.animation.applyPerspective&&(o.dom.parent.style[e.features.perspectiveProp]=o.config.animation.perspectiveDistance,o.dom.parent.style[e.features.perspectiveOriginProp]=o.config.animation.perspectiveOrigin),o.config.animation.animateResizeContainer&&i.startHeight!==i.newHeight&&i.viewportDeltaY!==i.startHeight-i.newHeight&&(o.dom.parent.style.height=i.startHeight+"px"),o.config.animation.animateResizeContainer&&i.startWidth!==i.newWidth&&i.viewportDeltaX!==i.startWidth-i.newWidth&&(o.dom.parent.style.width=i.startWidth+"px"),i.startHeight===i.newHeight&&(o.dom.parent.style.height=i.startHeight+"px"),i.startWidth===i.newWidth&&(o.dom.parent.style.width=i.startWidth+"px"),i.startHeight===i.newHeight&&i.startWidth===i.newWidth&&(o.dom.parent.style.overflow="hidden"),requestAnimationFrame(function(){o.moveTargets(i)}),o.callFilters("promiseGoMix",r.promise,arguments)):(o.config.debug.fauxAsync?setTimeout(function(){o.cleanUp(i)},o.config.animation.duration):o.cleanUp(i),o.callFilters("promiseGoMix",r.promise,arguments))},getStartMixData:function(n){var a=this,i=t.getComputedStyle(a.dom.parent),o=a.dom.parent.getBoundingClientRect(),r=null,s={},l=-1,c=i[e.features.boxSizingProp];for(a.incPadding="border-box"===c,a.callActions("beforeGetStartMixData",arguments),l=0;r=n.show[l];l++)s=r.getPosData(),n.showPosData[l]={startPosData:s};for(l=0;r=n.toHide[l];l++)s=r.getPosData(),n.toHidePosData[l]={startPosData:s};n.startX=o.left,n.startY=o.top,n.startHeight=a.incPadding?o.height:o.height-parseFloat(i.paddingTop)-parseFloat(i.paddingBottom)-parseFloat(i.borderTop)-parseFloat(i.borderBottom),n.startWidth=a.incPadding?o.width:o.width-parseFloat(i.paddingLeft)-parseFloat(i.paddingRight)-parseFloat(i.borderLeft)-parseFloat(i.borderRight),a.callActions("afterGetStartMixData",arguments)},setInter:function(t){var e=this,a=null,i=-1;for(e.callActions("beforeSetInter",arguments),e.config.animation.clampHeight&&(e.dom.parent.style.height=t.startHeight+"px",e.dom.parent.style.overflow="hidden"),e.config.animation.clampWidth&&(e.dom.parent.style.width=t.startWidth+"px",e.dom.parent.style.overflow="hidden"),i=0;a=t.toShow[i];i++)a.show();t.willChangeLayout&&(n.removeClass(e.dom.container,t.startContainerClassName),n.addClass(e.dom.container,t.newContainerClassName)),e.callActions("afterSetInter",arguments)},getInterMixData:function(t){var e=this,n=null,a=-1;for(e.callActions("beforeGetInterMixData",arguments),a=0;n=t.show[a];a++)t.showPosData[a].interPosData=n.getPosData();for(a=0;n=t.toHide[a];a++)t.toHidePosData[a].interPosData=n.getPosData();e.callActions("afterGetInterMixData",arguments)},setFinal:function(t){var e=this,n=null,a=-1;for(e.callActions("beforeSetFinal",arguments),t.willSort&&e.printSort(!1,t),a=0;n=t.toHide[a];a++)n.hide();e.callActions("afterSetFinal",arguments)},getFinalMixData:function(e){var a=this,i=null,o=null,r=null,s=-1;for(a.callActions("beforeGetFinalMixData",arguments),s=0;r=e.show[s];s++)e.showPosData[s].finalPosData=r.getPosData();for(s=0;r=e.toHide[s];s++)e.toHidePosData[s].finalPosData=r.getPosData();for((a.config.animation.clampHeight||a.config.animation.clampWidth)&&(a.dom.parent.style.height=a.dom.parent.style.width=a.dom.parent.style.overflow=""),a.incPadding||(i=t.getComputedStyle(a.dom.parent)),o=a.dom.parent.getBoundingClientRect(),e.newX=o.left,e.newY=o.top,e.newHeight=a.incPadding?o.height:o.height-parseFloat(i.paddingTop)-parseFloat(i.paddingBottom)-parseFloat(i.borderTop)-parseFloat(i.borderBottom),e.newWidth=a.incPadding?o.width:o.width-parseFloat(i.paddingLeft)-parseFloat(i.paddingRight)-parseFloat(i.borderLeft)-parseFloat(i.borderRight),e.viewportDeltaX=e.docState.viewportWidth-this.dom.document.documentElement.clientWidth,e.viewportDeltaY=e.docState.viewportHeight-this.dom.document.documentElement.clientHeight,e.willSort&&a.printSort(!0,e),s=0;r=e.toShow[s];s++)r.hide();for(s=0;r=e.toHide[s];s++)r.show();e.willChangeLayout&&(n.removeClass(a.dom.container,e.newContainerClassName),n.addClass(a.dom.container,a.config.layout.containerClassName)),a.callActions("afterGetFinalMixData",arguments)},getTweenData:function(t){var n=this,a=null,i=null,o=Object.getOwnPropertyNames(n.effectsIn),r="",s=null,l=-1,c=-1,u=-1,f=-1;for(n.callActions("beforeGetTweenData",arguments),u=0;a=t.show[u];u++)for(i=t.showPosData[u],i.posIn=new e.StyleData,i.posOut=new e.StyleData,i.tweenData=new e.StyleData,a.isShown?(i.posIn.x=i.startPosData.x-i.interPosData.x,i.posIn.y=i.startPosData.y-i.interPosData.y):i.posIn.x=i.posIn.y=0,i.posOut.x=i.finalPosData.x-i.interPosData.x,i.posOut.y=i.finalPosData.y-i.interPosData.y,i.posIn.opacity=a.isShown?1:n.effectsIn.opacity,i.posOut.opacity=1,i.tweenData.opacity=i.posOut.opacity-i.posIn.opacity,a.isShown||n.config.animation.nudge||(i.posIn.x=i.posOut.x,i.posIn.y=i.posOut.y),i.tweenData.x=i.posOut.x-i.posIn.x,i.tweenData.y=i.posOut.y-i.posIn.y,n.config.animation.animateResizeTargets&&(i.posIn.width=i.startPosData.width,i.posIn.height=i.startPosData.height,l=(i.startPosData.width||i.finalPosData.width)-i.interPosData.width,i.posIn.marginRight=i.startPosData.marginRight-l,c=(i.startPosData.height||i.finalPosData.height)-i.interPosData.height,i.posIn.marginBottom=i.startPosData.marginBottom-c,i.posOut.width=i.finalPosData.width,i.posOut.height=i.finalPosData.height,l=(i.finalPosData.width||i.startPosData.width)-i.interPosData.width,i.posOut.marginRight=i.finalPosData.marginRight-l,c=(i.finalPosData.height||i.startPosData.height)-i.interPosData.height,i.posOut.marginBottom=i.finalPosData.marginBottom-c,i.tweenData.width=i.posOut.width-i.posIn.width,i.tweenData.height=i.posOut.height-i.posIn.height,i.tweenData.marginRight=i.posOut.marginRight-i.posIn.marginRight,i.tweenData.marginBottom=i.posOut.marginBottom-i.posIn.marginBottom),f=0;r=o[f];f++)s=n.effectsIn[r],s instanceof e.TransformData&&s.value&&(i.posIn[r].value=s.value,i.posOut[r].value=0,i.tweenData[r].value=i.posOut[r].value-i.posIn[r].value,i.posIn[r].unit=i.posOut[r].unit=i.tweenData[r].unit=s.unit);for(u=0;a=t.toHide[u];u++)for(i=t.toHidePosData[u],i.posIn=new e.StyleData,i.posOut=new e.StyleData,i.tweenData=new e.StyleData,i.posIn.x=a.isShown?i.startPosData.x-i.interPosData.x:0,i.posIn.y=a.isShown?i.startPosData.y-i.interPosData.y:0,i.posOut.x=n.config.animation.nudge?0:i.posIn.x,i.posOut.y=n.config.animation.nudge?0:i.posIn.y,i.tweenData.x=i.posOut.x-i.posIn.x,i.tweenData.y=i.posOut.y-i.posIn.y,n.config.animation.animateResizeTargets&&(i.posIn.width=i.startPosData.width,i.posIn.height=i.startPosData.height,l=i.startPosData.width-i.interPosData.width,i.posIn.marginRight=i.startPosData.marginRight-l,c=i.startPosData.height-i.interPosData.height,i.posIn.marginBottom=i.startPosData.marginBottom-c),i.posIn.opacity=1,i.posOut.opacity=n.effectsOut.opacity,i.tweenData.opacity=i.posOut.opacity-i.posIn.opacity,f=0;r=o[f];f++)s=n.effectsOut[r],s instanceof e.TransformData&&s.value&&(i.posIn[r].value=0,i.posOut[r].value=s.value,i.tweenData[r].value=i.posOut[r].value-i.posIn[r].value,i.posIn[r].unit=i.posOut[r].unit=i.tweenData[r].unit=s.unit);n.callActions("afterGetTweenData",arguments)},moveTargets:function(t){var a=this,i=null,o=null,r=null,s="",l=!1,c=-1,u=-1,f=a.checkProgress.bind(a);for(a.callActions("beforeMoveTargets",arguments),u=0;i=t.show[u];u++)o=new e.IMoveData,r=t.showPosData[u],s=i.isShown?"none":"show",l=a.willTransition(s,t.hasEffect,r.posIn,r.posOut),l&&c++,i.show(),o.posIn=r.posIn,o.posOut=r.posOut,o.statusChange=s,o.staggerIndex=c,o.operation=t,o.callback=l?f:null,i.move(o);for(u=0;i=t.toHide[u];u++)r=t.toHidePosData[u],o=new e.IMoveData,s="hide",l=a.willTransition(s,r.posIn,r.posOut),o.posIn=r.posIn,o.posOut=r.posOut,o.statusChange=s,o.staggerIndex=u,o.operation=t,o.callback=l?f:null,i.move(o);a.config.animation.animateResizeContainer&&(a.dom.parent.style[e.features.transitionProp]="height "+a.config.animation.duration+"ms ease, width "+a.config.animation.duration+"ms ease ",requestAnimationFrame(function(){t.startHeight!==t.newHeight&&t.viewportDeltaY!==t.startHeight-t.newHeight&&(a.dom.parent.style.height=t.newHeight+"px"),t.startWidth!==t.newWidth&&t.viewportDeltaX!==t.startWidth-t.newWidth&&(a.dom.parent.style.width=t.newWidth+"px")})),t.willChangeLayout&&(n.removeClass(a.dom.container,a.config.layout.ContainerClassName),n.addClass(a.dom.container,t.newContainerClassName)),a.callActions("afterMoveTargets",arguments)},hasEffect:function(){var t=this,e=["scale","translateX","translateY","translateZ","rotateX","rotateY","rotateZ"],n="",a=null,i=!1,o=-1,r=-1;if(1!==t.effectsIn.opacity)return t.callFilters("resultHasEffect",!0,arguments);for(r=0;n=e[r];r++)if(a=t.effectsIn[n],o="undefined"!==a.value?a.value:a,0!==o){i=!0;break}return t.callFilters("resultHasEffect",i,arguments)},willTransition:function(t,e,a,i){var o=this,r=!1;return r=!!n.isVisible(o.dom.container)&&(!!("none"!==t&&e||a.x!==i.x||a.y!==i.y)||!!o.config.animation.animateResizeTargets&&(a.width!==i.width||a.height!==i.height||a.marginRight!==i.marginRight||a.marginTop!==i.marginTop)),o.callFilters("resultWillTransition",r,arguments)},checkProgress:function(t){var e=this;e.targetsDone++,e.targetsBound===e.targetsDone&&e.cleanUp(t)},cleanUp:function(t){var a=this,i=null,o=null,r=null,s=null,l=-1;for(a.callActions("beforeCleanUp",arguments),a.targetsMoved=a.targetsImmovable=a.targetsBound=a.targetsDone=0,l=0;i=t.show[l];l++)i.cleanUp(),i.show();for(l=0;i=t.toHide[l];l++)i.cleanUp(),i.hide();if(t.willSort&&a.printSort(!1,t),a.dom.parent.style[e.features.transitionProp]=a.dom.parent.style.height=a.dom.parent.style.width=a.dom.parent.style.overflow=a.dom.parent.style[e.features.perspectiveProp]=a.dom.parent.style[e.features.perspectiveOriginProp]="",t.willChangeLayout&&(n.removeClass(a.dom.container,t.startContainerClassName),n.addClass(a.dom.container,t.newContainerClassName)),t.toRemove.length){for(l=0;i=a.targets[l];l++)t.toRemove.indexOf(i)>-1&&((o=i.dom.el.previousSibling)&&"#text"===o.nodeName&&(r=i.dom.el.nextSibling)&&"#text"===r.nodeName&&n.removeWhitespace(o),t.willSort||a.dom.parent.removeChild(i.dom.el),a.targets.splice(l,1),i.isInDom=!1,l--);a.origOrder=a.targets}t.willSort&&(a.targets=t.newOrder),a.state=t.newState,a.lastOperation=t,a.dom.targets=a.state.targets,e.events.fire("mixEnd",a.dom.container,{state:a.state,instance:a},a.dom.document),"function"==typeof a.config.callbacks.onMixEnd&&a.config.callbacks.onMixEnd.call(a.dom.container,a.state,a),t.hasFailed&&(e.events.fire("mixFail",a.dom.container,{state:a.state,instance:a},a.dom.document),"function"==typeof a.config.callbacks.onMixFail&&a.config.callbacks.onMixFail.call(a.dom.container,a.state,a),n.addClass(a.dom.container,n.getClassname(a.config.classNames,"container",a.config.classNames.modifierFailed))),"function"==typeof a.userCallback&&a.userCallback.call(a.dom.container,a.state,a),"function"==typeof a.userDeferred.resolve&&a.userDeferred.resolve(a.state),a.userCallback=null,a.userDeferred=null,a.lastClicked=null,a.isToggling=!1,a.isBusy=!1,a.queue.length&&(a.callActions("beforeReadQueueCleanUp",arguments),s=a.queue.shift(),a.userDeferred=s.deferred,a.isToggling=s.isToggling,a.lastClicked=s.triggerElement,s.instruction.command instanceof e.CommandMultimix?a.multimix.apply(a,s.args):a.dataset.apply(a,s.args)),a.callActions("afterCleanUp",arguments)},parseMultimixArgs:function(t){var a=this,i=new e.UserInstruction,o=null,r=-1;for(i.animate=a.config.animation.enable,i.command=new e.CommandMultimix,r=0;r-1?i.command.position=o:"string"==typeof o?i.command.collection=n.arrayFromList(n.createElement(o).childNodes):"object"==typeof o&&n.isElement(o,a.dom.document)?i.command.collection.length?i.command.sibling=o:i.command.collection=[o]:"object"==typeof o&&o.length?i.command.collection.length?i.command.sibling=o[0]:i.command.collection=o:"object"==typeof o&&o.childNodes&&o.childNodes.length?i.command.collection.length?i.command.sibling=o.childNodes[0]:i.command.collection=n.arrayFromList(o.childNodes):"object"==typeof o?n.extend(i.command,o):"boolean"==typeof o?i.animate=o:"function"==typeof o&&(i.callback=o));if(i.command.index&&i.command.sibling)throw new Error(e.messages.errorInsertInvalidArguments());return!i.command.collection.length&&a.config.debug.showWarnings&&console.warn(e.messages.warningInsertNoElements()),i=a.callFilters("instructionParseInsertArgs",i,arguments),n.freeze(i),i},parseRemoveArgs:function(t){var a=this,i=new e.UserInstruction,o=null,r=null,s=-1;for(i.animate=a.config.animation.enable,i.command=new e.CommandRemove,s=0;s-1&&i.command.targets.push(o);return!i.command.targets.length&&a.config.debug.showWarnings&&console.warn(e.messages.warningRemoveNoElements()),n.freeze(i),i},parseDatasetArgs:function(t){var a=this,i=new e.UserInstruction,o=null,r=-1;for(i.animate=a.config.animation.enable,i.command=new e.CommandDataset,r=0;r-1&&t.toggleArray.splice(a,1),i=t.getToggleSelector(),t.multimix({filter:i},e.animate,e.callback)},sort:function(){var t=this,e=t.parseSortArgs(arguments);return t.multimix({sort:e.command},e.animate,e.callback)},changeLayout:function(){var t=this,e=t.parseChangeLayoutArgs(arguments);return t.multimix({changeLayout:e.command},e.animate,e.callback)},dataset:function(){var t=this,n=t.parseDatasetArgs(arguments),a=null,i=null,o=!1;return t.callActions("beforeDataset",arguments),t.isBusy?(i=new e.QueueItem,i.args=arguments,i.instruction=n,t.queueMix(i)):(n.callback&&(t.userCallback=n.callback),o=n.animate^t.config.animation.enable?n.animate:t.config.animation.enable,a=t.getDataOperation(n.command.dataset),t.goMix(o,a))},multimix:function(){var t=this,n=null,a=!1,i=null,o=t.parseMultimixArgs(arguments);return t.callActions("beforeMultimix",arguments),t.isBusy?(i=new e.QueueItem,i.args=arguments,i.instruction=o,i.triggerElement=t.lastClicked,i.isToggling=t.isToggling,t.queueMix(i)):(n=t.getOperation(o.command),t.config.controls.enable&&(o.command.filter&&!t.isToggling&&(t.toggleArray.length=0,t.buildToggleArray(n.command)),t.queue.length<1&&t.updateControls(n.command)),o.callback&&(t.userCallback=o.callback),a=o.animate^t.config.animation.enable?o.animate:t.config.animation.enable,t.callFilters("operationMultimix",n,arguments),t.goMix(a,n))},getOperation:function(t){var a=this,i=t.sort,o=t.filter,r=t.changeLayout,s=t.remove,l=t.insert,c=new e.Operation;return c=a.callFilters("operationUnmappedGetOperation",c,arguments),c.id=n.randomHex(),c.command=t,c.startState=a.state,c.triggerElement=a.lastClicked,a.isBusy?(a.config.debug.showWarnings&&console.warn(e.messages.warningGetOperationInstanceBusy()),null):(l&&a.insertTargets(l,c),s&&(c.toRemove=s.targets),c.startSort=c.newSort=c.startState.activeSort,c.startOrder=c.newOrder=a.targets,i&&(c.startSort=c.startState.activeSort,c.newSort=i,c.willSort=a.willSort(i,c.startState.activeSort),c.willSort&&a.sortOperation(c)),c.startFilter=c.startState.activeFilter,o?c.newFilter=o:c.newFilter=n.extend(new e.CommandFilter,c.startFilter),"all"===c.newFilter.selector?c.newFilter.selector=a.config.selectors.target:"none"===c.newFilter.selector&&(c.newFilter.selector=""),a.filterOperation(c),c.startContainerClassName=c.startState.activeContainerClassName,r?(c.newContainerClassName=r.containerClassName,c.newContainerClassName!==c.startContainerClassName&&(c.willChangeLayout=!0)):c.newContainerClassName=c.startContainerClassName,a.config.animation.enable&&(a.getStartMixData(c),a.setInter(c),c.docState=n.getDocumentState(a.dom.document),a.getInterMixData(c),a.setFinal(c),a.getFinalMixData(c),a.parseEffects(),c.hasEffect=a.hasEffect(),a.getTweenData(c)),c.willSort&&(a.targets=c.newOrder),c.newState=a.buildState(c),a.callFilters("operationMappedGetOperation",c,arguments))},tween:function(t,e){var n=null,a=null,i=-1,o=-1;for(e=Math.min(e,1),e=Math.max(e,0),o=0;n=t.show[o];o++)a=t.showPosData[o],n.applyTween(a,e);for(o=0;n=t.hide[o];o++)n.isShown&&n.hide(),(i=t.toHide.indexOf(n))>-1&&(a=t.toHidePosData[i],n.isShown||n.show(),n.applyTween(a,e))},insert:function(){var t=this,e=t.parseInsertArgs(arguments);return t.multimix({insert:e.command},e.animate,e.callback)},insertBefore:function(){var t=this,e=t.parseInsertArgs(arguments);return t.insert(e.command.collection,"before",e.command.sibling,e.animate,e.callback)},insertAfter:function(){var t=this,e=t.parseInsertArgs(arguments);return t.insert(e.command.collection,"after",e.command.sibling,e.animate,e.callback)},prepend:function(){var t=this,e=t.parseInsertArgs(arguments);return t.insert(0,e.command.collection,e.animate,e.callback)},append:function(){var t=this,e=t.parseInsertArgs(arguments);return t.insert(t.state.totalTargets,e.command.collection,e.animate,e.callback)},remove:function(){var t=this,e=t.parseRemoveArgs(arguments);return t.multimix({remove:e.command},e.animate,e.callback)},getConfig:function(t){var e=this,a=null;return a=t?n.getProperty(e.config,t):e.config,e.callFilters("valueGetConfig",a,arguments)},configure:function(t){var e=this;e.callActions("beforeConfigure",arguments),n.extend(e.config,t,!0,!0),e.callActions("afterConfigure",arguments)},getState:function(){var t=this,a=null;return a=new e.State,n.extend(a,t.state),n.freeze(a),t.callFilters("stateGetState",a,arguments)},forceRefresh:function(){var t=this;t.indexTargets()},forceRender:function(){var t=this,e=null,n=null,a="";for(a in t.cache)e=t.cache[a],n=e.render(e.data),n!==e.dom.el&&(e.isInDom&&(e.unbindEvents(),t.dom.parent.replaceChild(n,e.dom.el)),e.isShown||(n.style.display="none"),e.dom.el=n,e.isInDom&&e.bindEvents());t.state=t.buildState(t.lastOperation)},destroy:function(t){var n=this,a=null,i=null,o=0;for(n.callActions("beforeDestroy",arguments),o=0;a=n.controls[o];o++)a.removeBinding(n);for(o=0;i=n.targets[o];o++)t&&i.show(),i.unbindEvents();n.dom.container.id.match(/^MixItUp/)&&n.dom.container.removeAttribute("id"),delete e.instances[n.id],n.callActions("afterDestroy",arguments)}}),e.IMoveData=function(){e.Base.call(this),this.callActions("beforeConstruct"),this.posIn=null,this.posOut=null,this.operation=null,this.callback=null,this.statusChange="",this.duration=-1,this.staggerIndex=-1,this.callActions("afterConstruct"),n.seal(this)},e.BaseStatic.call(e.IMoveData),e.IMoveData.prototype=Object.create(e.Base.prototype),e.IMoveData.prototype.constructor=e.IMoveData,e.TargetDom=function(){e.Base.call(this),this.callActions("beforeConstruct"),this.el=null,this.callActions("afterConstruct"),n.seal(this)},e.BaseStatic.call(e.TargetDom),e.TargetDom.prototype=Object.create(e.Base.prototype),e.TargetDom.prototype.constructor=e.TargetDom,e.Target=function(){e.Base.call(this),this.callActions("beforeConstruct"),this.id="",this.sortString="",this.mixer=null,this.callback=null,this.isShown=!1,this.isBound=!1,this.isExcluded=!1,this.isInDom=!1,this.handler=null,this.operation=null,this.data=null,this.dom=new e.TargetDom,this.callActions("afterConstruct"),n.seal(this)},e.BaseStatic.call(e.Target),e.Target.prototype=Object.create(e.Base.prototype),n.extend(e.Target.prototype,{constructor:e.Target,init:function(t,n,a){var i=this,o="";if(i.callActions("beforeInit",arguments),i.mixer=n,t||(t=i.render(a)),i.cacheDom(t),i.bindEvents(),"none"!==i.dom.el.style.display&&(i.isShown=!0),a&&n.config.data.uidKey){if("undefined"==typeof(o=a[n.config.data.uidKey])||o.toString().length<1)throw new TypeError(e.messages.errorDatasetInvalidUidKey({uidKey:n.config.data.uidKey}));i.id=o,i.data=a,n.cache[o]=i}i.callActions("afterInit",arguments)},render:function(t){var a=this,i=null,o=null,r=null,s="";if(a.callActions("beforeRender",arguments),i=a.callFilters("renderRender",a.mixer.config.render.target,arguments),"function"!=typeof i)throw new TypeError(e.messages.errorDatasetRendererNotSet());return s=i(t),s&&"object"==typeof s&&n.isElement(s)?o=s:"string"==typeof s&&(r=document.createElement("div"),r.innerHTML=s,o=r.firstElementChild),a.callFilters("elRender",o,arguments)},cacheDom:function(t){var e=this;e.callActions("beforeCacheDom",arguments),e.dom.el=t,e.callActions("afterCacheDom",arguments)},getSortString:function(t){var e=this,n=e.dom.el.getAttribute("data-"+t)||"";e.callActions("beforeGetSortString",arguments),n=isNaN(1*n)?n.toLowerCase():1*n,e.sortString=n,e.callActions("afterGetSortString",arguments)},show:function(){var t=this;t.callActions("beforeShow",arguments),t.isShown||(t.dom.el.style.display="",t.isShown=!0),t.callActions("afterShow",arguments)},hide:function(){var t=this;t.callActions("beforeHide",arguments),t.isShown&&(t.dom.el.style.display="none",t.isShown=!1),t.callActions("afterHide",arguments)},move:function(t){var e=this;e.callActions("beforeMove",arguments),e.isExcluded||e.mixer.targetsMoved++,e.applyStylesIn(t),requestAnimationFrame(function(){e.applyStylesOut(t)}),e.callActions("afterMove",arguments)},applyTween:function(t,n){var a=this,i="",o=null,r=t.posIn,s=[],l=new e.StyleData,c=-1;for(a.callActions("beforeApplyTween",arguments),l.x=r.x,l.y=r.y,0===n?a.hide():a.isShown||a.show(),c=0;i=e.features.TWEENABLE[c];c++)if(o=t.tweenData[i],"x"===i){if(!o)continue;l.x=r.x+o*n}else if("y"===i){if(!o)continue;l.y=r.y+o*n}else if(o instanceof e.TransformData){if(!o.value)continue;l[i].value=r[i].value+o.value*n,l[i].unit=o.unit,s.push(i+"("+l[i].value+o.unit+")")}else{if(!o)continue;l[i]=r[i]+o*n,a.dom.el.style[i]=l[i]}(l.x||l.y)&&s.unshift("translate("+l.x+"px, "+l.y+"px)"),s.length&&(a.dom.el.style[e.features.transformProp]=s.join(" ")),a.callActions("afterApplyTween",arguments)},applyStylesIn:function(t){var n=this,a=t.posIn,i=1!==n.mixer.effectsIn.opacity,o=[];n.callActions("beforeApplyStylesIn",arguments),o.push("translate("+a.x+"px, "+a.y+"px)"),n.mixer.config.animation.animateResizeTargets&&("show"!==t.statusChange&&(n.dom.el.style.width=a.width+"px",n.dom.el.style.height=a.height+"px"),n.dom.el.style.marginRight=a.marginRight+"px",n.dom.el.style.marginBottom=a.marginBottom+"px"),i&&(n.dom.el.style.opacity=a.opacity),"show"===t.statusChange&&(o=o.concat(n.mixer.transformIn)),n.dom.el.style[e.features.transformProp]=o.join(" "),n.callActions("afterApplyStylesIn",arguments)},applyStylesOut:function(t){var n=this,a=[],i=[],o=n.mixer.config.animation.animateResizeTargets,r="undefined"!=typeof n.mixer.effectsIn.opacity;if(n.callActions("beforeApplyStylesOut",arguments),a.push(n.writeTransitionRule(e.features.transformRule,t.staggerIndex)),"none"!==t.statusChange&&a.push(n.writeTransitionRule("opacity",t.staggerIndex,t.duration)),o&&(a.push(n.writeTransitionRule("width",t.staggerIndex,t.duration)),a.push(n.writeTransitionRule("height",t.staggerIndex,t.duration)),a.push(n.writeTransitionRule("margin",t.staggerIndex,t.duration))),!t.callback)return n.mixer.targetsImmovable++,void(n.mixer.targetsMoved===n.mixer.targetsImmovable&&n.mixer.cleanUp(t.operation));switch(n.operation=t.operation,n.callback=t.callback,!n.isExcluded&&n.mixer.targetsBound++,n.isBound=!0,n.applyTransition(a),o&&t.posOut.width>0&&t.posOut.height>0&&(n.dom.el.style.width=t.posOut.width+"px",n.dom.el.style.height=t.posOut.height+"px",n.dom.el.style.marginRight=t.posOut.marginRight+"px",n.dom.el.style.marginBottom=t.posOut.marginBottom+"px"),n.mixer.config.animation.nudge||"hide"!==t.statusChange||i.push("translate("+t.posOut.x+"px, "+t.posOut.y+"px)"),t.statusChange){case"hide":r&&(n.dom.el.style.opacity=n.mixer.effectsOut.opacity),i=i.concat(n.mixer.transformOut);break;case"show":r&&(n.dom.el.style.opacity=1)}(n.mixer.config.animation.nudge||!n.mixer.config.animation.nudge&&"hide"!==t.statusChange)&&i.push("translate("+t.posOut.x+"px, "+t.posOut.y+"px)"),n.dom.el.style[e.features.transformProp]=i.join(" "),n.callActions("afterApplyStylesOut",arguments)},writeTransitionRule:function(t,e,n){var a=this,i=a.getDelay(e),o="";return o=t+" "+(n>0?n:a.mixer.config.animation.duration)+"ms "+i+"ms "+("opacity"===t?"linear":a.mixer.config.animation.easing),a.callFilters("ruleWriteTransitionRule",o,arguments)},getDelay:function(t){var e=this,n=-1;return"function"==typeof e.mixer.config.animation.staggerSequence&&(t=e.mixer.config.animation.staggerSequence.call(e,t,e.state)),n=e.mixer.staggerDuration?t*e.mixer.staggerDuration:0,e.callFilters("delayGetDelay",n,arguments)},applyTransition:function(t){var n=this,a=t.join(", ");n.callActions("beforeApplyTransition",arguments),n.dom.el.style[e.features.transitionProp]=a,n.callActions("afterApplyTransition",arguments)},handleTransitionEnd:function(t){var e=this,n=t.propertyName,a=e.mixer.config.animation.animateResizeTargets;e.callActions("beforeHandleTransitionEnd",arguments),e.isBound&&t.target.matches(e.mixer.config.selectors.target)&&(n.indexOf("transform")>-1||n.indexOf("opacity")>-1||a&&n.indexOf("height")>-1||a&&n.indexOf("width")>-1||a&&n.indexOf("margin")>-1)&&(e.callback.call(e,e.operation),e.isBound=!1,e.callback=null,e.operation=null),e.callActions("afterHandleTransitionEnd",arguments)},eventBus:function(t){var e=this;switch(e.callActions("beforeEventBus",arguments),t.type){case"webkitTransitionEnd":case"transitionend":e.handleTransitionEnd(t)}e.callActions("afterEventBus",arguments)},unbindEvents:function(){var t=this;t.callActions("beforeUnbindEvents",arguments),n.off(t.dom.el,"webkitTransitionEnd",t.handler),n.off(t.dom.el,"transitionend",t.handler),t.callActions("afterUnbindEvents",arguments)},bindEvents:function(){var t=this,a="";t.callActions("beforeBindEvents",arguments),a="webkit"===e.features.transitionPrefix?"webkitTransitionEnd":"transitionend",t.handler=function(e){return t.eventBus(e)},n.on(t.dom.el,a,t.handler),t.callActions("afterBindEvents",arguments)},getPosData:function(n){var a=this,i={},o=null,r=new e.StyleData;return a.callActions("beforeGetPosData",arguments),r.x=a.dom.el.offsetLeft,r.y=a.dom.el.offsetTop,(a.mixer.config.animation.animateResizeTargets||n)&&(o=a.dom.el.getBoundingClientRect(),r.top=o.top,r.right=o.right,r.bottom=o.bottom,r.left=o.left,r.width=o.width,r.height=o.height),a.mixer.config.animation.animateResizeTargets&&(i=t.getComputedStyle(a.dom.el),r.marginBottom=parseFloat(i.marginBottom),r.marginRight=parseFloat(i.marginRight)),a.callFilters("posDataGetPosData",r,arguments)},cleanUp:function(){var t=this;t.callActions("beforeCleanUp",arguments),t.dom.el.style[e.features.transformProp]="",t.dom.el.style[e.features.transitionProp]="",t.dom.el.style.opacity="",t.mixer.config.animation.animateResizeTargets&&(t.dom.el.style.width="",t.dom.el.style.height="",t.dom.el.style.marginRight="",t.dom.el.style.marginBottom=""),t.callActions("afterCleanUp",arguments)}}),e.Collection=function(t){var e=null,a=-1;for(this.callActions("beforeConstruct"),a=0;e=t[a];a++)this[a]=e;this.length=t.length,this.callActions("afterConstruct"),n.freeze(this)},e.BaseStatic.call(e.Collection),e.Collection.prototype=Object.create(e.Base.prototype),n.extend(e.Collection.prototype,{constructor:e.Collection,mixitup:function(t){var a=this,i=null,o=Array.prototype.slice.call(arguments),r=[],s=-1;for(this.callActions("beforeMixitup"),o.shift(),s=0;i=a[s];s++)r.push(i[t].apply(i,o));return a.callFilters("promiseMixitup",n.all(r,e.libraries),arguments)}}),e.Operation=function(){e.Base.call(this),this.callActions("beforeConstruct"),this.id="",this.args=[],this.command=null,this.showPosData=[],this.toHidePosData=[],this.startState=null,this.newState=null,this.docState=null,this.willSort=!1,this.willChangeLayout=!1,this.hasEffect=!1,this.hasFailed=!1,this.triggerElement=null,this.show=[],this.hide=[],this.matching=[],this.toShow=[],this.toHide=[],this.toMove=[],this.toRemove=[],this.startOrder=[],this.newOrder=[],this.startSort=null,this.newSort=null,this.startFilter=null,this.newFilter=null,this.startDataset=null,this.newDataset=null,this.viewportDeltaX=0,this.viewportDeltaY=0,this.startX=0,this.startY=0,this.startHeight=0,this.startWidth=0,this.newX=0,this.newY=0,this.newHeight=0,this.newWidth=0,this.startContainerClassName="",this.startDisplay="",this.newContainerClassName="",this.newDisplay="",this.callActions("afterConstruct"),n.seal(this)},e.BaseStatic.call(e.Operation),e.Operation.prototype=Object.create(e.Base.prototype),e.Operation.prototype.constructor=e.Operation,e.State=function(){e.Base.call(this),this.callActions("beforeConstruct"),this.id="",this.activeFilter=null,this.activeSort=null,this.activeContainerClassName="",this.container=null,this.targets=[],this.hide=[],this.show=[],this.matching=[],this.totalTargets=-1,this.totalShow=-1,this.totalHide=-1,this.totalMatching=-1,this.hasFailed=!1,this.triggerElement=null,this.activeDataset=null,this.callActions("afterConstruct"),n.seal(this)},e.BaseStatic.call(e.State),e.State.prototype=Object.create(e.Base.prototype),e.State.prototype.constructor=e.State,e.UserInstruction=function(){e.Base.call(this),this.callActions("beforeConstruct"),this.command={},this.animate=!1,this.callback=null,this.callActions("afterConstruct"),n.seal(this)},e.BaseStatic.call(e.UserInstruction),e.UserInstruction.prototype=Object.create(e.Base.prototype),e.UserInstruction.prototype.constructor=e.UserInstruction,e.Messages=function(){e.Base.call(this),this.callActions("beforeConstruct"),this.ERROR_FACTORY_INVALID_CONTAINER="[MixItUp] An invalid selector or element reference was passed to the mixitup factory function",this.ERROR_FACTORY_CONTAINER_NOT_FOUND="[MixItUp] The provided selector yielded no container element",this.ERROR_CONFIG_INVALID_ANIMATION_EFFECTS="[MixItUp] Invalid value for `animation.effects`",this.ERROR_CONFIG_INVALID_CONTROLS_SCOPE="[MixItUp] Invalid value for `controls.scope`",this.ERROR_CONFIG_INVALID_PROPERTY='[MixitUp] Invalid configuration object property "${erroneous}"${suggestion}',this.ERROR_CONFIG_INVALID_PROPERTY_SUGGESTION='. Did you mean "${probableMatch}"?',this.ERROR_CONFIG_DATA_UID_KEY_NOT_SET="[MixItUp] To use the dataset API, a UID key must be specified using `data.uidKey`",this.ERROR_DATASET_INVALID_UID_KEY='[MixItUp] The specified UID key "${uidKey}" is not present on one or more dataset items',this.ERROR_DATASET_DUPLICATE_UID='[MixItUp] The UID "${uid}" was found on two or more dataset items. UIDs must be unique.',this.ERROR_INSERT_INVALID_ARGUMENTS="[MixItUp] Please provider either an index or a sibling and position to insert, not both",this.ERROR_INSERT_PREEXISTING_ELEMENT="[MixItUp] An element to be inserted already exists in the container",this.ERROR_FILTER_INVALID_ARGUMENTS="[MixItUp] Please provide either a selector or collection `.filter()`, not both",this.ERROR_DATASET_NOT_SET="[MixItUp] To use the dataset API with pre-rendered targets, a starting dataset must be set using `load.dataset`",this.ERROR_DATASET_PRERENDERED_MISMATCH="[MixItUp] `load.dataset` does not match pre-rendered targets",this.ERROR_DATASET_RENDERER_NOT_SET="[MixItUp] To insert an element via the dataset API, a target renderer function must be provided to `render.target`",this.ERROR_SORT_NON_EXISTENT_ELEMENT="[MixItUp] An element to be sorted does not already exist in the container",this.WARNING_FACTORY_PREEXISTING_INSTANCE="[MixItUp] WARNING: This element already has an active MixItUp instance. The provided configuration object will be ignored. If you wish to perform additional methods on this instance, please create a reference.",this.WARNING_INSERT_NO_ELEMENTS="[MixItUp] WARNING: No valid elements were passed to `.insert()`",this.WARNING_REMOVE_NO_ELEMENTS="[MixItUp] WARNING: No valid elements were passed to `.remove()`",this.WARNING_MULTIMIX_INSTANCE_QUEUE_FULL="[MixItUp] WARNING: An operation was requested but the MixItUp instance was busy. The operation was rejected because the queue is full or queuing is disabled.",this.WARNING_GET_OPERATION_INSTANCE_BUSY="[MixItUp] WARNING: Operations can be be created while the MixItUp instance is busy.",this.WARNING_NO_PROMISE_IMPLEMENTATION="[MixItUp] WARNING: No Promise implementations could be found. If you wish to use promises with MixItUp please install an ES6 Promise polyfill.",this.WARNING_INCONSISTENT_SORTING_ATTRIBUTES='[MixItUp] WARNING: The requested sorting data attribute "${attribute}" was not present on one or more target elements which may product unexpected sort output',this.callActions("afterConstruct"),this.compileTemplates(),n.seal(this)},e.BaseStatic.call(e.Messages),e.Messages.prototype=Object.create(e.Base.prototype),e.Messages.prototype.constructor=e.Messages,e.Messages.prototype.compileTemplates=function(){var t="",e="";for(t in this)"string"==typeof(e=this[t])&&(this[n.camelCase(t)]=n.template(e))},e.messages=new e.Messages,e.Facade=function(t){e.Base.call(this),this.callActions("beforeConstruct",arguments),this.configure=t.configure.bind(t),this.show=t.show.bind(t),this.hide=t.hide.bind(t),this.filter=t.filter.bind(t),this.toggleOn=t.toggleOn.bind(t),this.toggleOff=t.toggleOff.bind(t),this.sort=t.sort.bind(t),this.changeLayout=t.changeLayout.bind(t),this.multimix=t.multimix.bind(t),this.dataset=t.dataset.bind(t),this.tween=t.tween.bind(t),this.insert=t.insert.bind(t),this.insertBefore=t.insertBefore.bind(t),this.insertAfter=t.insertAfter.bind(t),this.prepend=t.prepend.bind(t),this.append=t.append.bind(t),this.remove=t.remove.bind(t),this.destroy=t.destroy.bind(t),this.forceRefresh=t.forceRefresh.bind(t),this.forceRender=t.forceRender.bind(t),this.isMixing=t.isMixing.bind(t),this.getOperation=t.getOperation.bind(t),this.getConfig=t.getConfig.bind(t),this.getState=t.getState.bind(t),this.callActions("afterConstruct",arguments),n.freeze(this),n.seal(this)},e.BaseStatic.call(e.Facade),e.Facade.prototype=Object.create(e.Base.prototype),e.Facade.prototype.constructor=e.Facade,"object"==typeof exports&&"object"==typeof module?module.exports=e:"function"==typeof define&&define.amd?define(function(){return e}):"undefined"!=typeof t.mixitup&&"function"==typeof t.mixitup||(t.mixitup=e),e.BaseStatic.call(e.constructor),e.NAME="mixitup",e.CORE_VERSION="3.3.1"}(window); \ No newline at end of file diff --git a/themes/30coffe/assets/js/owl.carousel.min.js b/themes/30coffe/assets/js/owl.carousel.min.js new file mode 100644 index 00000000..fbbffc53 --- /dev/null +++ b/themes/30coffe/assets/js/owl.carousel.min.js @@ -0,0 +1,7 @@ +/** + * Owl Carousel v2.3.4 + * Copyright 2013-2018 David Deutsch + * Licensed under: SEE LICENSE IN https://github.com/OwlCarousel2/OwlCarousel2/blob/master/LICENSE + */ +!function(a,b,c,d){function e(b,c){this.settings=null,this.options=a.extend({},e.Defaults,c),this.$element=a(b),this._handlers={},this._plugins={},this._supress={},this._current=null,this._speed=null,this._coordinates=[],this._breakpoint=null,this._width=null,this._items=[],this._clones=[],this._mergers=[],this._widths=[],this._invalidated={},this._pipe=[],this._drag={time:null,target:null,pointer:null,stage:{start:null,current:null},direction:null},this._states={current:{},tags:{initializing:["busy"],animating:["busy"],dragging:["interacting"]}},a.each(["onResize","onThrottledResize"],a.proxy(function(b,c){this._handlers[c]=a.proxy(this[c],this)},this)),a.each(e.Plugins,a.proxy(function(a,b){this._plugins[a.charAt(0).toLowerCase()+a.slice(1)]=new b(this)},this)),a.each(e.Workers,a.proxy(function(b,c){this._pipe.push({filter:c.filter,run:a.proxy(c.run,this)})},this)),this.setup(),this.initialize()}e.Defaults={items:3,loop:!1,center:!1,rewind:!1,checkVisibility:!0,mouseDrag:!0,touchDrag:!0,pullDrag:!0,freeDrag:!1,margin:0,stagePadding:0,merge:!1,mergeFit:!0,autoWidth:!1,startPosition:0,rtl:!1,smartSpeed:250,fluidSpeed:!1,dragEndSpeed:!1,responsive:{},responsiveRefreshRate:200,responsiveBaseElement:b,fallbackEasing:"swing",slideTransition:"",info:!1,nestedItemSelector:!1,itemElement:"div",stageElement:"div",refreshClass:"owl-refresh",loadedClass:"owl-loaded",loadingClass:"owl-loading",rtlClass:"owl-rtl",responsiveClass:"owl-responsive",dragClass:"owl-drag",itemClass:"owl-item",stageClass:"owl-stage",stageOuterClass:"owl-stage-outer",grabClass:"owl-grab"},e.Width={Default:"default",Inner:"inner",Outer:"outer"},e.Type={Event:"event",State:"state"},e.Plugins={},e.Workers=[{filter:["width","settings"],run:function(){this._width=this.$element.width()}},{filter:["width","items","settings"],run:function(a){a.current=this._items&&this._items[this.relative(this._current)]}},{filter:["items","settings"],run:function(){this.$stage.children(".cloned").remove()}},{filter:["width","items","settings"],run:function(a){var b=this.settings.margin||"",c=!this.settings.autoWidth,d=this.settings.rtl,e={width:"auto","margin-left":d?b:"","margin-right":d?"":b};!c&&this.$stage.children().css(e),a.css=e}},{filter:["width","items","settings"],run:function(a){var b=(this.width()/this.settings.items).toFixed(3)-this.settings.margin,c=null,d=this._items.length,e=!this.settings.autoWidth,f=[];for(a.items={merge:!1,width:b};d--;)c=this._mergers[d],c=this.settings.mergeFit&&Math.min(c,this.settings.items)||c,a.items.merge=c>1||a.items.merge,f[d]=e?b*c:this._items[d].width();this._widths=f}},{filter:["items","settings"],run:function(){var b=[],c=this._items,d=this.settings,e=Math.max(2*d.items,4),f=2*Math.ceil(c.length/2),g=d.loop&&c.length?d.rewind?e:Math.max(e,f):0,h="",i="";for(g/=2;g>0;)b.push(this.normalize(b.length/2,!0)),h+=c[b[b.length-1]][0].outerHTML,b.push(this.normalize(c.length-1-(b.length-1)/2,!0)),i=c[b[b.length-1]][0].outerHTML+i,g-=1;this._clones=b,a(h).addClass("cloned").appendTo(this.$stage),a(i).addClass("cloned").prependTo(this.$stage)}},{filter:["width","items","settings"],run:function(){for(var a=this.settings.rtl?1:-1,b=this._clones.length+this._items.length,c=-1,d=0,e=0,f=[];++c",h)||this.op(b,"<",g)&&this.op(b,">",h))&&i.push(c);this.$stage.children(".active").removeClass("active"),this.$stage.children(":eq("+i.join("), :eq(")+")").addClass("active"),this.$stage.children(".center").removeClass("center"),this.settings.center&&this.$stage.children().eq(this.current()).addClass("center")}}],e.prototype.initializeStage=function(){this.$stage=this.$element.find("."+this.settings.stageClass),this.$stage.length||(this.$element.addClass(this.options.loadingClass),this.$stage=a("<"+this.settings.stageElement+">",{class:this.settings.stageClass}).wrap(a("
      ",{class:this.settings.stageOuterClass})),this.$element.append(this.$stage.parent()))},e.prototype.initializeItems=function(){var b=this.$element.find(".owl-item");if(b.length)return this._items=b.get().map(function(b){return a(b)}),this._mergers=this._items.map(function(){return 1}),void this.refresh();this.replace(this.$element.children().not(this.$stage.parent())),this.isVisible()?this.refresh():this.invalidate("width"),this.$element.removeClass(this.options.loadingClass).addClass(this.options.loadedClass)},e.prototype.initialize=function(){if(this.enter("initializing"),this.trigger("initialize"),this.$element.toggleClass(this.settings.rtlClass,this.settings.rtl),this.settings.autoWidth&&!this.is("pre-loading")){var a,b,c;a=this.$element.find("img"),b=this.settings.nestedItemSelector?"."+this.settings.nestedItemSelector:d,c=this.$element.children(b).width(),a.length&&c<=0&&this.preloadAutoWidthImages(a)}this.initializeStage(),this.initializeItems(),this.registerEventHandlers(),this.leave("initializing"),this.trigger("initialized")},e.prototype.isVisible=function(){return!this.settings.checkVisibility||this.$element.is(":visible")},e.prototype.setup=function(){var b=this.viewport(),c=this.options.responsive,d=-1,e=null;c?(a.each(c,function(a){a<=b&&a>d&&(d=Number(a))}),e=a.extend({},this.options,c[d]),"function"==typeof e.stagePadding&&(e.stagePadding=e.stagePadding()),delete e.responsive,e.responsiveClass&&this.$element.attr("class",this.$element.attr("class").replace(new RegExp("("+this.options.responsiveClass+"-)\\S+\\s","g"),"$1"+d))):e=a.extend({},this.options),this.trigger("change",{property:{name:"settings",value:e}}),this._breakpoint=d,this.settings=e,this.invalidate("settings"),this.trigger("changed",{property:{name:"settings",value:this.settings}})},e.prototype.optionsLogic=function(){this.settings.autoWidth&&(this.settings.stagePadding=!1,this.settings.merge=!1)},e.prototype.prepare=function(b){var c=this.trigger("prepare",{content:b});return c.data||(c.data=a("<"+this.settings.itemElement+"/>").addClass(this.options.itemClass).append(b)),this.trigger("prepared",{content:c.data}),c.data},e.prototype.update=function(){for(var b=0,c=this._pipe.length,d=a.proxy(function(a){return this[a]},this._invalidated),e={};b0)&&this._pipe[b].run(e),b++;this._invalidated={},!this.is("valid")&&this.enter("valid")},e.prototype.width=function(a){switch(a=a||e.Width.Default){case e.Width.Inner:case e.Width.Outer:return this._width;default:return this._width-2*this.settings.stagePadding+this.settings.margin}},e.prototype.refresh=function(){this.enter("refreshing"),this.trigger("refresh"),this.setup(),this.optionsLogic(),this.$element.addClass(this.options.refreshClass),this.update(),this.$element.removeClass(this.options.refreshClass),this.leave("refreshing"),this.trigger("refreshed")},e.prototype.onThrottledResize=function(){b.clearTimeout(this.resizeTimer),this.resizeTimer=b.setTimeout(this._handlers.onResize,this.settings.responsiveRefreshRate)},e.prototype.onResize=function(){return!!this._items.length&&(this._width!==this.$element.width()&&(!!this.isVisible()&&(this.enter("resizing"),this.trigger("resize").isDefaultPrevented()?(this.leave("resizing"),!1):(this.invalidate("width"),this.refresh(),this.leave("resizing"),void this.trigger("resized")))))},e.prototype.registerEventHandlers=function(){a.support.transition&&this.$stage.on(a.support.transition.end+".owl.core",a.proxy(this.onTransitionEnd,this)),!1!==this.settings.responsive&&this.on(b,"resize",this._handlers.onThrottledResize),this.settings.mouseDrag&&(this.$element.addClass(this.options.dragClass),this.$stage.on("mousedown.owl.core",a.proxy(this.onDragStart,this)),this.$stage.on("dragstart.owl.core selectstart.owl.core",function(){return!1})),this.settings.touchDrag&&(this.$stage.on("touchstart.owl.core",a.proxy(this.onDragStart,this)),this.$stage.on("touchcancel.owl.core",a.proxy(this.onDragEnd,this)))},e.prototype.onDragStart=function(b){var d=null;3!==b.which&&(a.support.transform?(d=this.$stage.css("transform").replace(/.*\(|\)| /g,"").split(","),d={x:d[16===d.length?12:4],y:d[16===d.length?13:5]}):(d=this.$stage.position(),d={x:this.settings.rtl?d.left+this.$stage.width()-this.width()+this.settings.margin:d.left,y:d.top}),this.is("animating")&&(a.support.transform?this.animate(d.x):this.$stage.stop(),this.invalidate("position")),this.$element.toggleClass(this.options.grabClass,"mousedown"===b.type),this.speed(0),this._drag.time=(new Date).getTime(),this._drag.target=a(b.target),this._drag.stage.start=d,this._drag.stage.current=d,this._drag.pointer=this.pointer(b),a(c).on("mouseup.owl.core touchend.owl.core",a.proxy(this.onDragEnd,this)),a(c).one("mousemove.owl.core touchmove.owl.core",a.proxy(function(b){var d=this.difference(this._drag.pointer,this.pointer(b));a(c).on("mousemove.owl.core touchmove.owl.core",a.proxy(this.onDragMove,this)),Math.abs(d.x)0^this.settings.rtl?"left":"right";a(c).off(".owl.core"),this.$element.removeClass(this.options.grabClass),(0!==d.x&&this.is("dragging")||!this.is("valid"))&&(this.speed(this.settings.dragEndSpeed||this.settings.smartSpeed),this.current(this.closest(e.x,0!==d.x?f:this._drag.direction)),this.invalidate("position"),this.update(),this._drag.direction=f,(Math.abs(d.x)>3||(new Date).getTime()-this._drag.time>300)&&this._drag.target.one("click.owl.core",function(){return!1})),this.is("dragging")&&(this.leave("dragging"),this.trigger("dragged"))},e.prototype.closest=function(b,c){var e=-1,f=30,g=this.width(),h=this.coordinates();return this.settings.freeDrag||a.each(h,a.proxy(function(a,i){return"left"===c&&b>i-f&&bi-g-f&&b",h[a+1]!==d?h[a+1]:i-g)&&(e="left"===c?a+1:a),-1===e},this)),this.settings.loop||(this.op(b,">",h[this.minimum()])?e=b=this.minimum():this.op(b,"<",h[this.maximum()])&&(e=b=this.maximum())),e},e.prototype.animate=function(b){var c=this.speed()>0;this.is("animating")&&this.onTransitionEnd(),c&&(this.enter("animating"),this.trigger("translate")),a.support.transform3d&&a.support.transition?this.$stage.css({transform:"translate3d("+b+"px,0px,0px)",transition:this.speed()/1e3+"s"+(this.settings.slideTransition?" "+this.settings.slideTransition:"")}):c?this.$stage.animate({left:b+"px"},this.speed(),this.settings.fallbackEasing,a.proxy(this.onTransitionEnd,this)):this.$stage.css({left:b+"px"})},e.prototype.is=function(a){return this._states.current[a]&&this._states.current[a]>0},e.prototype.current=function(a){if(a===d)return this._current;if(0===this._items.length)return d;if(a=this.normalize(a),this._current!==a){var b=this.trigger("change",{property:{name:"position",value:a}});b.data!==d&&(a=this.normalize(b.data)),this._current=a,this.invalidate("position"),this.trigger("changed",{property:{name:"position",value:this._current}})}return this._current},e.prototype.invalidate=function(b){return"string"===a.type(b)&&(this._invalidated[b]=!0,this.is("valid")&&this.leave("valid")),a.map(this._invalidated,function(a,b){return b})},e.prototype.reset=function(a){(a=this.normalize(a))!==d&&(this._speed=0,this._current=a,this.suppress(["translate","translated"]),this.animate(this.coordinates(a)),this.release(["translate","translated"]))},e.prototype.normalize=function(a,b){var c=this._items.length,e=b?0:this._clones.length;return!this.isNumeric(a)||c<1?a=d:(a<0||a>=c+e)&&(a=((a-e/2)%c+c)%c+e/2),a},e.prototype.relative=function(a){return a-=this._clones.length/2,this.normalize(a,!0)},e.prototype.maximum=function(a){var b,c,d,e=this.settings,f=this._coordinates.length;if(e.loop)f=this._clones.length/2+this._items.length-1;else if(e.autoWidth||e.merge){if(b=this._items.length)for(c=this._items[--b].width(),d=this.$element.width();b--&&!((c+=this._items[b].width()+this.settings.margin)>d););f=b+1}else f=e.center?this._items.length-1:this._items.length-e.items;return a&&(f-=this._clones.length/2),Math.max(f,0)},e.prototype.minimum=function(a){return a?0:this._clones.length/2},e.prototype.items=function(a){return a===d?this._items.slice():(a=this.normalize(a,!0),this._items[a])},e.prototype.mergers=function(a){return a===d?this._mergers.slice():(a=this.normalize(a,!0),this._mergers[a])},e.prototype.clones=function(b){var c=this._clones.length/2,e=c+this._items.length,f=function(a){return a%2==0?e+a/2:c-(a+1)/2};return b===d?a.map(this._clones,function(a,b){return f(b)}):a.map(this._clones,function(a,c){return a===b?f(c):null})},e.prototype.speed=function(a){return a!==d&&(this._speed=a),this._speed},e.prototype.coordinates=function(b){var c,e=1,f=b-1;return b===d?a.map(this._coordinates,a.proxy(function(a,b){return this.coordinates(b)},this)):(this.settings.center?(this.settings.rtl&&(e=-1,f=b+1),c=this._coordinates[b],c+=(this.width()-c+(this._coordinates[f]||0))/2*e):c=this._coordinates[f]||0,c=Math.ceil(c))},e.prototype.duration=function(a,b,c){return 0===c?0:Math.min(Math.max(Math.abs(b-a),1),6)*Math.abs(c||this.settings.smartSpeed)},e.prototype.to=function(a,b){var c=this.current(),d=null,e=a-this.relative(c),f=(e>0)-(e<0),g=this._items.length,h=this.minimum(),i=this.maximum();this.settings.loop?(!this.settings.rewind&&Math.abs(e)>g/2&&(e+=-1*f*g),a=c+e,(d=((a-h)%g+g)%g+h)!==a&&d-e<=i&&d-e>0&&(c=d-e,a=d,this.reset(c))):this.settings.rewind?(i+=1,a=(a%i+i)%i):a=Math.max(h,Math.min(i,a)),this.speed(this.duration(c,a,b)),this.current(a),this.isVisible()&&this.update()},e.prototype.next=function(a){a=a||!1,this.to(this.relative(this.current())+1,a)},e.prototype.prev=function(a){a=a||!1,this.to(this.relative(this.current())-1,a)},e.prototype.onTransitionEnd=function(a){if(a!==d&&(a.stopPropagation(),(a.target||a.srcElement||a.originalTarget)!==this.$stage.get(0)))return!1;this.leave("animating"),this.trigger("translated")},e.prototype.viewport=function(){var d;return this.options.responsiveBaseElement!==b?d=a(this.options.responsiveBaseElement).width():b.innerWidth?d=b.innerWidth:c.documentElement&&c.documentElement.clientWidth?d=c.documentElement.clientWidth:console.warn("Can not detect viewport width."),d},e.prototype.replace=function(b){this.$stage.empty(),this._items=[],b&&(b=b instanceof jQuery?b:a(b)),this.settings.nestedItemSelector&&(b=b.find("."+this.settings.nestedItemSelector)),b.filter(function(){return 1===this.nodeType}).each(a.proxy(function(a,b){b=this.prepare(b),this.$stage.append(b),this._items.push(b),this._mergers.push(1*b.find("[data-merge]").addBack("[data-merge]").attr("data-merge")||1)},this)),this.reset(this.isNumeric(this.settings.startPosition)?this.settings.startPosition:0),this.invalidate("items")},e.prototype.add=function(b,c){var e=this.relative(this._current);c=c===d?this._items.length:this.normalize(c,!0),b=b instanceof jQuery?b:a(b),this.trigger("add",{content:b,position:c}),b=this.prepare(b),0===this._items.length||c===this._items.length?(0===this._items.length&&this.$stage.append(b),0!==this._items.length&&this._items[c-1].after(b),this._items.push(b),this._mergers.push(1*b.find("[data-merge]").addBack("[data-merge]").attr("data-merge")||1)):(this._items[c].before(b),this._items.splice(c,0,b),this._mergers.splice(c,0,1*b.find("[data-merge]").addBack("[data-merge]").attr("data-merge")||1)),this._items[e]&&this.reset(this._items[e].index()),this.invalidate("items"),this.trigger("added",{content:b,position:c})},e.prototype.remove=function(a){(a=this.normalize(a,!0))!==d&&(this.trigger("remove",{content:this._items[a],position:a}),this._items[a].remove(),this._items.splice(a,1),this._mergers.splice(a,1),this.invalidate("items"),this.trigger("removed",{content:null,position:a}))},e.prototype.preloadAutoWidthImages=function(b){b.each(a.proxy(function(b,c){this.enter("pre-loading"),c=a(c),a(new Image).one("load",a.proxy(function(a){c.attr("src",a.target.src),c.css("opacity",1),this.leave("pre-loading"),!this.is("pre-loading")&&!this.is("initializing")&&this.refresh()},this)).attr("src",c.attr("src")||c.attr("data-src")||c.attr("data-src-retina"))},this))},e.prototype.destroy=function(){this.$element.off(".owl.core"),this.$stage.off(".owl.core"),a(c).off(".owl.core"),!1!==this.settings.responsive&&(b.clearTimeout(this.resizeTimer),this.off(b,"resize",this._handlers.onThrottledResize));for(var d in this._plugins)this._plugins[d].destroy();this.$stage.children(".cloned").remove(),this.$stage.unwrap(),this.$stage.children().contents().unwrap(),this.$stage.children().unwrap(),this.$stage.remove(),this.$element.removeClass(this.options.refreshClass).removeClass(this.options.loadingClass).removeClass(this.options.loadedClass).removeClass(this.options.rtlClass).removeClass(this.options.dragClass).removeClass(this.options.grabClass).attr("class",this.$element.attr("class").replace(new RegExp(this.options.responsiveClass+"-\\S+\\s","g"),"")).removeData("owl.carousel")},e.prototype.op=function(a,b,c){var d=this.settings.rtl;switch(b){case"<":return d?a>c:a":return d?ac;case">=":return d?a<=c:a>=c;case"<=":return d?a>=c:a<=c}},e.prototype.on=function(a,b,c,d){a.addEventListener?a.addEventListener(b,c,d):a.attachEvent&&a.attachEvent("on"+b,c)},e.prototype.off=function(a,b,c,d){a.removeEventListener?a.removeEventListener(b,c,d):a.detachEvent&&a.detachEvent("on"+b,c)},e.prototype.trigger=function(b,c,d,f,g){var h={item:{count:this._items.length,index:this.current()}},i=a.camelCase(a.grep(["on",b,d],function(a){return a}).join("-").toLowerCase()),j=a.Event([b,"owl",d||"carousel"].join(".").toLowerCase(),a.extend({relatedTarget:this},h,c));return this._supress[b]||(a.each(this._plugins,function(a,b){b.onTrigger&&b.onTrigger(j)}),this.register({type:e.Type.Event,name:b}),this.$element.trigger(j),this.settings&&"function"==typeof this.settings[i]&&this.settings[i].call(this,j)),j},e.prototype.enter=function(b){a.each([b].concat(this._states.tags[b]||[]),a.proxy(function(a,b){this._states.current[b]===d&&(this._states.current[b]=0),this._states.current[b]++},this))},e.prototype.leave=function(b){a.each([b].concat(this._states.tags[b]||[]),a.proxy(function(a,b){this._states.current[b]--},this))},e.prototype.register=function(b){if(b.type===e.Type.Event){if(a.event.special[b.name]||(a.event.special[b.name]={}),!a.event.special[b.name].owl){var c=a.event.special[b.name]._default;a.event.special[b.name]._default=function(a){return!c||!c.apply||a.namespace&&-1!==a.namespace.indexOf("owl")?a.namespace&&a.namespace.indexOf("owl")>-1:c.apply(this,arguments)},a.event.special[b.name].owl=!0}}else b.type===e.Type.State&&(this._states.tags[b.name]?this._states.tags[b.name]=this._states.tags[b.name].concat(b.tags):this._states.tags[b.name]=b.tags,this._states.tags[b.name]=a.grep(this._states.tags[b.name],a.proxy(function(c,d){return a.inArray(c,this._states.tags[b.name])===d},this)))},e.prototype.suppress=function(b){a.each(b,a.proxy(function(a,b){this._supress[b]=!0},this))},e.prototype.release=function(b){a.each(b,a.proxy(function(a,b){delete this._supress[b]},this))},e.prototype.pointer=function(a){var c={x:null,y:null};return a=a.originalEvent||a||b.event,a=a.touches&&a.touches.length?a.touches[0]:a.changedTouches&&a.changedTouches.length?a.changedTouches[0]:a,a.pageX?(c.x=a.pageX,c.y=a.pageY):(c.x=a.clientX,c.y=a.clientY),c},e.prototype.isNumeric=function(a){return!isNaN(parseFloat(a))},e.prototype.difference=function(a,b){return{x:a.x-b.x,y:a.y-b.y}},a.fn.owlCarousel=function(b){var c=Array.prototype.slice.call(arguments,1);return this.each(function(){var d=a(this),f=d.data("owl.carousel");f||(f=new e(this,"object"==typeof b&&b),d.data("owl.carousel",f),a.each(["next","prev","to","destroy","refresh","replace","add","remove"],function(b,c){f.register({type:e.Type.Event,name:c}),f.$element.on(c+".owl.carousel.core",a.proxy(function(a){a.namespace&&a.relatedTarget!==this&&(this.suppress([c]),f[c].apply(this,[].slice.call(arguments,1)),this.release([c]))},f))})),"string"==typeof b&&"_"!==b.charAt(0)&&f[b].apply(f,c)})},a.fn.owlCarousel.Constructor=e}(window.Zepto||window.jQuery,window,document),function(a,b,c,d){var e=function(b){this._core=b,this._interval=null,this._visible=null,this._handlers={"initialized.owl.carousel":a.proxy(function(a){a.namespace&&this._core.settings.autoRefresh&&this.watch()},this)},this._core.options=a.extend({},e.Defaults,this._core.options),this._core.$element.on(this._handlers)};e.Defaults={autoRefresh:!0,autoRefreshInterval:500},e.prototype.watch=function(){this._interval||(this._visible=this._core.isVisible(),this._interval=b.setInterval(a.proxy(this.refresh,this),this._core.settings.autoRefreshInterval))},e.prototype.refresh=function(){this._core.isVisible()!==this._visible&&(this._visible=!this._visible,this._core.$element.toggleClass("owl-hidden",!this._visible),this._visible&&this._core.invalidate("width")&&this._core.refresh())},e.prototype.destroy=function(){var a,c;b.clearInterval(this._interval);for(a in this._handlers)this._core.$element.off(a,this._handlers[a]);for(c in Object.getOwnPropertyNames(this))"function"!=typeof this[c]&&(this[c]=null)},a.fn.owlCarousel.Constructor.Plugins.AutoRefresh=e}(window.Zepto||window.jQuery,window,document),function(a,b,c,d){var e=function(b){this._core=b,this._loaded=[],this._handlers={"initialized.owl.carousel change.owl.carousel resized.owl.carousel":a.proxy(function(b){if(b.namespace&&this._core.settings&&this._core.settings.lazyLoad&&(b.property&&"position"==b.property.name||"initialized"==b.type)){var c=this._core.settings,e=c.center&&Math.ceil(c.items/2)||c.items,f=c.center&&-1*e||0,g=(b.property&&b.property.value!==d?b.property.value:this._core.current())+f,h=this._core.clones().length,i=a.proxy(function(a,b){this.load(b)},this);for(c.lazyLoadEager>0&&(e+=c.lazyLoadEager,c.loop&&(g-=c.lazyLoadEager,e++));f++-1||(e.each(a.proxy(function(c,d){var e,f=a(d),g=b.devicePixelRatio>1&&f.attr("data-src-retina")||f.attr("data-src")||f.attr("data-srcset");this._core.trigger("load",{element:f,url:g},"lazy"),f.is("img")?f.one("load.owl.lazy",a.proxy(function(){f.css("opacity",1),this._core.trigger("loaded",{element:f,url:g},"lazy")},this)).attr("src",g):f.is("source")?f.one("load.owl.lazy",a.proxy(function(){this._core.trigger("loaded",{element:f,url:g},"lazy")},this)).attr("srcset",g):(e=new Image,e.onload=a.proxy(function(){f.css({"background-image":'url("'+g+'")',opacity:"1"}),this._core.trigger("loaded",{element:f,url:g},"lazy")},this),e.src=g)},this)),this._loaded.push(d.get(0)))},e.prototype.destroy=function(){var a,b;for(a in this.handlers)this._core.$element.off(a,this.handlers[a]);for(b in Object.getOwnPropertyNames(this))"function"!=typeof this[b]&&(this[b]=null)},a.fn.owlCarousel.Constructor.Plugins.Lazy=e}(window.Zepto||window.jQuery,window,document),function(a,b,c,d){var e=function(c){this._core=c,this._previousHeight=null,this._handlers={"initialized.owl.carousel refreshed.owl.carousel":a.proxy(function(a){a.namespace&&this._core.settings.autoHeight&&this.update()},this),"changed.owl.carousel":a.proxy(function(a){a.namespace&&this._core.settings.autoHeight&&"position"===a.property.name&&this.update()},this),"loaded.owl.lazy":a.proxy(function(a){a.namespace&&this._core.settings.autoHeight&&a.element.closest("."+this._core.settings.itemClass).index()===this._core.current()&&this.update()},this)},this._core.options=a.extend({},e.Defaults,this._core.options),this._core.$element.on(this._handlers),this._intervalId=null;var d=this;a(b).on("load",function(){d._core.settings.autoHeight&&d.update()}),a(b).resize(function(){d._core.settings.autoHeight&&(null!=d._intervalId&&clearTimeout(d._intervalId),d._intervalId=setTimeout(function(){d.update()},250))})};e.Defaults={autoHeight:!1,autoHeightClass:"owl-height"},e.prototype.update=function(){var b=this._core._current,c=b+this._core.settings.items,d=this._core.settings.lazyLoad,e=this._core.$stage.children().toArray().slice(b,c),f=[],g=0;a.each(e,function(b,c){f.push(a(c).height())}),g=Math.max.apply(null,f),g<=1&&d&&this._previousHeight&&(g=this._previousHeight),this._previousHeight=g,this._core.$stage.parent().height(g).addClass(this._core.settings.autoHeightClass)},e.prototype.destroy=function(){var a,b;for(a in this._handlers)this._core.$element.off(a,this._handlers[a]);for(b in Object.getOwnPropertyNames(this))"function"!=typeof this[b]&&(this[b]=null)},a.fn.owlCarousel.Constructor.Plugins.AutoHeight=e}(window.Zepto||window.jQuery,window,document),function(a,b,c,d){var e=function(b){this._core=b,this._videos={},this._playing=null,this._handlers={"initialized.owl.carousel":a.proxy(function(a){a.namespace&&this._core.register({type:"state",name:"playing",tags:["interacting"]})},this),"resize.owl.carousel":a.proxy(function(a){a.namespace&&this._core.settings.video&&this.isInFullScreen()&&a.preventDefault()},this),"refreshed.owl.carousel":a.proxy(function(a){a.namespace&&this._core.is("resizing")&&this._core.$stage.find(".cloned .owl-video-frame").remove()},this),"changed.owl.carousel":a.proxy(function(a){a.namespace&&"position"===a.property.name&&this._playing&&this.stop()},this),"prepared.owl.carousel":a.proxy(function(b){if(b.namespace){var c=a(b.content).find(".owl-video");c.length&&(c.css("display","none"),this.fetch(c,a(b.content)))}},this)},this._core.options=a.extend({},e.Defaults,this._core.options),this._core.$element.on(this._handlers),this._core.$element.on("click.owl.video",".owl-video-play-icon",a.proxy(function(a){this.play(a)},this))};e.Defaults={video:!1,videoHeight:!1,videoWidth:!1},e.prototype.fetch=function(a,b){var c=function(){return a.attr("data-vimeo-id")?"vimeo":a.attr("data-vzaar-id")?"vzaar":"youtube"}(),d=a.attr("data-vimeo-id")||a.attr("data-youtube-id")||a.attr("data-vzaar-id"),e=a.attr("data-width")||this._core.settings.videoWidth,f=a.attr("data-height")||this._core.settings.videoHeight,g=a.attr("href");if(!g)throw new Error("Missing video URL.");if(d=g.match(/(http:|https:|)\/\/(player.|www.|app.)?(vimeo\.com|youtu(be\.com|\.be|be\.googleapis\.com|be\-nocookie\.com)|vzaar\.com)\/(video\/|videos\/|embed\/|channels\/.+\/|groups\/.+\/|watch\?v=|v\/)?([A-Za-z0-9._%-]*)(\&\S+)?/),d[3].indexOf("youtu")>-1)c="youtube";else if(d[3].indexOf("vimeo")>-1)c="vimeo";else{if(!(d[3].indexOf("vzaar")>-1))throw new Error("Video URL not supported.");c="vzaar"}d=d[6],this._videos[g]={type:c,id:d,width:e,height:f},b.attr("data-video",g),this.thumbnail(a,this._videos[g])},e.prototype.thumbnail=function(b,c){var d,e,f,g=c.width&&c.height?"width:"+c.width+"px;height:"+c.height+"px;":"",h=b.find("img"),i="src",j="",k=this._core.settings,l=function(c){e='
      ',d=k.lazyLoad?a("
      ",{class:"owl-video-tn "+j,srcType:c}):a("
      ",{class:"owl-video-tn",style:"opacity:1;background-image:url("+c+")"}),b.after(d),b.after(e)};if(b.wrap(a("
      ",{class:"owl-video-wrapper",style:g})),this._core.settings.lazyLoad&&(i="data-src",j="owl-lazy"),h.length)return l(h.attr(i)),h.remove(),!1;"youtube"===c.type?(f="//img.youtube.com/vi/"+c.id+"/hqdefault.jpg",l(f)):"vimeo"===c.type?a.ajax({type:"GET",url:"//vimeo.com/api/v2/video/"+c.id+".json",jsonp:"callback",dataType:"jsonp",success:function(a){f=a[0].thumbnail_large,l(f)}}):"vzaar"===c.type&&a.ajax({type:"GET",url:"//vzaar.com/api/videos/"+c.id+".json",jsonp:"callback",dataType:"jsonp",success:function(a){f=a.framegrab_url,l(f)}})},e.prototype.stop=function(){this._core.trigger("stop",null,"video"),this._playing.find(".owl-video-frame").remove(),this._playing.removeClass("owl-video-playing"),this._playing=null,this._core.leave("playing"),this._core.trigger("stopped",null,"video")},e.prototype.play=function(b){var c,d=a(b.target),e=d.closest("."+this._core.settings.itemClass),f=this._videos[e.attr("data-video")],g=f.width||"100%",h=f.height||this._core.$stage.height();this._playing||(this._core.enter("playing"),this._core.trigger("play",null,"video"),e=this._core.items(this._core.relative(e.index())),this._core.reset(e.index()),c=a(''),c.attr("height",h),c.attr("width",g),"youtube"===f.type?c.attr("src","//www.youtube.com/embed/"+f.id+"?autoplay=1&rel=0&v="+f.id):"vimeo"===f.type?c.attr("src","//player.vimeo.com/video/"+f.id+"?autoplay=1"):"vzaar"===f.type&&c.attr("src","//view.vzaar.com/"+f.id+"/player?autoplay=true"),a(c).wrap('
      ').insertAfter(e.find(".owl-video")),this._playing=e.addClass("owl-video-playing"))},e.prototype.isInFullScreen=function(){var b=c.fullscreenElement||c.mozFullScreenElement||c.webkitFullscreenElement;return b&&a(b).parent().hasClass("owl-video-frame")},e.prototype.destroy=function(){var a,b;this._core.$element.off("click.owl.video");for(a in this._handlers)this._core.$element.off(a,this._handlers[a]);for(b in Object.getOwnPropertyNames(this))"function"!=typeof this[b]&&(this[b]=null)},a.fn.owlCarousel.Constructor.Plugins.Video=e}(window.Zepto||window.jQuery,window,document),function(a,b,c,d){var e=function(b){this.core=b,this.core.options=a.extend({},e.Defaults,this.core.options),this.swapping=!0,this.previous=d,this.next=d,this.handlers={"change.owl.carousel":a.proxy(function(a){a.namespace&&"position"==a.property.name&&(this.previous=this.core.current(),this.next=a.property.value)},this),"drag.owl.carousel dragged.owl.carousel translated.owl.carousel":a.proxy(function(a){a.namespace&&(this.swapping="translated"==a.type)},this),"translate.owl.carousel":a.proxy(function(a){a.namespace&&this.swapping&&(this.core.options.animateOut||this.core.options.animateIn)&&this.swap()},this)},this.core.$element.on(this.handlers)};e.Defaults={animateOut:!1, +animateIn:!1},e.prototype.swap=function(){if(1===this.core.settings.items&&a.support.animation&&a.support.transition){this.core.speed(0);var b,c=a.proxy(this.clear,this),d=this.core.$stage.children().eq(this.previous),e=this.core.$stage.children().eq(this.next),f=this.core.settings.animateIn,g=this.core.settings.animateOut;this.core.current()!==this.previous&&(g&&(b=this.core.coordinates(this.previous)-this.core.coordinates(this.next),d.one(a.support.animation.end,c).css({left:b+"px"}).addClass("animated owl-animated-out").addClass(g)),f&&e.one(a.support.animation.end,c).addClass("animated owl-animated-in").addClass(f))}},e.prototype.clear=function(b){a(b.target).css({left:""}).removeClass("animated owl-animated-out owl-animated-in").removeClass(this.core.settings.animateIn).removeClass(this.core.settings.animateOut),this.core.onTransitionEnd()},e.prototype.destroy=function(){var a,b;for(a in this.handlers)this.core.$element.off(a,this.handlers[a]);for(b in Object.getOwnPropertyNames(this))"function"!=typeof this[b]&&(this[b]=null)},a.fn.owlCarousel.Constructor.Plugins.Animate=e}(window.Zepto||window.jQuery,window,document),function(a,b,c,d){var e=function(b){this._core=b,this._call=null,this._time=0,this._timeout=0,this._paused=!0,this._handlers={"changed.owl.carousel":a.proxy(function(a){a.namespace&&"settings"===a.property.name?this._core.settings.autoplay?this.play():this.stop():a.namespace&&"position"===a.property.name&&this._paused&&(this._time=0)},this),"initialized.owl.carousel":a.proxy(function(a){a.namespace&&this._core.settings.autoplay&&this.play()},this),"play.owl.autoplay":a.proxy(function(a,b,c){a.namespace&&this.play(b,c)},this),"stop.owl.autoplay":a.proxy(function(a){a.namespace&&this.stop()},this),"mouseover.owl.autoplay":a.proxy(function(){this._core.settings.autoplayHoverPause&&this._core.is("rotating")&&this.pause()},this),"mouseleave.owl.autoplay":a.proxy(function(){this._core.settings.autoplayHoverPause&&this._core.is("rotating")&&this.play()},this),"touchstart.owl.core":a.proxy(function(){this._core.settings.autoplayHoverPause&&this._core.is("rotating")&&this.pause()},this),"touchend.owl.core":a.proxy(function(){this._core.settings.autoplayHoverPause&&this.play()},this)},this._core.$element.on(this._handlers),this._core.options=a.extend({},e.Defaults,this._core.options)};e.Defaults={autoplay:!1,autoplayTimeout:5e3,autoplayHoverPause:!1,autoplaySpeed:!1},e.prototype._next=function(d){this._call=b.setTimeout(a.proxy(this._next,this,d),this._timeout*(Math.round(this.read()/this._timeout)+1)-this.read()),this._core.is("interacting")||c.hidden||this._core.next(d||this._core.settings.autoplaySpeed)},e.prototype.read=function(){return(new Date).getTime()-this._time},e.prototype.play=function(c,d){var e;this._core.is("rotating")||this._core.enter("rotating"),c=c||this._core.settings.autoplayTimeout,e=Math.min(this._time%(this._timeout||c),c),this._paused?(this._time=this.read(),this._paused=!1):b.clearTimeout(this._call),this._time+=this.read()%c-e,this._timeout=c,this._call=b.setTimeout(a.proxy(this._next,this,d),c-e)},e.prototype.stop=function(){this._core.is("rotating")&&(this._time=0,this._paused=!0,b.clearTimeout(this._call),this._core.leave("rotating"))},e.prototype.pause=function(){this._core.is("rotating")&&!this._paused&&(this._time=this.read(),this._paused=!0,b.clearTimeout(this._call))},e.prototype.destroy=function(){var a,b;this.stop();for(a in this._handlers)this._core.$element.off(a,this._handlers[a]);for(b in Object.getOwnPropertyNames(this))"function"!=typeof this[b]&&(this[b]=null)},a.fn.owlCarousel.Constructor.Plugins.autoplay=e}(window.Zepto||window.jQuery,window,document),function(a,b,c,d){"use strict";var e=function(b){this._core=b,this._initialized=!1,this._pages=[],this._controls={},this._templates=[],this.$element=this._core.$element,this._overrides={next:this._core.next,prev:this._core.prev,to:this._core.to},this._handlers={"prepared.owl.carousel":a.proxy(function(b){b.namespace&&this._core.settings.dotsData&&this._templates.push('
      '+a(b.content).find("[data-dot]").addBack("[data-dot]").attr("data-dot")+"
      ")},this),"added.owl.carousel":a.proxy(function(a){a.namespace&&this._core.settings.dotsData&&this._templates.splice(a.position,0,this._templates.pop())},this),"remove.owl.carousel":a.proxy(function(a){a.namespace&&this._core.settings.dotsData&&this._templates.splice(a.position,1)},this),"changed.owl.carousel":a.proxy(function(a){a.namespace&&"position"==a.property.name&&this.draw()},this),"initialized.owl.carousel":a.proxy(function(a){a.namespace&&!this._initialized&&(this._core.trigger("initialize",null,"navigation"),this.initialize(),this.update(),this.draw(),this._initialized=!0,this._core.trigger("initialized",null,"navigation"))},this),"refreshed.owl.carousel":a.proxy(function(a){a.namespace&&this._initialized&&(this._core.trigger("refresh",null,"navigation"),this.update(),this.draw(),this._core.trigger("refreshed",null,"navigation"))},this)},this._core.options=a.extend({},e.Defaults,this._core.options),this.$element.on(this._handlers)};e.Defaults={nav:!1,navText:['',''],navSpeed:!1,navElement:'button type="button" role="presentation"',navContainer:!1,navContainerClass:"owl-nav",navClass:["owl-prev","owl-next"],slideBy:1,dotClass:"owl-dot",dotsClass:"owl-dots",dots:!0,dotsEach:!1,dotsData:!1,dotsSpeed:!1,dotsContainer:!1},e.prototype.initialize=function(){var b,c=this._core.settings;this._controls.$relative=(c.navContainer?a(c.navContainer):a("
      ").addClass(c.navContainerClass).appendTo(this.$element)).addClass("disabled"),this._controls.$previous=a("<"+c.navElement+">").addClass(c.navClass[0]).html(c.navText[0]).prependTo(this._controls.$relative).on("click",a.proxy(function(a){this.prev(c.navSpeed)},this)),this._controls.$next=a("<"+c.navElement+">").addClass(c.navClass[1]).html(c.navText[1]).appendTo(this._controls.$relative).on("click",a.proxy(function(a){this.next(c.navSpeed)},this)),c.dotsData||(this._templates=[a('',nextArrow:'',autoplay:!1,autoplaySpeed:3e3,centerMode:!1,centerPadding:"50px",cssEase:"ease",customPaging:function(e,t){return i('