diff --git a/.idea/cms-niceties.iml b/.idea/cms-niceties.iml
new file mode 100644
index 0000000..c956989
--- /dev/null
+++ b/.idea/cms-niceties.iml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
new file mode 100644
index 0000000..4c91e9e
--- /dev/null
+++ b/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/_config.php b/_config.php
new file mode 100755
index 0000000..f9f25bd
--- /dev/null
+++ b/_config.php
@@ -0,0 +1,39 @@
+enablePlugins([
+ 'template',
+ 'fullscreen',
+ 'hr',
+ 'contextmenu',
+ 'charmap',
+ 'visualblocks',
+ 'lists',
+ 'charcount' => ModuleResourceLoader::resourceURL(
+ 'drmartingonzo/ss-tinymce-charcount:client/dist/js/bundle.js'
+ ),
+]);
+$config->addButtonsToLine(2, 'hr');
+$config->setOption('block_formats', 'Paragraph=p;Heading 2=h2;Heading 3=h3;Heading 4=h4;Heading 5=h5;Heading 6=h6;Address=address;Pre=pre');
+$config->setOption('invalid_elements', 'h1');
+$config->setOption(
+ 'table_class_list',
+ [
+ ['title' => 'Transparent Table', 'value' => 'table-none'],
+ ['title' => 'Shaded rows', 'value' => 'table table-striped table-bordered'],
+ ]
+);
+
+FulltextSearchable::enable();
+
+// replace embed parser
+/*$parser = ShortcodeParser::get('default');
+$parser->unregister('embed');
+$parser->register('embed', [EmbedShortcodeProvider::class, 'handle_shortcode']);*/
diff --git a/_config/base-config.yml b/_config/base-config.yml
new file mode 100755
index 0000000..e99f8d8
--- /dev/null
+++ b/_config/base-config.yml
@@ -0,0 +1,28 @@
+---
+Name: webapp-base-config
+---
+SilverStripe\Core\Manifest\ModuleManifest:
+ project: app
+
+Page:
+ default_container_class: 'container'
+
+#SilverStripe\Admin\LeftAndMain:
+# extra_requirements_javascript:
+# - 'colymba/gridfield-bulk-editing-tools:client/dist/js/main.js'
+# - 'colymba/gridfield-bulk-editing-tools:client/lang/en.js'
+# extra_requirements_css:
+# - 'colymba/gridfield-bulk-editing-tools:client/dist/styles/main.css'
+
+SilverStripe\Admin\LeftAndMain:
+ extra_requirements_javascript:
+ - 'app/client/dist/js/app_cms.js'
+ extra_requirements_css:
+ - 'app/client/dist/css/app_cms.css'
+
+SilverStripe\Forms\HTMLEditor\TinyMCEConfig:
+ editor_css:
+ - 'app/client/dist/css/app_editor.css'
+
+SilverStripe\Control\Email\Email:
+ send_all_emails_from: noreply@twma.pro
diff --git a/_config/base-extensions.yml b/_config/base-extensions.yml
new file mode 100755
index 0000000..5ddc89c
--- /dev/null
+++ b/_config/base-extensions.yml
@@ -0,0 +1,47 @@
+---
+Name: webapp-base-extensions
+---
+# Basic extensions
+SilverStripe\Admin\LeftAndMain:
+ extensions:
+ - A2nt\CMSNiceties\Extensions\LeftAndMainExtension
+
+SilverStripe\SiteConfig\SiteConfig:
+ extensions:
+ - A2nt\CMSNiceties\Extensions\SocialExtension
+ - A2nt\CMSNiceties\Extensions\SiteConfigExtension
+ - A2nt\CMSNiceties\Extensions\NotificationsExtension
+
+SilverStripe\CMS\Model\SiteTree:
+ extensions:
+ - A2nt\CMSNiceties\Extensions\SiteTreeExtension
+
+Sheadawson\Linkable\Models\EmbeddedObject:
+ extensions:
+ - A2nt\CMSNiceties\Extensions\EmbeddedObjectExtension
+
+SilverStripe\Assets:
+ extensions:
+ - A2nt\CMSNiceties\Extensions\ImageExtension
+
+Dynamic\FlexSlider\Model\SlideImage:
+ extensions:
+ - A2nt\CMSNiceties\Extensions\SlideImageExtension
+
+SilverStripe\Core\Injector\Injector:
+ #SilverStripe\UserForms\Model\UserDefinedForm:
+ # class: A2nt\CMSNiceties\Extensions\UserDefinedForm_HiddenClass
+ Sheadawson\Linkable\Forms\EmbeddedObjectField:
+ class: A2nt\CMSNiceties\Extensions\EmbedObjectField
+ SilverStripe\Forms\CompositeField:
+ class: A2nt\CMSNiceties\Extensions\CompositeFieldExtension
+
+SilverStripe\UserForms\Form\UserForm:
+ extensions:
+ - A2nt\CMSNiceties\Extensions\PlaceholderFormExtension
+
+Page:
+ searchable_objects:
+ - A2nt\CMSNiceties\Models\TeamMember
+ extensions:
+ - DNADesign\Elemental\Extensions\ElementalPageExtension
diff --git a/_config/base-files.yml b/_config/base-files.yml
new file mode 100755
index 0000000..881603a
--- /dev/null
+++ b/_config/base-files.yml
@@ -0,0 +1,81 @@
+---
+Name: webapp-base-files
+---
+SilverStripe\Blog\Model\BlogPost:
+ featured_images_directory: 'blog-posts'
+
+SilverStripe\Assets\Upload_Validator:
+ allowedExtensions:
+ - 'stl'
+
+SilverStripe\Assets\File:
+ allowed_extensions:
+ - 'ace'
+ - 'arc'
+ - 'arj'
+ - 'asf'
+ - 'au'
+ - 'avi'
+ - 'bmp'
+ - 'bz2'
+ - 'cab'
+ - 'cda'
+ - 'csv'
+ - 'dmg'
+ - 'doc'
+ - 'docx'
+ - 'dotx'
+ - 'flv'
+ - 'gif'
+ - 'gpx'
+ - 'gz'
+ - 'hqx'
+ - 'ico'
+ - 'jpeg'
+ - 'jpg'
+ - 'kml'
+ - 'm4a'
+ - 'm4v'
+ - 'mid'
+ - 'midi'
+ - 'mkv'
+ - 'mov'
+ - 'mp3'
+ - 'mp4'
+ - 'mpa'
+ - 'mpeg'
+ - 'mpg'
+ - 'ogg'
+ - 'ogv'
+ - 'pages'
+ - 'pcx'
+ - 'pdf'
+ - 'png'
+ - 'pps'
+ - 'ppt'
+ - 'pptx'
+ - 'potx'
+ - 'ra'
+ - 'ram'
+ - 'rm'
+ - 'rtf'
+ - 'sit'
+ - 'sitx'
+ - 'tar'
+ - 'tgz'
+ - 'tif'
+ - 'tiff'
+ - 'txt'
+ - 'wav'
+ - 'webm'
+ - 'wma'
+ - 'wmv'
+ - 'xls'
+ - 'xlsx'
+ - 'xltx'
+ - 'zip'
+ - 'zipx'
+ - 'stl'
+ app_categories:
+ document:
+ - 'stl'
diff --git a/_config/base-graphql.yml b/_config/base-graphql.yml
new file mode 100644
index 0000000..9f98f7e
--- /dev/null
+++ b/_config/base-graphql.yml
@@ -0,0 +1,38 @@
+---
+Name: webapp-base-graphql
+After: graphqlconfig
+---
+SilverStripe\Control\Director:
+ rules:
+ 'graphql': '%$SilverStripe\GraphQL\Controller.default'
+
+SilverStripe\GraphQL\Controller:
+ cors:
+ Enabled: true
+ Allow-Origin: '*'
+ Allow-Headers: 'Authorization, Content-Type, Content-Language, apikey'
+ Allow-Methods: 'GET, PUT, DELETE, OPTIONS, POST'
+ #Allow-Credentials: 'true'
+ Max-Age: 600 # 600 seconds = 10 minutes.
+
+SilverStripe\GraphQL\Auth\Handler:
+ authenticators:
+ - class: A2nt\CMSNiceties\GraphQL\APIKeyAuthenticator
+ priority: 30
+
+SilverStripe\GraphQL\Manager.default:
+ properties:
+ Middlewares:
+ APIKeyMiddleware: A2nt\CMSNiceties\GraphQL\APIKeyMiddleware
+
+SilverStripe\GraphQL\Manager:
+ schemas:
+ default:
+ types:
+ member: 'A2nt\CMSNiceties\GraphQL\MemberTypeCreator'
+ page: 'A2nt\CMSNiceties\GraphQL\PageTypeCreator'
+ element: 'A2nt\CMSNiceties\GraphQL\ElementTypeCreator'
+ queries:
+ readPages: 'A2nt\CMSNiceties\GraphQL\PaginatedReadPagesQueryCreator'
+ readMembers: 'A2nt\CMSNiceties\GraphQL\ReadMembersQueryCreator'
+ paginatedReadMembers: 'A2nt\CMSNiceties\GraphQL\PaginatedReadMembersQueryCreator'
diff --git a/_config/base-logs.yml_ b/_config/base-logs.yml_
new file mode 100755
index 0000000..3da07ff
--- /dev/null
+++ b/_config/base-logs.yml_
@@ -0,0 +1,60 @@
+---
+Name: webapp-base-logs-dev
+Only:
+ environment: dev
+---
+SilverStripe\Core\Injector\Injector:
+ Psr\Log\LoggerInterface.errorhandler:
+ calls:
+ pushMyDisplayErrorHandler: [pushHandler, ['%$DisplayErrorHandler']]
+ DisplayErrorHandler:
+ class: SilverStripe\Logging\HTTPOutputHandler
+ constructor:
+ - 'notice'
+ properties:
+ Formatter: '%$SilverStripe\Logging\DetailedErrorFormatter'
+ CLIFormatter: '%$SilverStripe\Logging\DetailedErrorFormatter'
+---
+Name: webapp-base-logs-live
+Except:
+ environment: dev
+---
+SilverStripe\Core\Injector\Injector:
+ # Default logger implementation for general purpose use
+ Psr\Log\LoggerInterface:
+ calls:
+ # Save system logs to file
+ pushFileLogHandler: [pushHandler, ['%$LogFileHandler']]
+
+ # Core error handler for system use
+ Psr\Log\LoggerInterface.errorhandler:
+ calls:
+ # Save errors to file
+ pushFileLogHandler: [pushHandler, ['%$LogFileHandler']]
+ # Format and display errors in the browser/CLI
+ pushMyDisplayErrorHandler: [pushHandler, ['%$DisplayErrorHandler']]
+
+ # Custom handler to log to a file
+ LogFileHandler:
+ class: Monolog\Handler\StreamHandler
+ constructor:
+ - '../silverstripe.log'
+ - 'notice'
+ properties:
+ Formatter: '%$Monolog\Formatter\HtmlFormatter'
+ ContentType: text/html
+
+ # Handler for displaying errors in the browser or CLI
+ DisplayErrorHandler:
+ class: SilverStripe\Logging\HTTPOutputHandler
+ constructor:
+ - 'error'
+ properties:
+ Formatter: '%$SilverStripe\Logging\DebugViewFriendlyErrorFormatter'
+
+ # Configuration for the "friendly" error formatter
+ SilverStripe\Logging\DebugViewFriendlyErrorFormatter:
+ class: SilverStripe\Logging\DebugViewFriendlyErrorFormatter
+ properties:
+ Title: 'There has been an error'
+ Body: 'The website server has not been able to respond to your request'
diff --git a/_config/base-mimevalidator.yml b/_config/base-mimevalidator.yml
new file mode 100644
index 0000000..d1261af
--- /dev/null
+++ b/_config/base-mimevalidator.yml
@@ -0,0 +1,9 @@
+---
+Name: webapp-base-mimeuploadvalidator
+After: '#mimeuploadvalidator'
+Only:
+ moduleexists: 'silverstripe/mimevalidator'
+---
+SilverStripe\Core\Injector\Injector:
+ SilverStripe\Assets\Upload_Validator:
+ class: SilverStripe\MimeValidator\MimeUploadValidator
diff --git a/_config/base-security.yml b/_config/base-security.yml
new file mode 100755
index 0000000..e7b4c8a
--- /dev/null
+++ b/_config/base-security.yml
@@ -0,0 +1,105 @@
+---
+Name: 'webapp-base-security'
+After: 'framework/*, cms/*, security_baseline'
+---
+SilverStripe\Core\Injector\Injector:
+ SilverStripe\Security\MemberAuthenticator\LostPasswordHandler:
+ class: A2nt\CMSNiceties\Extensions\LostPasswordHandlerExtension
+ SilverStripe\Security\MemberAuthenticator\MemberLoginForm:
+ class: A2nt\CMSNiceties\Extensions\SiteMemberLoginForm
+---
+Except:
+ environment: dev
+---
+# Secure cookies
+SilverStripe\Control\Session:
+ cookie_secure: true
+ strict_user_agent_check: false
+ timeout: 604800
+
+SilverStripe\Forms\PasswordField:
+ autocompleate: false
+
+SilverStripe\Security\Member:
+ lock_out_after_incorrect_logins: 5
+ lock_out_delay_mins: 5
+ # Password expiry should only happen when the password is leaked (optionally expire automatically if PCI/NIST compliance is required)
+ # password_expiry_days: 90
+ # instead of password change, we send out a notice on change of password OR Email (notify_account_security_change)
+ notify_password_change: false
+#######################
+# Security Headers
+#######################
+#Controller:
+# security_headers:
+# # # Values may contain :security_reporting_base_url: placeholders, will be replaced with the URL to SecurityBaselineController endpoint
+# # Header-Directive: "value; another value;"
+# # X-Version-Alias-Of-Same-Header: "x:Header-Directive" # 'x-alias' headers may be aliased to the standard by a value starting with "x:Standard"
+# # X-Another-Alias-Version-Of-Same: "different; value syntax as well;"
+
+#
+# A useful base from guttmann/silverstripe-security-headers - https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers#Security:
+#
+
+# # Content-Security-Policy - https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP
+# # Specifies approved sources of content that the browser may load from your website
+# # Useful: upgrade-insecure-requests; (Instructs browser to treat a site's insecure URLs as if they are HTTPS (eg for legacy sites)
+# # Example: Allow everything but only from the same origin:
+# Content-Security-Policy: "default-src 'self';"
+# # Example: Allow Google Analytics, Google AJAX CDN and Same Origin
+# Content-Security-Policy: "script-src 'self' www.google-analytics.com ajax.googleapis.com;"
+# # Example: Starter Policy - allows images, scripts, AJAX, form actions, and CSS from the same origin, and does not allow any
+# # other resources to load (eg object, frame, media, etc). It is a good starting point for many sites.
+# Content-Security-Policy: "default-src 'none'; script-src 'self'; connect-src 'self'; img-src 'self'; style-src 'self';base-uri 'self';form-action 'self'"
+
+# # Content-Security-Policy-Report-Only - https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only
+# # Allows web developers to experiment with policies by monitoring (but not enforcing) their effects
+# # Browsers capable of enforcing CSP will send a violation report as a POST request to report-uri
+# Content-Security-Policy-Report-Only: default-src https:; report-uri /security-reporting-endpoint/csp/
+# Content-Security-Policy-Report-Only: "default-src https:; script-src 'self' https: 'unsafe-inline' 'unsafe-eval'; connect-src 'self'; img-src 'self' https: data:; style-src 'self' 'unsafe-inline'; base-uri 'self'; form-action 'self'; report-uri /security-reporting-endpoint/csp/;"
+
+# # Strict-Transport-Security - https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security
+# # Tells the browser to ONLY interact with the site using HTTPS and never HTTP
+# Strict-Transport-Security: "max-age=31536000" # time in seconds (one year=31536000) to remember that the site is only accessible over HTTPS
+
+# # Frame-Options - https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options
+# # Disallowes pages to render within a frame - protects against clickjacking attacks
+# Frame-Options: "deny"
+
+# # XSS-Protection - https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-XSS-Protection
+# # protect against Cross-site Scripting attacks (value 1=sanitize (default in most browsers), set to "1; mode=block" to prevent rendering if attack is detected)
+# # Deprecated: if you do not need to support legacy browsers, it is recommended that you use Content-Security-Policy without allowing unsafe-inline scripts instead
+# X-XSS-Protection: "1; mode=block"
+
+# # X-Content-Type-Options - https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Content-Type-Options
+# # Indicate that the MIME types advertised in the Content-Type headers should not be changed and be followed
+# # NOTE: Opting out of MIME sniffing can cause HTML web pages to be downloaded instead of rendered when they are
+# # served with a MIME type other than text/html. Make sure to set both headers correctly.
+# # Site security testers usually expect this header to be set.
+# X-Content-Type-Options: "nosniff"
+
+#
+# Some more from https://help.dreamhost.com/hc/en-us/articles/360036486952-Security-headers
+#
+
+# # Referrer-Policy - https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy
+# # controls how much referrer information should be sent to another server
+# Referrer-Policy: no-referrer
+
+# # Feature-Policy - https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Feature-Policy
+# # (Experimental 2020) controls which browser features are allowed on your website, eg for sites allowing third-party content
+
+# # CORS - Allow resource sharing with another domain (eg webfonts & ajax requests)
+# # Access-Control-Allow-Origin - developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin
+
+#
+# A further selection from https://github.com/bepsvpt/secure-headers/blob/master/config/secure-headers.php
+#
+
+# Clear-Site-Data - https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Clear-Site-Data
+# Clears browsing data (cookies, storage, cache) associated with the requesting website
+
+# Expect-CT - https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Expect-CT
+# Lets sites opt in to reporting and/or enforcement of Certificate Transparency requirements,
+# to prevent the use of misissued certificates for that site from going unnoticed.
+# (will likely become obsolete in June 2021)
diff --git a/_config/base-version-truncator.yml b/_config/base-version-truncator.yml
new file mode 100755
index 0000000..66bca45
--- /dev/null
+++ b/_config/base-version-truncator.yml
@@ -0,0 +1,8 @@
+---
+Name: 'webapp-base-version-truncator'
+---
+Axllent\VersionTruncator\VersionTruncator:
+ keep_versions: 4 # how many (published) versions of each page to keep
+ keep_drafts: 2 # how many drafts of each page to keep
+ keep_redirects: true # keep page versions that have a different URLSegment (for redirects)
+ keep_old_page_types: false # keep page versions where page type (ClassName) has changed
diff --git a/_config/debugbar.yml b/_config/debugbar.yml
new file mode 100755
index 0000000..2154cb2
--- /dev/null
+++ b/_config/debugbar.yml
@@ -0,0 +1,12 @@
+---
+Name: 'webapp-debugbar'
+After:
+ - 'framework'
+ - 'debugbar'
+Only:
+ environment: 'dev'
+---
+LeKoala\DebugBar\DebugBar:
+ enabled_in_admin: false
+ query_limit: 500
+ max_header_length: 2048
diff --git a/_config/env-check.yml b/_config/env-check.yml
new file mode 100755
index 0000000..8d3b8fc
--- /dev/null
+++ b/_config/env-check.yml
@@ -0,0 +1,28 @@
+---
+Name: webapp-env-check
+---
+
+SilverStripe\EnvironmentCheck\EnvironmentCheckSuite:
+ registered_checks:
+ curl:
+ definition: 'HasFunctionCheck("curl_init")'
+ title: 'is curl available?'
+ gd:
+ definition: 'HasFunctionCheck("imagecreatetruecolor")'
+ title: 'Does PHP have GD2 support?'
+ db:
+ definition: 'DatabaseCheck("Page")'
+ title: 'Is the database accessible?'
+ url:
+ definition: 'URLCheck()'
+ title: 'Is the homepage accessible?'
+ registered_suites:
+ check:
+ - curl
+ - gd
+ - db
+ - url
+ health:
+ - db
+ - url
+
diff --git a/_config/locale-fluent.yml_ b/_config/locale-fluent.yml_
new file mode 100755
index 0000000..83bbe2b
--- /dev/null
+++ b/_config/locale-fluent.yml_
@@ -0,0 +1,42 @@
+---
+Name: webapp-locale-fluent
+After:
+ - webapp-extensions
+ - webapp-elemental
+ - webapp-locale
+---
+# Define Fluent locales
+TractorCow\Fluent\Model\Locale:
+ default_records:
+ en:
+ Title: 'EN'
+ Locale: en_US
+ URLSegment: en
+ IsGlobalDefault: 1
+ us:
+ Locale: en_US
+ Title: 'EN'
+ URLSegment: en
+ ru:
+ Locale: ru_RU
+ Title: 'RU'
+ URLSegment: ru
+ Fallbacks:
+ - =>TractorCow\Fluent\Model\Locale.us
+
+# Enable Fluent extensions
+Page:
+ extensions:
+ - DNADesign\Elemental\TopPage\SiteTreeExtension
+ - DNADesign\Elemental\Extensions\ElementalPageExtension
+ - A2nt\CMSNiceties\Extensions\PageFluentExtension
+
+DNADesign\Elemental\Models\ElementalArea:
+ extensions:
+ - DNADesign\Elemental\TopPage\FluentExtension
+ - A2nt\CMSNiceties\Extensions\ElementalArea
+
+DNADesign\Elemental\Models\BaseElement:
+ extensions:
+ - DNADesign\Elemental\TopPage\FluentExtension
+ - A2nt\CMSNiceties\Extensions\ElementRows
diff --git a/_config/locale.yml b/_config/locale.yml
new file mode 100755
index 0000000..d29471d
--- /dev/null
+++ b/_config/locale.yml
@@ -0,0 +1,59 @@
+---
+Name: webapp-locale
+---
+
+Symbiote\Addressable\Addressable:
+ allowed_countries:
+ 'us': 'United States'
+ allowed_states:
+ 'AL': 'Alabama'
+ 'AK': 'Alaska'
+ 'AZ': 'Arizona'
+ 'AR': 'Arkansas'
+ 'CA': 'California'
+ 'CO': 'Colorado'
+ 'CT': 'Connecticut'
+ 'DE': 'Delaware'
+ 'DC': 'District Of Columbia'
+ 'FL': 'Florida'
+ 'GA': 'Georgia'
+ 'HI': 'Hawaii'
+ 'ID': 'Idaho'
+ 'IL': 'Illinois'
+ 'IN': 'Indiana'
+ 'IA': 'Iowa'
+ 'KS': 'Kansas'
+ 'KY': 'Kentucky'
+ 'LA': 'Louisiana'
+ 'ME': 'Maine'
+ 'MD': 'Maryland'
+ 'MA': 'Massachusetts'
+ 'MI': 'Michigan'
+ 'MN': 'Minnesota'
+ 'MS': 'Mississippi'
+ 'MO': 'Missouri'
+ 'MT': 'Montana'
+ 'NE': 'Nebraska'
+ 'NV': 'Nevada'
+ 'NH': 'New Hampshire'
+ 'NJ': 'New Jersey'
+ 'NM': 'New Mexico'
+ 'NY': 'New York'
+ 'NC': 'North Carolina'
+ 'ND': 'North Dakota'
+ 'OH': 'Ohio'
+ 'OK': 'Oklahoma'
+ 'OR': 'Oregon'
+ 'PA': 'Pennsylvania'
+ 'RI': 'Rhode Island'
+ 'SC': 'South Carolina'
+ 'SD': 'South Dakota'
+ 'TN': 'Tennessee'
+ 'TX': 'Texas'
+ 'UT': 'Utah'
+ 'VT': 'Vermont'
+ 'VA': 'Virginia'
+ 'WA': 'Washington'
+ 'WV': 'West Virginia'
+ 'WI': 'Wisconsin'
+ 'WY': 'Wyoming'
diff --git a/_config/options-elements.yml b/_config/options-elements.yml
new file mode 100755
index 0000000..d7b2da6
--- /dev/null
+++ b/_config/options-elements.yml
@@ -0,0 +1,52 @@
+---
+Name: webapp-options-elements
+After:
+ - elemental
+ - elemental-list
+ - elementalvirtual
+ - webapp-base-extensions
+---
+Page:
+ searchable_elements:
+ - DNADesign\Elemental\Models\ElementContent
+ extensions:
+ - DNADesign\Elemental\Extensions\ElementalPageExtension
+
+SilverStripe\CMS\Model\SiteTree:
+ allowed_elements:
+ - DNADesign\ElementalList\Model\ElementList
+ - DNADesign\Elemental\Models\ElementContent
+ - DNADesign\ElementalUserForms\Model\ElementForm
+ - Dynamic\Elements\Image\Elements\ElementImage
+ - Dynamic\Elements\Blog\Elements\ElementBlogPosts
+ - Dynamic\Elements\Oembed\Elements\ElementOembed
+ - Dynamic\Elements\Elements\ElementTestimonials
+ #- A2nt\ElementalBasics\Elements\TeamMembersElement
+ - A2nt\ElementalBasics\Elements\SliderElement
+ - A2nt\ElementalBasics\Elements\MapElement
+ #- A2nt\ElementalBasics\Elements\AccordionElement
+ - DNADesign\ElementalVirtual\Model\ElementVirtual
+ - A2nt\ElementalBasics\Elements\AccordionElement
+ - A2nt\ElementalBasics\Elements\CustomSnippetElement
+ - A2nt\ElementalBasics\Elements\InstagramElement
+
+DNADesign\ElementalList\Model\ElementList:
+ inline_editable: false
+ default_global_elements: false
+ allowed_elements:
+ - DNADesign\ElementalList\Model\ElementList
+ - DNADesign\Elemental\Models\ElementContent
+ - DNADesign\ElementalUserForms\Model\ElementForm
+ - Dynamic\Elements\Image\Elements\ElementImage
+ - Dynamic\Elements\Blog\Elements\ElementBlogPosts
+ - Dynamic\Elements\Oembed\Elements\ElementOembed
+ - Dynamic\Elements\Elements\ElementTestimonials
+ #- A2nt\ElementalBasics\Elements\TeamMembersElement
+ - A2nt\ElementalBasics\Elements\SliderElement
+ - A2nt\ElementalBasics\Elements\MapElement
+ - A2nt\ElementalBasics\Elements\AccordionElement
+ - A2nt\ElementalBasics\Elements\CustomSnippetElement
+ - A2nt\ElementalBasics\Elements\InstagramElement
+ styles:
+ whiteframe: 'White Frame'
+ noframe: 'No Frame'
diff --git a/_config/options-widgets.yml b/_config/options-widgets.yml
new file mode 100755
index 0000000..a0f1ab1
--- /dev/null
+++ b/_config/options-widgets.yml
@@ -0,0 +1,33 @@
+---
+Name: webapp-options-widgets
+---
+# Blog + Widgets module extensions
+Page:
+ extensions:
+ - A2nt\CMSNiceties\Widgets\WidgetPageExtension
+
+SilverStripe\Blog\Model\Blog:
+ extensions:
+ - A2nt\CMSNiceties\Extensions\BlogExtension
+SilverStripe\Blog\Model\BlogPost:
+ extensions:
+ - A2nt\CMSNiceties\Extensions\BlogPostExtension
+
+SilverStripe\Widgets\Model\Widget:
+ icon: ''
+ extensions:
+ - A2nt\CMSNiceties\Widgets\WidgetExtension
+SilverStripe\Blog\Widgets\BlogArchiveWidget:
+ icon: ''
+SilverStripe\Blog\Widgets\BlogCategoriesWidget:
+ icon: ''
+SilverStripe\Blog\Widgets\BlogFeaturedPostsWidget:
+ icon: ''
+SilverStripe\Blog\Widgets\BlogRecentPostsWidget:
+ icon: ''
+SilverStripe\Blog\Widgets\BlogTagsCloudWidget:
+ icon: ''
+SilverStripe\Blog\Widgets\BlogTagsWidget:
+ icon: ''
+ only_available_in:
+ - CMSMain_HiddenClass
diff --git a/_config/shop.yml_ b/_config/shop.yml_
new file mode 100755
index 0000000..41ac173
--- /dev/null
+++ b/_config/shop.yml_
@@ -0,0 +1,23 @@
+---
+Name: webapp-shop
+---
+SilverStripe\Core\Injector\Injector:
+ SilverShop\Checkout\SinglePageCheckoutComponentConfig:
+ class: A2nt\CMSNiceties\Models\CheckoutNoDeliveryConfig
+
+SilverShop\Extension\ShopConfigExtension:
+ base_currency: USD
+
+SilverShop\Model\Address:
+ extensions:
+ - A2nt\CMSNiceties\Extensions\AddressExtension
+
+SilverShop\Cart\ShoppingCartController:
+ extensions:
+ - A2nt\CMSNiceties\Extensions\ShoppingCartControllerExtension
+
+A2nt\CMSNiceties\Templates\DeferedRequirements:
+ custom_requirements:
+ SilverShop\Page\AccountPageController:
+ - SilverShop.Page.CheckoutPageController.js
+ - SilverShop.Page.CheckoutPageController.css
diff --git a/_config/templates-requirements.yml b/_config/templates-requirements.yml
new file mode 100755
index 0000000..0691bce
--- /dev/null
+++ b/_config/templates-requirements.yml
@@ -0,0 +1,41 @@
+---
+Name: webapp-templates-requirements
+---
+App\Templates\DeferredRequirements:
+ nofontawesome: false
+ version: false
+ static_domain: false
+ deferred: true
+ noreact: false
+ nojquery: true
+ jquery_version: '3.4.1'
+
+SilverStripe\FontAwesome\FontAwesomeField:
+ version: '5.12.0'
+
+SilverStripe\View\Requirements:
+ disable_flush_combined: true
+SilverStripe\View\Requirements_Backend:
+ combine_in_dev: true
+ combine_hash_querystring: true
+ default_combined_files_folder: 'combined'
+SilverStripe\Core\Injector\Injector:
+ # Create adapter that points to the custom directory root
+ SilverStripe\Assets\Flysystem\PublicAdapter.custom-adapter:
+ class: SilverStripe\Assets\Flysystem\PublicAssetAdapter
+ constructor:
+ Root: ./app/javascript
+ # Set flysystem filesystem that uses this adapter
+ League\Flysystem\Filesystem.custom-filesystem:
+ class: 'League\Flysystem\Filesystem'
+ constructor:
+ Adapter: '%$SilverStripe\Assets\Flysystem\PublicAdapter.custom-adapter'
+ # Create handler to generate assets using this filesystem
+ SilverStripe\Assets\Storage\GeneratedAssetHandler.custom-generated-assets:
+ class: SilverStripe\Assets\Flysystem\GeneratedAssets
+ properties:
+ Filesystem: '%$League\Flysystem\Filesystem.custom-filesystem'
+ # Assign this generator to the requirements builder
+ SilverStripe\View\Requirements_Backend:
+ properties:
+ AssetHandler: '%$SilverStripe\Assets\Storage\GeneratedAssetHandler.custom-generated-assets'
diff --git a/_config/templates-themes.yml b/_config/templates-themes.yml
new file mode 100755
index 0000000..c26bf12
--- /dev/null
+++ b/_config/templates-themes.yml
@@ -0,0 +1,19 @@
+---
+Name: webapp-templates-themes
+After:
+ - webapp-options-elements
+---
+SilverStripe\View\SSViewer:
+ source_file_comments: true
+ themes:
+ - '$public'
+ - '$default'
+
+App\Elements\SliderElement:
+ slide_width: 2140
+ slide_height: 700
+
+# 2x container width to automatically resize images for 2K display
+App\Elements\Extensions\ElementRows:
+ container_max_width: 2280
+ column_class: 'col-block col-md'
diff --git a/_config/webpack.yml b/_config/webpack.yml
new file mode 100755
index 0000000..a358a90
--- /dev/null
+++ b/_config/webpack.yml
@@ -0,0 +1,21 @@
+# Name: webapp-webpack
+# that's important to place this file into /app/_config/webpack.yml
+# with all configuration variables presented
+# Cuz WebPack compiling script use it to set configuration
+
+A2nt\CMSNiceties\Templates\WebpackTemplateProvider:
+ APPDIR: './app'
+ THEMESDIR: './themes'
+ HOSTNAME: 127.0.0.1
+ PORT: 3000
+ SRC: client/src
+ DIST: client/dist
+ TYPESJS: client/src/js/types
+ TYPESSCSS: client/src/scss/types
+ webp: false
+ NODE_ENV: production #production,development
+ HTTPS: true
+ injectClient: true
+ GRAPHQL_URL: '/graphql'
+ GRAPHQL_API_KEY: 'LgPaRkVPYa8IY7x3AjbLC8wx6oPPSlO01yPflFXecvQ'
+ #STATIC_URL: 'http://127.0.0.1'
diff --git a/composer.json b/composer.json
new file mode 100644
index 0000000..e515f4d
--- /dev/null
+++ b/composer.json
@@ -0,0 +1,27 @@
+{
+ "name": "a2nt/cms-niceties",
+ "description": "Some useful CMS updates",
+ "type": "silverstripe-vendormodule",
+ "keywords": [
+ "silverstripe",
+ "elemental"
+ ],
+ "license": "BSD-3-Clause",
+ "authors": [{
+ "name": "Tony Air",
+ "email": "tony@twma.pro"
+ }],
+ "minimum-stability": "dev",
+ "require": {
+ "silverstripe/cms": "^4",
+ "a2nt/silverstripe-elemental-basics": "*"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^5.7"
+ },
+ "autoload": {
+ "psr-4": {
+ "A2nt\\ElementalBasics\\": "src/"
+ }
+ }
+}
diff --git a/lang/en.yml b/lang/en.yml
new file mode 100755
index 0000000..5e70948
--- /dev/null
+++ b/lang/en.yml
@@ -0,0 +1,31 @@
+en:
+ SilverStripe\Security\Member:
+ SURNAME: 'Last Name'
+ db_Surname: 'Last Name'
+ SUBJECTPASSWORDRESET: 'Your password reset link'
+ ENTEREMAIL: 'Please enter an email address to get a password reset link.'
+ PASSWORDRESETSENTHEADER: 'Password reset link sent'
+ PASSWORDRESETSENTTEXT: 'Thank you. A reset link has been sent, provided an account exists for this email address.'
+ Page:
+ LOADINGTEXT: 'LOADING ..'
+ JAVASCRIPTREQUIRED: 'Please, enable javascript!'
+ UPGRADEBROWSER: 'Upgrade your browser'
+ OUTDATEDBROWSER: 'You are using an outdated browser. For a faster, safer browsing experience, upgrade for free today.'
+ SilverShop\Model\Address:
+ db_Surname: 'Last Name'
+ SilverShop\Model\Order:
+ db_Surname: 'Last Name'
+ SilverShop\Page\CheckoutPage:
+ ProceedToPayment: 'Send Order to Store'
+ UndefinedOffset\NoCaptcha\Forms\NocaptchaField:
+ EMPTY: 'Please prove you are human - check the Captcha box.'
+ NOSCRIPT: 'You must enable JavaScript to submit this form'
+ VALIDATE_ERROR: 'Captcha could not be validated'
+ Dynamic\FlexSlider\Model\SlideImage:
+ SINGULARNAME: 'Slide'
+ PLURALNAME: 'Slides'
+ Addressable:
+ SUBURB: 'City'
+ Symbiote\AddressableAddressable:
+ SUBURB: 'City'
+
diff --git a/lang/ru.yml b/lang/ru.yml
new file mode 100755
index 0000000..8804957
--- /dev/null
+++ b/lang/ru.yml
@@ -0,0 +1,19 @@
+ru:
+ Page:
+ LOADINGTEXT: "ЗАГРУЗКА .."
+ JAVASCRIPTREQUIRED: "Для корректной работы страницы требуется включить Javascript!"
+ UPGRADEBROWSER: "Обновите браузер!"
+ OUTDATEDBROWSER: "Вы используете устаревшую версию браузера. Обновите ваш браузер сейчас для повышения уровня безопасности вашей системы."
+ PaginationLabel: "Переключение страниц"
+ PaginationPrevious: "Назад"
+ PaginationNext: "Вперед"
+ SilverStripe\Blog\Model\Blog:
+ PostedIn: "Категории"
+ Tagged: "Отметки"
+ Comments: "Коментарии"
+ Posted: "Опубликовано"
+ By: " - "
+ AND: "и"
+ LessThanAMinuteToRead: "Меньше минуты на чтение"
+ MinutesToRead: "Минут на чтение"
+ READMORE: "Подробнее"
diff --git a/src/Extensions/AddressExtension.php b/src/Extensions/AddressExtension.php
new file mode 100755
index 0000000..a6e439c
--- /dev/null
+++ b/src/Extensions/AddressExtension.php
@@ -0,0 +1,29 @@
+push($field);
+ $fields->remove($field);
+ }
+
+ $holder->addExtraClass('col-sm-6');
+ $fields->push($holder);
+ }
+}
diff --git a/src/Extensions/BlogExtension.php b/src/Extensions/BlogExtension.php
new file mode 100755
index 0000000..53c1c4f
--- /dev/null
+++ b/src/Extensions/BlogExtension.php
@@ -0,0 +1,19 @@
+dataFieldByName('ChildPages');
+ if ($f) {
+ $f->setConfig(GridFieldConfigBlogPost::create(75));
+ }
+ }
+}
diff --git a/src/Extensions/BlogPostExtension.php b/src/Extensions/BlogPostExtension.php
new file mode 100755
index 0000000..7a2d7cf
--- /dev/null
+++ b/src/Extensions/BlogPostExtension.php
@@ -0,0 +1,27 @@
+ 'Boolean(0)',
+ ];
+
+ public function updateCMSFields(FieldList $fields)
+ {
+ $mainTab = $fields->findOrMakeTab('Root.Main');
+ $mainTab->push(CheckboxField::create('Featured'));
+ }
+}
diff --git a/src/Extensions/CMSMain_HiddenClass.php b/src/Extensions/CMSMain_HiddenClass.php
new file mode 100755
index 0000000..87162bc
--- /dev/null
+++ b/src/Extensions/CMSMain_HiddenClass.php
@@ -0,0 +1,17 @@
+getName();
+
+ $fields = [
+ CheckboxField::create(
+ $name . '[autoplay]',
+ _t(self::CLASS.'AUTOPLAY', 'Autoplay video?')
+ )->setValue($this->object->getField('Autoplay')),
+
+ CheckboxField::create(
+ $name . '[loop]',
+ _t(self::CLASS.'LOOP', 'Loop video?')
+ )->setValue($this->object->getField('Loop')),
+
+ CheckboxField::create(
+ $name.'[controls]',
+ _t(self::CLASS.'CONTROLS', 'Show player controls?')
+ )->setValue($this->object->getField('Controls'))
+ ];
+
+ return CompositeField::create(array_merge([
+ LiteralField::create(
+ $name.'Options',
+ parent::FieldHolder($properties)
+ )
+ ], $fields));
+ }
+}
diff --git a/src/Extensions/EmbedShortcodeProvider.php b/src/Extensions/EmbedShortcodeProvider.php
new file mode 100755
index 0000000..4ee503f
--- /dev/null
+++ b/src/Extensions/EmbedShortcodeProvider.php
@@ -0,0 +1,52 @@
+
+ if (!empty($arguments['caption'])) {
+ $xmlCaption = Convert::raw2xml($arguments['caption']);
+ $content .= "\n
{$xmlCaption}
";
+ }
+
+ // Convert arguments to data-*argument_name*
+ foreach ($arguments as $k => $v) {
+ if($k === 'class' || $k === 'style') {
+ continue;
+ }
+
+ unset($arguments[$k]);
+ $arguments['data-'.$k] = $v;
+ }
+
+ $arguments['class'] .= ' embed-youtube embed-responsive embed-responsive-16by9';
+
+ $iframe = strpos($content, 'iframe');
+ if($iframe >= 0) {
+ $content = substr($content, 0, $iframe+6).' class="embed-responsive-item" '.substr($content, $iframe +7);
+ }
+
+ return HTML::createTag('div', $arguments, $content);
+ }
+}
diff --git a/src/Extensions/EmbeddedObjectExtension.php b/src/Extensions/EmbeddedObjectExtension.php
new file mode 100755
index 0000000..93477c8
--- /dev/null
+++ b/src/Extensions/EmbeddedObjectExtension.php
@@ -0,0 +1,136 @@
+ 'Boolean(0)',
+ 'Loop' => 'Boolean(0)',
+ 'Controls' => 'Boolean(1)',
+ ];
+
+ public function Embed()
+ {
+ $this->owner->Embed();
+ $this->setEmbedParams();
+
+ return $this->owner;
+ }
+
+ public function setEmbedParams($params = [])
+ {
+ $iframe_params = [];
+ if ($this->owner->getField('Autoplay')) {
+ $iframe_params[] = 'allow="autoplay"';
+ }
+
+ // YouTube params
+ if (stripos($this->owner->EmbedHTML, 'https://www.youtube.com/embed/') > 0) {
+ $url = $this->owner->getField('SourceURL');
+ preg_match(
+ '/^(?:http(?:s)?:\/\/)?(?:www\.)?(?:m\.)?(?:youtu\.be\/|youtube\.com\/(?:(?:watch)?\?(?:.*&)?v(?:i)?=|(?:embed|v|vi|user)\/))([^\?&"\'>]+)/',
+ $url,
+ $matches
+ );
+ if (isset($matches[1])) {
+ $videoID = $matches[1];
+
+ $params = array_merge($params, [
+ 'feature=oembed',
+ 'wmode=transparent',
+ 'enablejsapi=1',
+ 'disablekb=1',
+ 'iv_load_policy=3',
+ 'modestbranding=1',
+ 'rel=0',
+ 'showinfo=0',
+ //'controls='.($this->owner->getField('Controls') ? '1': '0')
+ ]);
+
+ if ($this->owner->getField('Autoplay')) {
+ $params[] = 'autoplay=1';
+ $params[] = 'mute=1';
+ }
+
+ if ($this->owner->getField('Loop')) {
+ $params[] = 'loop=1';
+ $params[] = 'playlist=' . $videoID;
+ }
+
+ $this->owner->EmbedHTML = preg_replace(
+ '/src="([A-z0-9:\/\.]+)\??(.*?)"/',
+ 'src="https://www.youtube.com/embed/' . $videoID . '?' . implode('&', $params) . '" '
+ . implode(' ', $iframe_params),
+ $this->owner->EmbedHTML
+ );
+ }
+ }
+
+ if (stripos($this->owner->EmbedHTML, 'https://player.vimeo.com/video/') > 0) {
+ $url = $this->owner->getField('SourceURL');
+ preg_match(
+ '/^https:\/\/vimeo\.com\/([A-z0-9]+)/',
+ $url,
+ $matches
+ );
+ $videoID = $matches[1];
+
+ $params = array_merge($params, [
+ 'controls='.($this->owner->getField('Controls') ? '1': '0'),
+ 'background=1',
+ ]);
+
+ if ($this->owner->getField('Autoplay')) {
+ $params[] = 'autoplay=1';
+ }
+
+ if ($this->owner->getField('Loop')) {
+ $params[] = 'loop=1';
+ }
+ $this->owner->EmbedHTML = preg_replace(
+ '/src="([A-z0-9:\/\.]+)\??(.*?)"/',
+ 'src="https://player.vimeo.com/video/'.$videoID.'?' . implode('&', $params) . '" '
+ .implode(' ', $iframe_params),
+ $this->owner->EmbedHTML
+ );
+ }
+ }
+
+ public function updateCMSFields(FieldList $fields)
+ {
+ parent::updateCMSFields($fields);
+
+ $fields->removeByName([
+ 'Width', 'Height', 'EmbedHTML', 'ThumbURL',
+ 'Autoplay', 'Loop', 'Controls',
+ 'ExtraClass', 'Type',
+ ]);
+
+ $fields->addFieldsToTab('Root.Extra', [
+ CheckboxField::create('Autoplay'),
+ CheckboxField::create('Loop'),
+ CheckboxField::create('Controls'),
+ NumericField::create('Width'),
+ NumericField::create('Height'),
+ TextareaField::create('EmbedHTML'),
+ TextField::create('ThumbURL'),
+ TextField::create('ExtraClass'),
+ TextField::create('Type'),
+ ]);
+ }
+
+ public function onBeforeWrite()
+ {
+ parent::onBeforeWrite();
+ $this->setEmbedParams();
+ }
+}
diff --git a/src/Extensions/HtmlEditorFieldExtension.php b/src/Extensions/HtmlEditorFieldExtension.php
new file mode 100755
index 0000000..c436d05
--- /dev/null
+++ b/src/Extensions/HtmlEditorFieldExtension.php
@@ -0,0 +1,19 @@
+byID($page_id)->URLSegment;
+
+ $computerUploadField = $form->Fields()->dataFieldByName('AssetUploadField');
+ $computerUploadField->setFolderName(sprintf("%s/images/%s", 'Uploads', $page_urlsegment));
+ }
+}
diff --git a/src/Extensions/ImageExtension.php b/src/Extensions/ImageExtension.php
new file mode 100755
index 0000000..a3aef40
--- /dev/null
+++ b/src/Extensions/ImageExtension.php
@@ -0,0 +1,23 @@
+removeByName([
+ 'Filename',
+ ]);*/
+ }
+}
diff --git a/src/Extensions/LeftAndMainExtension.php b/src/Extensions/LeftAndMainExtension.php
new file mode 100755
index 0000000..943f15c
--- /dev/null
+++ b/src/Extensions/LeftAndMainExtension.php
@@ -0,0 +1,26 @@
+get(DeferredRequirements::class);
+ // App libs
+ if (!$config['nofontawesome']) {
+ $v = !isset($config['fontawesome_version']) || !$config['fontawesome_version']
+ ? Config::inst()->get(FontAwesomeField::class, 'version')
+ : $config['fontawesome_version'];
+
+ Requirements::css('//use.fontawesome.com/releases/v'.$v.'/css/all.css');
+ }
+ }
+}
diff --git a/src/Extensions/LostPasswordHandlerExtension.php b/src/Extensions/LostPasswordHandlerExtension.php
new file mode 100755
index 0000000..35407cc
--- /dev/null
+++ b/src/Extensions/LostPasswordHandlerExtension.php
@@ -0,0 +1,69 @@
+ 'passwordsent',
+ ];
+
+ private static $allowed_actions = [
+ 'passwordsent',
+ ];
+
+ /**
+ * Show the "password sent" page, after a user has requested
+ * to reset their password.
+ *
+ * @return array
+ */
+ public function passwordsent()
+ {
+ $message = _t(
+ 'SilverStripe\\Security\\Security.PASSWORDRESETSENTTEXT',
+ "Thank you. A reset link has been sent, provided an account exists for this email address."
+ );
+
+ $email = $this->getRequest()->getVar('email');
+ $message = $email
+ ? 'Thank you! A reset link has been sent to \''.$email.'\', provided an account exists for this email address.'
+ : $message;
+
+ return [
+ 'Title' => _t(
+ 'SilverStripe\\Security\\Security.PASSWORDRESETSENTHEADER',
+ "Password reset link sent".($email ? ' to \''.$email.'\'' : '')
+ ),
+ 'ElementalArea' => DBField::create_field('HTMLFragment', "$message
"),
+ ];
+ }
+
+ /**
+ * Avoid information disclosure by displaying the same status, regardless wether the email address actually exists
+ *
+ * @param array $data
+ * @return HTTPResponse
+ */
+ protected function redirectToSuccess(array $data)
+ {
+ $link = $this->link('passwordsent').'?email='.$data['Email'];
+
+ return $this->redirect($this->addBackURLParam($link));
+ }
+}
diff --git a/src/Extensions/NotificationsExtension.php b/src/Extensions/NotificationsExtension.php
new file mode 100755
index 0000000..dd95c3f
--- /dev/null
+++ b/src/Extensions/NotificationsExtension.php
@@ -0,0 +1,98 @@
+ 'Boolean(1)',
+ ];
+
+ private static $has_many = [
+ 'Notifications' => Notification::class,
+ ];
+
+ public function updateCMSFields(FieldList $fields)
+ {
+ $tab = $fields->findOrMakeTab('Root.Notifications');
+
+ if(!$this->owner->exists()) {
+ $tab->push(LiteralField::create(
+ 'NotificationsNotice',
+ 'The object must be saved before notifications can be added
'
+ ));
+
+ return null;
+ }
+
+ $items = $this->owner->Notifications();
+
+ $config = GridFieldConfig::create();
+ $config->addComponents([
+ new GridFieldToolbarHeader(),
+ new GridFieldTitleHeader(),
+ new GridFieldEditableColumns(),
+ new GridFieldAddNewInlineButton('toolbar-header-right'),
+ new GridFieldDetailForm(),
+ new GridFieldEditButton(),
+ new GridFieldDeleteAction(),
+ ]);
+
+ $tab->setChildren(FieldList::create(
+ HeaderField::create('NotificationsHeader','Notifications'),
+ LiteralField::create(
+ 'CurrentNotifications',
+ 'Current:'
+ .$this->owner->renderWith('App\\Objects\\NotificationsList')
+ ),
+ CheckboxField::create('ShowNotifications'),
+ GridField::create(
+ 'Notifications',
+ '',
+ $items,
+ $config
+ )
+ ));
+ }
+
+ public function NotificationsToday()
+ {
+ $items = $this->owner->Notifications();
+ $time = time();
+
+ return $items->filter([
+ 'DateOn:LessThanOrEqual' => date('Y-m-d', $time),
+ 'DateOff:GreaterThanOrEqual' => date('Y-m-d', $time),
+ ]);
+ }
+}
diff --git a/src/Extensions/OpenningHoursExtension.php b/src/Extensions/OpenningHoursExtension.php
new file mode 100755
index 0000000..5a26fc8
--- /dev/null
+++ b/src/Extensions/OpenningHoursExtension.php
@@ -0,0 +1,189 @@
+ 'Boolean(1)',
+ 'OpenningHoursNote' => 'Varchar(255)',
+ ];
+
+ private static $has_one = [
+ 'OpeningHoursPage' => SiteTree::class,
+ ];
+
+ private static $has_many = [
+ 'OpeningHours' => OpeningHour::class,
+ 'Holidays' => Holiday::class,
+ ];
+
+ public function HoursLink()
+ {
+ return $this->owner->OpeningHoursPage()->Link();
+ }
+
+ public function updateCMSFields(FieldList $fields)
+ {
+ $tab = $fields->findOrMakeTab('Root.OpeningHours');
+
+ if(!$this->owner->exists()) {
+ $tab->push(LiteralField::create(
+ 'OpeningHoursNotice',
+ 'The object must be saved before opening hours can be added
'
+ ));
+
+ return null;
+ }
+
+ $hours = $this->owner->OpeningHours();
+
+ $config = GridFieldConfig::create();
+ $config->addComponents([
+ new GridFieldToolbarHeader(),
+ new GridFieldTitleHeader(),
+ new GridFieldEditableColumns(),
+ new GridFieldAddNewInlineButton('toolbar-header-right'),
+ new GridFieldDeleteAction(),
+ ]);
+
+ $tab->setChildren(FieldList::create(
+ HeaderField::create('OpeningHours','Opening Hours'),
+ LiteralField::create(
+ 'CurrentOpeningHour',
+ 'Today:'
+ .''
+ .$this->owner->renderWith('App\\Objects\\OpeningHoursList')
+ .'
'
+ ),
+ CheckboxField::create('ShowOpeningHours'),
+ DropdownField::create(
+ 'OpeningHoursPageID',
+ 'Opening Hours Page',
+ SiteTree::get()->map()->toArray()
+ ),
+ /*TextareaField::create('OpenningHoursNote'),
+ LiteralField::create(
+ 'OpeningHoursNote',
+ 'Please, specify time ranges. For example:
'
+ .'Monday 10:00 AM - 2:00 PM
'
+ .'Monday 3:00 PM - 6:00 PM
'
+ .'Tuesday 12:00 AM - 2:00 PM
'
+ .'Tuesday 3:00 PM - 4:00 PM
'
+ .'...
'
+ .'Short day example durring holidays:
'
+ .'Monday 12:00 AM - 2:00 PM 12/31/2018 - 01/06/2019'
+ .'
'
+ ),*/
+ GridField::create(
+ 'OpeningHours',
+ 'Opening Hours',
+ $hours,
+ $config
+ )
+ ));
+
+ $tab = $fields->findOrMakeTab('Root.Holidays');
+ $tab->push(GridField::create(
+ 'Holidays',
+ 'Holidays',
+ $this->owner->Holidays(),
+ $config
+ ));
+ }
+
+ /**
+ * Get the opening hours
+ *
+ * @return OpeningHour|DataObject|null
+ */
+ public function OpeningHoursToday()
+ {
+ $hours = $this->owner->OpeningHours();
+ $time = time();
+
+ $today = $hours->filter([
+ 'Day' => date('l', $time),
+ 'DisplayStart:LessThanOrEqual' => date('Y-m-d', $time),
+ 'DisplayEnd:GreaterThanOrEqual' => date('Y-m-d', $time),
+ ]);
+
+ return $today->exists() ? $today : $hours->filter([
+ 'Day' => date('l', $time),
+ 'DisplayStart' => null,
+ 'DisplayEnd' => null,
+ ]);
+ }
+
+ public function OpeningHoursJSON()
+ {
+ $hours = $this->owner->OpeningHours();
+ $result = [];
+ foreach ($hours as $hour) {
+ $from = str_replace(':00', '', date('g:i a', strtotime($hour->getField('From'))));
+ $till = str_replace(':00', '', date('g:i a', strtotime($hour->getField('Till'))));
+
+ $result['days'][$hour->getField('Day')][] = [
+ 'From' => $from,
+ 'Till' => $till,
+ 'DisplayStart' => $hour->getField('DisplayStart'),
+ 'DisplayEnd' => $hour->getField('DisplayEnd'),
+ ];
+ }
+
+ $holidays = $this->owner->Holidays();
+ foreach ($holidays as $holiday) {
+ $result['holidays'][$holiday->getField('Date')] = $holiday->getField('Title');
+ }
+
+ return json_encode($result);
+ }
+
+ public function onBeforeWrite()
+ {
+ parent::onBeforeWrite();
+ if ($this->owner->exists() && !$this->owner->OpeningHours()->exists()) {
+ $this->createOpeningHours();
+ }
+ }
+
+ /**
+ * Set up the opening hours for each day of the week
+ */
+ private function createOpeningHours()
+ {
+ $days = OpeningHour::singleton()->dbObject('Day')->enumValues();
+ foreach ($days as $day) {
+ $openingHour = OpeningHour::create();
+ $openingHour->Day = $day;
+ $this->owner->OpeningHours()->add($openingHour);
+ }
+ }
+}
diff --git a/src/Extensions/PageFluentExtension.php b/src/Extensions/PageFluentExtension.php
new file mode 100755
index 0000000..b418957
--- /dev/null
+++ b/src/Extensions/PageFluentExtension.php
@@ -0,0 +1,28 @@
+owner->isPublishedInLocale()) {
+ $query = '"' . $table . '_Localised_' . $locale->getLocale() . '"."' . $field . '"';
+ }
+ }
+}
diff --git a/src/Extensions/PlaceholderFormExtension.php b/src/Extensions/PlaceholderFormExtension.php
new file mode 100755
index 0000000..ded63dd
--- /dev/null
+++ b/src/Extensions/PlaceholderFormExtension.php
@@ -0,0 +1,37 @@
+setPlaceholder($field);
+ }
+ }
+
+ private function setPlaceholder($field)
+ {
+ if (is_a($field, TextField::class)) {
+ $field->setAttribute(
+ 'placeholder',
+ $field->Title()
+ .($field->hasClass('requiredField') ? '*' : '')
+ );
+ $field->setTitle('');
+ }
+
+ if (is_a($field, CompositeField::class)) {
+ $children = $field->getChildren();
+ foreach ($children as $child) {
+ $this->setPlaceholder($child);
+ }
+ }
+ }
+}
diff --git a/src/Extensions/ShoppingCartControllerExtension.php b/src/Extensions/ShoppingCartControllerExtension.php
new file mode 100755
index 0000000..9cd2079
--- /dev/null
+++ b/src/Extensions/ShoppingCartControllerExtension.php
@@ -0,0 +1,24 @@
+ 'Text',
+ 'Longitude' => 'Decimal(10, 8)',
+ 'Latitude' => 'Decimal(11, 8)',
+ 'MapZoom' => 'Int',
+ //'MapAPIKey' => 'Varchar(255)',
+ 'Description' => 'Varchar(255)',
+ 'Address' => 'Varchar(255)',
+ 'Suburb' => 'Varchar(255)',
+ 'State' => 'Varchar(255)',
+ 'ZipCode' => 'Varchar(6)',
+ ];
+
+ private static $has_one = [
+ 'PrivacyPolicy' => SiteTree::class,
+ 'Sitemap' => SiteTree::class,
+ ];
+
+ private static $many_many = [
+ 'Navigation' => SiteTree::class,
+ ];
+
+ public function updateCMSFields(FieldList $fields)
+ {
+ $img = Image::get()->filter([
+ 'ParentID' => 0,
+ 'FileFilename' => 'qrcode.png',
+ ])->first();
+ if ($img) {
+ $fields->addFieldsToTab('Root.Main', [
+ LiteralField::create('QRCode', ''),
+ ]);
+ }
+
+ $fields->addFieldsToTab('Root.Main', [
+ TreeMultiselectField::create(
+ 'Navigation',
+ 'Navigation',
+ SiteTree::class
+ )->setDisableFunction(static function ($el) {
+ return $el->getField('ParentID') !== 0;
+ }),
+ TextareaField::create('Description', 'Website Description'),
+ TextareaField::create('ExtraCode', 'Extra site-wide HTML code'),
+ DropdownField::create(
+ 'PrivacyPolicyID',
+ 'Privacy Policy Page',
+ SiteTree::get()->map()->toArray()
+ )->setEmptyString('(Select one)'),
+ DropdownField::create(
+ 'SitemapID',
+ 'Sitemap Page',
+ SitemapPage::get()->map()->toArray()
+ )->setEmptyString('(Select one)'),
+ ]);
+
+ $mapTab = $fields->findOrMakeTab('Root.Maps');
+ $mapTab->setTitle('Address / Map');
+
+ $fields->addFieldsToTab('Root.Maps', [
+ TextField::create('Address'),
+ TextField::create('Suburb', 'City'),
+ TextField::create('State'),
+ TextField::create('ZipCode'),
+ ]);
+
+ if (MapboxField::getAccessToken()) {
+ $fields->addFieldsToTab('Root.Maps', [
+ //TextField::create('MapAPIKey'),
+ TextField::create('MapZoom'),
+ MapboxField::create('Map', 'Choose a location', 'Latitude', 'Longitude'),
+ ]);
+ } else {
+ $fields->addFieldsToTab('Root.Maps', [
+ LiteralField::create('MapNotice', 'No Map API keys specified.
')
+ ]);
+ }
+
+ /*GoogleMapField::create(
+ $this->owner,
+ 'Location',
+ [
+ 'show_search_box' => true,
+ ]
+ )*/
+ }
+
+ public function MapAPIKey()
+ {
+ return MapboxField::config()->get('access_token');
+ }
+
+ public function MapStyle()
+ {
+ return MapboxField::config()->get('map_style');
+ }
+
+ public function getGeoJSON()
+ {
+ return '{"type": "MarkerCollection","features": [{"type": "Feature","icon": "",'
+ .'"properties": {"content": "'.$this->owner->getTitle().'"},"geometry": {"type": "Point",'
+ .'"coordinates": ['.$this->owner->getField('Longitude').','.$this->owner->getField('Latitude').']}}]}';
+ }
+
+ public function DirectionsLink()
+ {
+ return ''
+ .' Get Directions';
+ }
+
+ public function getLatestBlogPosts()
+ {
+ return BlogPost::get()->sort('PublishDate DESC');
+ }
+}
diff --git a/src/Extensions/SiteMemberLoginForm.php b/src/Extensions/SiteMemberLoginForm.php
new file mode 100644
index 0000000..ac33484
--- /dev/null
+++ b/src/Extensions/SiteMemberLoginForm.php
@@ -0,0 +1,51 @@
+Fields();
+ $actions = $this->Actions();
+
+ $email = $fields->fieldByName('Email');
+ if ($email) {
+ $email
+ ->setAttribute('placeholder', 'your@email.com')
+ ->setAttribute('autocomplete', 'email')
+ ->setAttribute('type', 'email');
+ }
+
+ $pass = $fields->fieldByName('Password');
+ if($pass) {
+ //$pass->setAttribute('autocomplete', 'current-password');
+ $pass->setAttribute('placeholder', '**********');
+ $pass->setAutofocus(true);
+ }
+
+ $btn = $actions->fieldByName('action_doLogin');
+ if($btn) {
+ $btn->setUseButtonTag(true);
+ $btn->setButtonContent(' '.$btn->Title());
+ $btn->addExtraClass('btn-lg');
+ }
+
+ if (Director::isLive()) {
+ $this->enableSpamProtection();
+ }
+ }
+}
diff --git a/src/Extensions/SiteTreeExtension.php b/src/Extensions/SiteTreeExtension.php
new file mode 100755
index 0000000..76cda6b
--- /dev/null
+++ b/src/Extensions/SiteTreeExtension.php
@@ -0,0 +1,24 @@
+ 'Text',
+ ];
+
+ public function updateSettingsFields(FieldList $fields)
+ {
+ $fields->addFieldsToTab('Root.Settings', [
+ TextareaField::create(
+ 'ExtraCode',
+ 'Extra page specific HTML code'
+ ),
+ ]);
+ }
+}
diff --git a/src/Extensions/SlideImageExtension.php b/src/Extensions/SlideImageExtension.php
new file mode 100755
index 0000000..941be09
--- /dev/null
+++ b/src/Extensions/SlideImageExtension.php
@@ -0,0 +1,74 @@
+ 'Boolean(0)',
+ 'DateOn' => 'Datetime',
+ 'DateOff' => 'Datetime',
+ ];
+
+ private $_cache = [];
+
+ public function getElement()
+ {
+ if(!isset($this->_cache['element'])) {
+ $this->_cache['element'] = $this->owner->SlideshowElement();
+ }
+
+ return $this->_cache['element'];
+ }
+
+ public function getSlideWidth()
+ {
+ $element = $this->getElement();
+ return $element->getSlideWidth();
+ }
+
+ public function getSlideHeight()
+ {
+ $element = $this->getElement();
+ return $element->getSlideHeight();
+ }
+
+ public function updateCMSFields(FieldList $fields)
+ {
+ parent::updateCMSFields($fields);
+
+ $fields->removeByName([
+ 'PageLinkID',
+ 'Hide',
+ 'DateOn',
+ 'DateOff',
+ ]);
+
+
+ $fields->dataFieldByName('Image')
+ ->setTitle('Image ('.$this->getSlideWidth().' x '.$this->getSlideHeight().' px)');
+
+ $fields->addFieldToTab('Root.Main', ToggleCompositeField::create(
+ 'ConfigHD',
+ 'Slide Settings',
+ [
+ CheckboxField::create('Hide', 'Hide this slide? (That will hide the slide regardless of start/end fields)'),
+ DatetimeField::create('DateOn', 'When would you like to start showing the slide?'),
+ DatetimeField::create('DateOff', 'When would you like to stop showing the slide?'),
+ ]
+ ));
+ }
+}
diff --git a/src/Extensions/SocialExtension.php b/src/Extensions/SocialExtension.php
new file mode 100755
index 0000000..3cde299
--- /dev/null
+++ b/src/Extensions/SocialExtension.php
@@ -0,0 +1,75 @@
+ 'Varchar(255)',
+ ];
+
+ private static $has_one = [
+ 'Facebook' => Link::class,
+ 'LinkedIn' => Link::class,
+ 'Pinterest' => Link::class,
+ 'Instagram' => Link::class,
+ 'Twitter' => Link::class,
+ 'PublicEmail' => Link::class,
+ 'PhoneNumber' => Link::class,
+ ];
+
+ public function updateCMSFields(FieldList $fields)
+ {
+ parent::updateCMSFields($fields);
+
+ $linkFields = [
+ LinkField::create('FacebookID'),
+ LinkField::create('LinkedInID'),
+ LinkField::create('PinterestID'),
+ LinkField::create('InstagramID'),
+ LinkField::create('TwitterID'),
+ ];
+
+ foreach ($linkFields as $field) {
+ $field->setAllowedTypes(['URL']);
+ }
+
+ $fields->findOrMakeTab('Root.Social');
+
+ $fields->addFieldsToTab('Root.Social', [
+ LinkField::create('PublicEmailID', 'Public Email')
+ ->setAllowedTypes(['Email']),
+ LinkField::create('PhoneNumberID', 'Phone Number')
+ ->setAllowedTypes(['Phone']),
+ ]);
+
+ $fields->addFieldsToTab('Root.Social', $linkFields);
+ }
+
+ public static function byPhone($phone)
+ {
+ $links = Link::get()->filter('Phone', $phone);
+
+ if ($links->exists()) {
+ return Member::get()->filter(
+ 'PhoneNumberID',
+ array_keys($links->map('ID', 'Title')->toArray())
+ )->first();
+ }
+
+ return null;
+ }
+}
diff --git a/src/Extensions/UserDefinedForm_HiddenClass.php b/src/Extensions/UserDefinedForm_HiddenClass.php
new file mode 100644
index 0000000..a95fd3b
--- /dev/null
+++ b/src/Extensions/UserDefinedForm_HiddenClass.php
@@ -0,0 +1,10 @@
+
+ */
+
+use SilverStripe\Forms\GridField\GridField;
+use SilverStripe\Forms\GridField\GridField_HTMLProvider;
+use SilverStripe\Forms\GridField\GridField_ActionProvider;
+use SilverStripe\Forms\GridField\GridField_FormAction;
+use SilverStripe\Forms\GridField\GridField_SaveHandler;
+use SilverStripe\Control\Controller;
+
+class SaveAllButton implements GridField_HTMLProvider, GridField_ActionProvider
+{
+ protected $targetFragment;
+ protected $actionName = 'saveallrecords';
+
+ public $buttonName;
+
+ public $publish = true;
+
+ public $completeMessage;
+
+ public $removeChangeFlagOnFormOnSave = false;
+
+ public function setButtonName($name)
+ {
+ $this->buttonName = $name;
+ return $this;
+ }
+
+ public function setRemoveChangeFlagOnFormOnSave($flag)
+ {
+ $this->removeChangeFlagOnFormOnSave = $flag;
+ return $this;
+ }
+
+ public function __construct($targetFragment = 'before', $publish = true, $action = 'saveallrecords')
+ {
+ $this->targetFragment = $targetFragment;
+ $this->publish = $publish;
+ $this->actionName = $action;
+ }
+
+ public function getHTMLFragments($gridField)
+ {
+ $singleton = singleton($gridField->getModelClass());
+
+ if (!$singleton->canEdit() && !$singleton->canCreate()) {
+ return [];
+ }
+
+ if (!$this->buttonName) {
+ if ($this->publish && $singleton->hasExtension('Versioned')) {
+ $this->buttonName = _t('GridField.SAVE_ALL_AND_PUBLISH', 'Save all and publish');
+ } else {
+ $this->buttonName = _t('GridField.SAVE_ALL', 'Save all');
+ }
+ }
+
+ $button = GridField_FormAction::create(
+ $gridField,
+ $this->actionName,
+ $this->buttonName,
+ $this->actionName,
+ null
+ );
+
+ $button->setAttribute('data-icon', 'disk')->addExtraClass('new new-link ui-button-text-icon-primary');
+
+ if ($this->removeChangeFlagOnFormOnSave) {
+ $button->addExtraClass('js-mwm-gridfield--saveall');
+ }
+
+ return [
+ $this->targetFragment => $button->Field(),
+ ];
+ }
+
+ public function getActions($gridField)
+ {
+ return [$this->actionName];
+ }
+
+ public function handleAction(GridField $gridField, $actionName, $arguments, $data)
+ {
+ if ($actionName == $this->actionName) {
+ return $this->saveAllRecords($gridField, $arguments, $data);
+ }
+ }
+
+ protected function saveAllRecords(GridField $grid, $arguments, $data)
+ {
+ if (isset($data[$grid->Name])) {
+ $currValue = $grid->Value();
+ $grid->setValue($data[$grid->Name]);
+ $model = singleton($grid->List->dataClass());
+
+ foreach ($grid->getConfig()->getComponents() as $component) {
+ if ($component instanceof GridField_SaveHandler) {
+ $component->handleSave($grid, $model);
+ }
+ }
+
+ if ($this->publish) {
+ // Only use the viewable list items, since bulk publishing can take a toll on the system
+ $list = ($paginator = $grid->getConfig()->getComponentByType('GridFieldPaginator')) ? $paginator->getManipulatedData($grid, $grid->List) : $grid->List;
+
+ $list->each(
+ function ($item) {
+ if ($item->hasExtension('Versioned')) {
+ $item->writeToStage('Stage');
+ $item->publish('Stage', 'Live');
+ }
+ }
+ );
+ }
+
+ if ($model->exists()) {
+ $model->delete();
+ $model->destroy();
+ }
+
+ $grid->setValue($currValue);
+
+ if (Controller::curr() && $response = Controller::curr()->Response) {
+ if (!$this->completeMessage) {
+ $this->completeMessage = _t('GridField.DONE', 'Done.');
+ }
+
+ $response->addHeader('X-Status', rawurlencode($this->completeMessage));
+ }
+ }
+ }
+}
diff --git a/src/GraphQL/APIKeyAuthenticator.php b/src/GraphQL/APIKeyAuthenticator.php
new file mode 100644
index 0000000..cef6b97
--- /dev/null
+++ b/src/GraphQL/APIKeyAuthenticator.php
@@ -0,0 +1,46 @@
+getHeader('apikey') !== WebpackTemplateProvider::config()['GRAPHQL_API_KEY']
+ ) {
+ if ($member && Permission::checkMember($member, 'CMS_ACCESS')) {
+ return $member;
+ }
+
+ throw new ValidationException('Restricted resource', 401);
+ }
+
+ return Member::get()->first();
+ }
+
+ public function isApplicable(HTTPRequest $request)
+ {
+ if ($request->param('Controller') === '%$SilverStripe\GraphQL\Controller.admin') {
+ return false;
+ }
+
+ /*if($request->getHeader('apikey')){
+ return true;
+ }*/
+ return true;
+ return false;
+ }
+}
diff --git a/src/GraphQL/APIKeyMiddleware.php b/src/GraphQL/APIKeyMiddleware.php
new file mode 100644
index 0000000..da543b5
--- /dev/null
+++ b/src/GraphQL/APIKeyMiddleware.php
@@ -0,0 +1,23 @@
+getHeader('apikey') === WebpackTemplateProvider::config()['GRAPHQL_API_KEY']) {
+ return $next($schema, $query, $context, $params);
+ }
+
+ throw new \Exception('Invalid API key token');
+ }
+}
diff --git a/src/GraphQL/ElementTypeCreator.php b/src/GraphQL/ElementTypeCreator.php
new file mode 100644
index 0000000..1559408
--- /dev/null
+++ b/src/GraphQL/ElementTypeCreator.php
@@ -0,0 +1,37 @@
+ 'element'
+ ];
+ }
+
+ public function fields()
+ {
+ return [
+ '_id' => ['type' => Type::nonNull(Type::id()),'resolve' => static function($object) {
+ return $object->ID;
+ }],
+ 'ID' => ['type' => Type::nonNull(Type::id())],
+ 'Title' => ['type' => Type::string()],
+ 'ParentID' => ['type' => Type::id()],
+ 'Render' => [
+ 'type' => Type::string(),
+ 'resolve' => static function($object, array $args, $context, ResolveInfo $info) {
+ return $object->getController()->forTemplate()->HTML();
+ }
+ ],
+ ];
+ }
+}
diff --git a/src/GraphQL/MemberTypeCreator.php b/src/GraphQL/MemberTypeCreator.php
new file mode 100644
index 0000000..ed25ab4
--- /dev/null
+++ b/src/GraphQL/MemberTypeCreator.php
@@ -0,0 +1,29 @@
+ 'member'
+ ];
+ }
+
+ public function fields()
+ {
+ return [
+ 'ID' => ['type' => Type::nonNull(Type::id())],
+ 'Email' => ['type' => Type::string()],
+ 'FirstName' => ['type' => Type::string()],
+ 'Surname' => ['type' => Type::string()],
+ ];
+ }
+}
diff --git a/src/GraphQL/PageTypeCreator.php b/src/GraphQL/PageTypeCreator.php
new file mode 100644
index 0000000..ad069ec
--- /dev/null
+++ b/src/GraphQL/PageTypeCreator.php
@@ -0,0 +1,122 @@
+ 'page'
+ ];
+ }
+
+ public function fields()
+ {
+ $elementsConnection = Connection::create('Elements')
+ ->setConnectionType($this->manager->getType('element'))
+ ->setDescription('A list of the page elements')
+ ->setSortableFields(['ID', 'Title']);
+
+ return [
+ '_id' => ['type' => Type::nonNull(Type::id()),'resolve' => static function($object) {
+ return $object->ID;
+ }],
+ 'ID' => ['type' => Type::nonNull(Type::id())],
+ 'Title' => ['type' => Type::string()],
+ 'Content' => ['type' => Type::string()],
+ 'Link' => ['type' => Type::string(), 'resolve' => static function($object) {
+ return $object->Link();
+ }],
+ 'URLSegment' => ['type' => Type::string()],
+ 'ParentID' => ['type' => Type::id()],
+ 'ClassName' => ['type' => Type::string()],
+ 'CSSClass' => ['type' => Type::string(), 'resolve' => static function($object) {
+ return $object->CSSClass();
+ }],
+ 'Summary' => ['type' => Type::string(), 'resolve' => static function($object) {
+ return $object->Summary();
+ }],
+ 'HTML' => ['type' => Type::string(), 'resolve' => static function($object) {
+ // get action from request
+ $action = null;
+
+ /** @var \Page $object */
+ Director::set_current_page($object);
+ /** @var \PageController $controller */
+ $controller = ModelAsController::controller_for($object);
+
+ // find templates
+ $tpl = 'Page';
+ $tpls = SSViewer::get_templates_by_class(
+ $object->ClassName,
+ ($action ? '_'.$action : ''),
+ \Page::class
+ );
+
+ foreach ($tpls as $tpl){
+ if(is_array($tpl)){
+ continue;
+ }
+
+ $a_tpl = explode('\\',$tpl);
+ $last_name = array_pop($a_tpl);
+ $a_tpl[] = 'Layout';
+ $a_tpl[] = $last_name;
+ $a_tpl = implode('\\', $a_tpl);
+
+ if(SSViewer::hasTemplate($a_tpl)){
+ break;
+ }
+ }
+ //
+
+ $tpl = ($tpl !== 'Page') ? $tpl : 'Layout/Page';
+
+ $action = $action ? $action : 'index';
+ /** @var HTTPRequest $request */
+ $request = new HTTPRequest('GET', $object->AbsoluteLink());
+ $request->setSession(new Session([]));
+
+ // a little dirty way to make forms working
+ Controller::curr()->config()->set('url_segment', $object->AbsoluteLink());
+ /*$controller->setRequest($request);*/
+ //$request->getSession()->init($request);
+
+ $controller->setRequest($request);
+ $controller->setAction($action);
+ //$controller->pushCurrent();
+ $controller->doInit();
+
+ $layout = $controller->renderWith($tpl);
+ return $controller
+ ->customise(['Layout' => $layout])
+ ->renderWith('GraphQLPage')->HTML();
+ }],
+ 'Elements' => [
+ 'type' => $elementsConnection->toType(),
+ 'args' => $elementsConnection->args(),
+ 'resolve' => static function($object, array $args, $context, ResolveInfo $info) use ($elementsConnection) {
+ return $elementsConnection->resolveList(
+ $object->ElementalArea()->Elements(),
+ $args,
+ $context
+ );
+ }
+ ]
+ ];
+ }
+}
diff --git a/src/GraphQL/PaginatedReadMembersQueryCreator.php b/src/GraphQL/PaginatedReadMembersQueryCreator.php
new file mode 100644
index 0000000..a46200d
--- /dev/null
+++ b/src/GraphQL/PaginatedReadMembersQueryCreator.php
@@ -0,0 +1,44 @@
+setConnectionType($this->manager->getType('member'))
+ ->setArgs([
+ 'Email' => [
+ 'type' => Type::string()
+ ]
+ ])
+ ->setSortableFields(['ID', 'FirstName', 'Email'])
+ ->setConnectionResolver(static function ($object, array $args, $context, ResolveInfo $info) {
+ $member = Member::singleton();
+ if (!$member->canView($context['currentUser'])) {
+ throw new \InvalidArgumentException(sprintf(
+ '%s view access not permitted',
+ Member::class
+ ));
+ }
+ $list = Member::get();
+
+ // Optional filtering by properties
+ if (isset($args['Email'])) {
+ $list = $list->filter('Email', $args['Email']);
+ }
+
+ return $list;
+ });
+ }
+}
diff --git a/src/GraphQL/PaginatedReadPagesQueryCreator.php b/src/GraphQL/PaginatedReadPagesQueryCreator.php
new file mode 100644
index 0000000..2b6e09d
--- /dev/null
+++ b/src/GraphQL/PaginatedReadPagesQueryCreator.php
@@ -0,0 +1,59 @@
+setConnectionType($this->manager->getType('page'))
+ ->setArgs([
+ 'Link' => [
+ 'type' => Type::string()
+ ]
+ ])
+ ->setSortableFields(['Sort'])
+ ->setConnectionResolver(static function ($object, array $args, $context, ResolveInfo $info) {
+
+ if (isset($args['Link'])) {
+ $link = $args['Link'];
+
+ if(SiteTree::has_extension('\TractorCow\Fluent\Extension\FluentSiteTreeExtension')) {
+ $arr = array_filter(explode('/', $args['Link']));
+
+ $locale = \TractorCow\Fluent\Model\Locale::get()->filter('URLSegment', array_shift($arr))->first();
+ \TractorCow\Fluent\State\FluentState::singleton()->setLocale($locale->Locale);
+
+ $link = implode('/', $arr);
+ }
+
+
+ $list = ArrayList::create();
+ $page = SiteTree::get_by_link($link);
+ $list->add($page);
+ }
+
+ /*$list = \Page::get();
+
+ // Optional filtering by properties
+ if (isset($args['ID'])) {
+ $list = $list->filter('ID', $args['ID']);
+ }*/
+
+ return $list;
+ });
+ }
+}
diff --git a/src/GraphQL/ReadMembersQueryCreator.php b/src/GraphQL/ReadMembersQueryCreator.php
new file mode 100644
index 0000000..3f4683f
--- /dev/null
+++ b/src/GraphQL/ReadMembersQueryCreator.php
@@ -0,0 +1,53 @@
+ 'readMembers'
+ ];
+ }
+
+ public function args()
+ {
+ return [
+ 'Email' => ['type' => Type::string()]
+ ];
+ }
+
+ public function type()
+ {
+ return Type::listOf($this->manager->getType('member'));
+ }
+
+ public function resolve($object, array $args, $context, ResolveInfo $info)
+ {
+ $member = Member::singleton();
+
+ if (!$member->canView($context['currentUser'])) {
+ throw new \InvalidArgumentException(sprintf(
+ '%s view access not permitted',
+ Member::class
+ ));
+ }
+ $list = Member::get();
+
+ // Optional filtering by properties
+ if (isset($args['Email'])) {
+ $list = $list->filter('Email', $args['Email']);
+ }
+
+ return $list;
+ }
+}
diff --git a/src/Models/Holiday.php b/src/Models/Holiday.php
new file mode 100755
index 0000000..aad9163
--- /dev/null
+++ b/src/Models/Holiday.php
@@ -0,0 +1,57 @@
+ 'Varchar(255)',
+ 'Date' => 'Date',
+ ];
+
+ private static $has_one = [
+ 'Parent' => SiteConfig::class,
+ ];
+
+
+ private static $summary_fields = [
+ 'Title' => 'Title',
+ 'Date' => 'Date',
+ ];
+
+ private static $default_sort = 'Date ASC, Title ASC';
+
+ public function validate()
+ {
+ $result = parent::validate();
+
+ $exists = self::get()->filter([
+ 'ID:not' => $this->ID,
+ 'Date' => $this->getField('Date'),
+ ])->exists();
+
+ if($exists) {
+ return $result->addError(
+ 'Holiday was defined already.',
+ ValidationResult::TYPE_ERROR
+ );
+ }
+
+ return $result;
+ }
+}
diff --git a/src/Models/Notification.php b/src/Models/Notification.php
new file mode 100755
index 0000000..1b02bb8
--- /dev/null
+++ b/src/Models/Notification.php
@@ -0,0 +1,80 @@
+ 'Varchar(255)',
+ 'Content' => 'Text',
+ 'DateOn' => 'Date',
+ 'DateOff' => 'Date',
+ 'Area' => 'Enum("Site","Site")',
+ ];
+
+ private static $has_one = [
+ 'Parent' => SiteConfig::class,
+ 'TargetLink' => Link::class,
+ ];
+
+ private static $defaults = [
+ 'Area' => 'Site',
+ ];
+
+
+ private static $summary_fields = [
+ 'Title' => 'Title',
+ 'Content' => 'Text',
+ 'DateOn' => 'Turn on date',
+ 'DateOff' => 'Turn off date',
+ ];
+
+ private static $default_sort = 'DateOn DESC, DateOff DESC, Title ASC';
+
+ public function getCMSFields()
+ {
+ $fields = parent::getCMSFields();
+
+ $fields->addFieldsToTab('Root.Main', [
+ LinkField::create('TargetLinkID', 'Link'),
+ ]);
+
+ return $fields;
+ }
+
+ public function validate()
+ {
+ $result = parent::validate();
+
+ if (!$this->getField('DateOn') || !$this->getField('DateOff')) {
+ return $result->addError(
+ 'Turn on and turn off dates are required.',
+ ValidationResult::TYPE_ERROR
+ );
+ }
+
+ if (!$this->getField('Content')) {
+ return $result->addError(
+ 'Text field required.',
+ ValidationResult::TYPE_ERROR
+ );
+ }
+
+ return $result;
+ }
+}
diff --git a/src/Models/OpeningHour.php b/src/Models/OpeningHour.php
new file mode 100755
index 0000000..a1fa2af
--- /dev/null
+++ b/src/Models/OpeningHour.php
@@ -0,0 +1,90 @@
+ 'Enum("Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday", "Monday")',
+ 'From' => 'Time',
+ 'Till' => 'Time',
+ 'Comment' => 'Varchar(255)',
+ 'DisplayStart' => 'Date',
+ 'DisplayEnd' => 'Date',
+ ];
+
+ private static $has_one = [
+ 'Parent' => SiteConfig::class,
+ ];
+
+ private static $defaults = [
+ 'Day' => 'Monday',
+ 'From' => '09:00:00',
+ 'Till' => '22:00:00',
+ ];
+
+ private static $summary_fields = [
+ 'Day' => 'Day *',
+ 'From' => 'From *',
+ 'Till' => 'Till *',
+ //'Comment' => 'Comment',
+ 'DisplayStart' => 'Display Start',
+ 'DisplayEnd' => 'Display End',
+ ];
+
+ private static $default_sort = 'Day ASC, From ASC';
+
+ public function validate()
+ {
+ $result = parent::validate();
+
+
+ if (!$this->getField('Day')
+ || !$this->getField('From')
+ || !$this->getField('Till')
+ || $this->getField('From') > $this->getField('Till')
+ ) {
+ return $result->addError(
+ 'Day, From and Till fields are required or wrong time range was specified.',
+ ValidationResult::TYPE_ERROR
+ );
+ }
+
+ $exists = self::get()->filter([
+ 'ID:not' => $this->ID,
+ 'ParentID' => $this->getField('ParentID'),
+ 'Day' => $this->getField('Day'),
+ 'From:GreaterThanOrEqual' => $this->getField('From'),
+ 'Till:LessThanOrEqual' => $this->getField('Till'),
+ ])->exists();
+
+ if ($exists) {
+ return $result->addError(
+ 'Hours were defined already for specified range.',
+ ValidationResult::TYPE_ERROR
+ );
+ }
+
+ return $result;
+ }
+
+ public function forTemplate()
+ {
+ return $this->renderWith(__CLASS__);
+ }
+}
diff --git a/src/Shop/CheckoutMapComponent.php b/src/Shop/CheckoutMapComponent.php
new file mode 100755
index 0000000..fb3557e
--- /dev/null
+++ b/src/Shop/CheckoutMapComponent.php
@@ -0,0 +1,47 @@
+process($config)
+ )
+ );
+ }
+
+ public function validateData(Order $order, array $data)
+ {
+ }
+
+ public function setData(Order $order, array $data)
+ {
+ }
+
+ public function getData(Order $order)
+ {
+ return [];
+ }
+}
diff --git a/src/Shop/CheckoutNoDeliveryConfig.php b/src/Shop/CheckoutNoDeliveryConfig.php
new file mode 100755
index 0000000..7c1b0a7
--- /dev/null
+++ b/src/Shop/CheckoutNoDeliveryConfig.php
@@ -0,0 +1,43 @@
+addComponent(CustomerDetails::create());
+
+ if (Checkout::member_creation_enabled() && !Security::getCurrentUser()) {
+ $this->addComponent(Membership::create());
+ }
+
+ if (count(GatewayInfo::getSupportedGateways()) > 1) {
+ $this->addComponent(Payment::create());
+ }
+
+ $this->addComponent(Notes::create());
+
+ $this->addComponent(CheckoutMapComponent::create());
+ $this->addComponent(Terms::create());
+ }
+}
diff --git a/src/Shop/_manifest_exclude b/src/Shop/_manifest_exclude
new file mode 100755
index 0000000..c309c9d
--- /dev/null
+++ b/src/Shop/_manifest_exclude
@@ -0,0 +1 @@
+Remove it in case u need silvershop module
diff --git a/src/Tasks/BrokenFilesTask.php b/src/Tasks/BrokenFilesTask.php
new file mode 100755
index 0000000..fd37367
--- /dev/null
+++ b/src/Tasks/BrokenFilesTask.php
@@ -0,0 +1,39 @@
+exists()) {
+ echo 'File name was not found at SS DB: '
+ .$file->getField('Name').'
'
+ .PHP_EOL;
+
+ $i++;
+
+ continue;
+ }
+ }
+
+ echo ($i > 0) ?
+ 'Missing '.$i.' files
'
+ : 'All files are ok!
';
+
+ die('Done!');
+ }
+}
diff --git a/src/Tasks/BuildTask.php b/src/Tasks/BuildTask.php
new file mode 100755
index 0000000..cfe8924
--- /dev/null
+++ b/src/Tasks/BuildTask.php
@@ -0,0 +1,78 @@
+render();
+ }
+
+ public function Title()
+ {
+ return $this->title;
+ }
+
+ protected function setMessage($msg, $type = 'msg')
+ {
+ if(is_array($msg)) {
+ $type = 'list';
+ }
+
+ $this->messages[] = [$type, $msg];
+ }
+
+ public function render()
+ {
+ echo '';
+
+ foreach ($this->messages as $item) {
+ $type = $item[0];
+ $msg = $item[1];
+
+ switch ($type) {
+ case 'h2':
+ echo ''.$msg.'
'.PHP_EOL;
+ break;
+ case 'h3':
+ echo ''.$msg.'
'.PHP_EOL;
+ break;
+ case 'list':
+ echo '';
+ foreach ($msg as $m) {
+ echo '- '.$m.'
';
+ }
+ echo '
';
+ break;
+ default:
+ echo $msg.'
'.PHP_EOL;
+ break;
+ }
+ }
+
+ echo 'Success!
';
+ }
+}
diff --git a/src/Tasks/CleanContentTask.php b/src/Tasks/CleanContentTask.php
new file mode 100755
index 0000000..36d09f0
--- /dev/null
+++ b/src/Tasks/CleanContentTask.php
@@ -0,0 +1,29 @@
+setField('Content', '');
+ $p->write();
+ echo '#'.$p->ID.' '.$p->getField('Title').'
';
+ }
+
+ die('Done!');
+ }
+}
diff --git a/src/Tasks/QRCodeTask.php b/src/Tasks/QRCodeTask.php
new file mode 100755
index 0000000..69616ab
--- /dev/null
+++ b/src/Tasks/QRCodeTask.php
@@ -0,0 +1,105 @@
+'.$this->Title().'';
+
+ echo self::generateQR();
+
+ die('Done!');
+ }
+
+ public static function generateQR()
+ {
+ $qrCode = new QrCode(Director::absoluteBaseURL());
+ $qrCode->setSize(600);
+ $qrCode->setMargin(10);
+
+ $qrCode->setWriterByName('png');
+ $qrCode->setEncoding('UTF-8');
+ $qrCode->setErrorCorrectionLevel(ErrorCorrectionLevel::HIGH());
+ $qrCode->setForegroundColor(['r' => 0, 'g' => 0, 'b' => 0, 'a' => 0]);
+ $qrCode->setBackgroundColor(['r' => 255, 'g' => 255, 'b' => 255, 'a' => 0]);
+ $qrCode->setLabel(Director::absoluteBaseURL(), 16, null, LabelAlignment::CENTER());
+ /*$qrCode->setLogoPath('/'.File::join_paths(
+ PUBLIC_PATH,
+ RESOURCES_DIR,
+ project(),
+ 'client', 'dist', 'icons',
+ 'apple-touch-icon-152x152.png'
+ ));
+ $qrCode->setLogoSize(152, 152);*/
+ $qrCode->setValidateResult(true);
+
+
+ // Round block sizes to improve readability and make the blocks sharper in pixel based outputs (like png).
+ // There are three approaches:
+ $qrCode->setRoundBlockSize(true, QrCode::ROUND_BLOCK_SIZE_MODE_MARGIN); // The size of the qr code is shrinked, if necessary, but the size of the final image remains unchanged due to additional margin being added (default)
+ $qrCode->setRoundBlockSize(true, QrCode::ROUND_BLOCK_SIZE_MODE_ENLARGE); // The size of the qr code and the final image is enlarged, if necessary
+ $qrCode->setRoundBlockSize(true, QrCode::ROUND_BLOCK_SIZE_MODE_SHRINK); // The size of the qr code and the final image is shrinked, if necessary
+
+ // Set additional writer options (SvgWriter example)
+ $qrCode->setWriterOptions(['exclude_xml_declaration' => true]);
+
+ // Directly output the QR code
+ /*header('Content-Type: '.$qrCode->getContentType());
+ echo $qrCode->writeString();
+ die();*/
+
+ // Save it to a file
+ $qrCode->writeFile(TEMP_PATH.'/qrcode.png');
+ $res = self::getAssetStore()->setFromLocalFile(
+ TEMP_PATH.'/qrcode.png',
+ 'qrcode.png', null, null,
+ [
+ 'conflict' => AssetStore::CONFLICT_OVERWRITE,
+ 'visibility' => AssetStore::VISIBILITY_PUBLIC,
+ ]
+ );
+
+ $img = Image::get()->filter([
+ 'ParentID' => 0,
+ 'FileFilename' => $res['Filename'],
+ ])->first();
+ if(!$img) {
+ $img = Image::create();
+ }
+
+ $res['FileHash'] = $res['Hash'];
+ $res['FileFilename'] = $res['Filename'];
+ $res['ParentID'] = 0;
+
+ $img = $img->update($res);
+ $img->write();
+ $img->publishFile();
+
+ return '
';
+ }
+
+ protected static function getAssetStore()
+ {
+ return Injector::inst()->get(AssetStore::class);
+ }
+}
diff --git a/src/Tasks/RestoreFilesTask.php b/src/Tasks/RestoreFilesTask.php
new file mode 100755
index 0000000..32c0448
--- /dev/null
+++ b/src/Tasks/RestoreFilesTask.php
@@ -0,0 +1,47 @@
+*';
+
+ $files = array_diff(scandir($path), ['.','..']);
+ foreach ($files as $fileName) {
+ $file = File::get()->filter('Name', $fileName);
+ if (!$file->count()) {
+ echo 'File name was not found at SS DB: '.$fileName.'
'.PHP_EOL;
+ continue;
+ }
+
+ foreach ($file as $f) {
+ if ($f->exists()) {
+ echo 'File #'.$f->ID.' already exists at SS file structure. '.$fileName.'
' . PHP_EOL;
+ continue;
+ }
+
+ echo 'Found non existing at SS file system file and found it at SS DB.'
+ .' Creating the file #'.$f->ID.' at SS file system. "' . $fileName . '"
' . PHP_EOL;
+
+ $f->setFromLocalFile($path.'/'.$fileName);
+ $f->write();
+ $f->publishFile();
+ }
+ }
+
+ die('Success!');
+ }
+}
diff --git a/src/Templates/DeferredRequirements.php b/src/Templates/DeferredRequirements.php
new file mode 100755
index 0000000..a9afb88
--- /dev/null
+++ b/src/Templates/DeferredRequirements.php
@@ -0,0 +1,229 @@
+ 'Auto',
+ 'DeferedCSS' => 'loadCSS',
+ 'DeferedJS' => 'loadJS',
+ 'WebpackActive' => 'webpackActive',
+ 'EmptyImgSrc' => 'emptyImageSrc',
+ ];
+ }
+
+ public static function Auto($class = false): string
+ {
+ $config = Config::inst()->get(self::class);
+ $projectName = WebpackTemplateProvider::projectName();
+ $mainTheme = WebpackTemplateProvider::mainTheme();
+ $mainTheme = $mainTheme ?: $projectName;
+
+ $dir = Path::join(
+ Director::publicFolder(),
+ RESOURCES_DIR,
+ $projectName,
+ 'client',
+ 'dist'
+ );
+ $cssPath = Path::join($dir, 'css');
+ $jsPath = Path::join($dir, 'js');
+
+ // Initialization
+ Requirements::block(THIRDPARTY_DIR.'/jquery/jquery.js');
+ /*if (defined('FONT_AWESOME_DIR')) {
+ Requirements::block(FONT_AWESOME_DIR.'/css/lib/font-awesome.min.css');
+ }*/
+ Requirements::set_force_js_to_bottom(true);
+
+ // Main libs
+ if (!$config['nojquery']) {
+ self::loadJS(
+ '//ajax.googleapis.com/ajax/libs/jquery/'
+ .$config['jquery_version'].'/jquery.min.js'
+ );
+ }
+
+ if (!$config['noreact']) {
+ if (!Director::isDev()) {
+ self::loadJS('https://unpkg.com/react@17/umd/react.production.min.js');
+ self::loadJS('https://unpkg.com/react-dom@17/umd/react-dom.production.min.js');
+ } else {
+ self::loadJS('https://unpkg.com/react@17/umd/react.development.js');
+ self::loadJS('https://unpkg.com/react-dom@17/umd/react-dom.development.js');
+ }
+ }
+
+ // App libs
+ if (!$config['nofontawesome']) {
+ $v = !isset($config['fontawesome_version']) || !$config['fontawesome_version']
+ ? Config::inst()->get(FontAwesomeField::class, 'version')
+ : $config['fontawesome_version'];
+
+ self::loadCSS('//use.fontawesome.com/releases/v'.$v.'/css/all.css');
+ }
+
+ self::loadCSS($mainTheme.'.css');
+
+ // hot reloading
+ /*if (self::webpackActive()) {
+ self::loadJS('hot.js');
+ }*/
+
+ self::loadJS($mainTheme.'.js');
+
+ // Custom controller requirements
+ $curr_class = $class ?: get_class(Controller::curr());
+ if (isset($config['custom_requirements'][$curr_class])) {
+ foreach ($config['custom_requirements'][$curr_class] as $file) {
+ if (strpos($file, '.css')) {
+ self::loadCSS($file);
+ }
+ if (strpos($file, '.js')) {
+ self::loadJS($file);
+ }
+ }
+ }
+
+ $curr_class = str_replace('\\', '.', $curr_class);
+
+ // Controller requirements
+ $themePath = Path::join($cssPath, $mainTheme.'_'.$curr_class . '.css');
+ $projectPath = Path::join($cssPath, $projectName.'_'.$curr_class . '.css');
+ if ($mainTheme && file_exists($themePath)) {
+ self::loadCSS($mainTheme.'_'.$curr_class . '.css');
+ } elseif (file_exists($projectPath)) {
+ self::loadCSS($projectName.'_'.$curr_class . '.css');
+ }
+
+ $themePath = Path::join($jsPath, $mainTheme.'_'.$curr_class . '.js');
+ $projectPath = Path::join($jsPath, $projectName.'_'.$curr_class . '.js');
+ if ($mainTheme && file_exists($themePath)) {
+ self::loadJS($mainTheme.'_'.$curr_class . '.js');
+ } elseif (file_exists($projectPath)) {
+ self::loadJS($projectName.'_'.$curr_class . '.js');
+ }
+
+ return self::forTemplate();
+ }
+
+ public static function loadCSS($css): void
+ {
+ $external = (mb_strpos($css, '//') === 0 || mb_strpos($css, 'http') === 0);
+ if ($external) {
+ self::$css[] = $css;
+ } else {
+ WebpackTemplateProvider::loadCSS($css);
+ }
+ }
+
+ public static function loadJS($js): void
+ {
+ /*$external = (mb_substr($js, 0, 2) === '//' || mb_substr($js, 0, 4) === 'http');
+ if ($external || (self::getDeferred() && !self::_webpackActive())) {*/
+ // webpack supposed to load external JS
+ if (self::getDeferred() && !self::webpackActive()) {
+ self::$js[] = $js;
+ } else {
+ WebpackTemplateProvider::loadJS($js);
+ }
+ }
+
+ public static function webpackActive(): bool
+ {
+ return WebpackTemplateProvider::isActive();
+ }
+
+ public static function setDeferred($bool): void
+ {
+ Config::inst()->set(__CLASS__, 'deferred', $bool);
+ }
+
+ public static function getDeferred(): bool
+ {
+ return self::config()['deferred'];
+ }
+
+ public static function forTemplate(): string
+ {
+ $result = '';
+ self::$css = array_unique(self::$css);
+ foreach (self::$css as $css) {
+ $result .= '';
+ }
+
+ self::$js = array_unique(self::$js);
+ foreach (self::$js as $js) {
+ $result .= '';
+ }
+
+ $result .=
+ '';
+
+ return $result;
+ }
+
+ private static function get_url($url): string
+ {
+ $config = self::config();
+
+ // external URL
+ if (strpos($url, '//') !== false) {
+ return $url;
+ }
+
+ $path = WebpackTemplateProvider::toPublicPath($url);
+
+ $absolutePath = Director::getAbsFile($path);
+ $hash = sha1_file($absolutePath);
+
+ $version = $config['version'] ? '&v='.$config['version'] : '';
+ //$static_domain = $config['static_domain'];
+ //$static_domain = $static_domain ?: '';
+
+ return Controller::join_links(WebpackTemplateProvider::toPublicPath($url), '?m='.$hash.$version);
+ }
+
+ public static function emptyImageSrc(): string
+ {
+ return 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
+ }
+
+ public static function config(): array
+ {
+ return Config::inst()->get(__CLASS__);
+ }
+}
diff --git a/src/Templates/WebpackTemplateProvider.php b/src/Templates/WebpackTemplateProvider.php
new file mode 100755
index 0000000..0a77e06
--- /dev/null
+++ b/src/Templates/WebpackTemplateProvider.php
@@ -0,0 +1,153 @@
+ 'isActive',
+ 'WebpackCSS' => 'loadCSS',
+ 'WebpackJS' => 'loadJS',
+ 'ResourcesURL' => 'resourcesURL',
+ 'ProjectName' => 'themeName',
+ ];
+ }
+
+ /**
+ * Load CSS file
+ * @param $path
+ */
+ public static function loadCSS($path): void
+ {
+ /*if (self::isActive()) {
+ return;
+ }*/
+
+ Requirements::css(self::_getPath($path));
+ }
+
+ /**
+ * Load JS file
+ * @param $path
+ */
+ public static function loadJS($path): void
+ {
+ Requirements::javascript(self::_getPath($path));
+ }
+
+ public static function projectName(): string
+ {
+ return Config::inst()->get(ModuleManifest::class, 'project');
+ }
+
+ public static function mainTheme()
+ {
+ $themes = Config::inst()->get(SSViewer::class, 'themes');
+ return is_array($themes) && $themes[0] !== '$public' && $themes[0] !== '$default' ? $themes[0] : false;
+ }
+
+ public static function resourcesURL($link = null): string
+ {
+ $cfg = self::config();
+
+ if ($cfg['webp'] && !self::isActive()) {
+ $link = str_replace(['.png','.jpg','.jpeg'], '.webp', $link);
+ }
+
+ return Controller::join_links(
+ Director::baseURL(),
+ '/resources/',
+ self::projectName(),
+ $cfg['dist'],
+ 'img',
+ $link
+ );
+ }
+
+
+ /**
+ * Checks if dev mode is enabled and if webpack server is online
+ * @return bool
+ */
+ public static function isActive(): bool
+ {
+ $cfg = self::config();
+ return Director::isDev() && @fsockopen(
+ $cfg['HOSTNAME'],
+ $cfg['PORT']
+ );
+ }
+
+ protected static function _getPath($path): string
+ {
+ return self::isActive() && strpos($path, '//') === false ?
+ self::_toDevServerPath($path) :
+ self::toPublicPath($path);
+ }
+
+ protected static function _toDevServerPath($path): string
+ {
+ $cfg = self::config();
+ return sprintf(
+ '%s%s:%s/%s',
+ ($cfg['HTTPS'] ? 'https://' : 'http://'),
+ $cfg['HOSTNAME'],
+ $cfg['PORT'],
+ basename($path)
+ //Controller::join_links($cfg['APPDIR'], $cfg['SRC'], basename($path))
+ );
+ }
+
+ public static function toPublicPath($path): string
+ {
+ $cfg = self::config();
+ return strpos($path, '//') === false ?
+ Controller::join_links(
+ RESOURCES_DIR,
+ self::projectName(),
+ $cfg['dist'],
+ (strpos($path, '.css') ? 'css' : 'js'),
+ $path
+ )
+ : $path;
+ }
+
+ public static function config(): array
+ {
+ return Config::inst()->get(__CLASS__);
+ }
+}
diff --git a/src/Tests/TestServer.php b/src/Tests/TestServer.php
new file mode 100755
index 0000000..5167417
--- /dev/null
+++ b/src/Tests/TestServer.php
@@ -0,0 +1,142 @@
+table{width:100%}table td,table th{border:1px solid #dedede}';
+
+ echo 'Testing Server
';
+ echo self::success('BASE_PATH: '.BASE_PATH.'');
+ echo self::success('PHP: '.phpversion().'');
+
+ $v = Deprecation::dump_settings()['version'];
+ if ($v) {
+ echo self::success('SilverStipe version: '.$v.'');
+ } else {
+ echo self::success('SilverStipe version unknown: '
+ .(file_exists(BASE_PATH.'/framework') ? '3.x.x' : '4.x.x')
+ .'');
+ }
+
+ echo self::success('Memory limit: '.ini_get('memory_limit').'');
+
+
+ if (is_writable(TEMP_FOLDER)) {
+ echo self::success('TMP (cache) dir '.TEMP_FOLDER.' dir is writable.');
+ } else {
+ echo self::error('TMP (cache) dir '.TEMP_FOLDER.' dir is no writable!');
+ }
+
+ echo 'Testing Uploads
';
+ $maxUpload = ini_get('upload_max_filesize');
+ $maxPost = ini_get('post_max_size');
+
+ echo self::success('PHP max upload size: '.$maxUpload);
+ echo self::success('PHP max post size: '.$maxPost);
+ echo self::success('So calculated max upload size: '.self::formatBytes(min(
+ self::memstring2bytes($maxUpload),
+ self::memstring2bytes($maxPost)
+ )));
+
+ $defaultSizes = Config::inst()->get(Upload_Validator::class, 'default_max_file_size');
+ if ($defaultSizes) {
+ if (!is_array($defaultSizes)) {
+ echo self::error('default_max_file_size miss-configuration, plz fix');
+ var_dump($defaultSizes);
+ die();
+ }
+
+ echo 'Configured limits:
'
+ .'File | Size limit |
';
+ foreach ($defaultSizes as $k => $size) {
+ echo ''.$k.' | '.$size.' |
';
+ }
+ echo '
';
+ }
+
+ if (is_writable(ASSETS_DIR)) {
+ echo self::success('Assets dir '.ASSETS_DIR.' dir is writable.');
+ } else {
+ echo self::error('Assets dir '.ASSETS_DIR.' dir is no writable!');
+ }
+
+ if (function_exists('imagewebp')) {
+ echo self::success('WebP is available');
+ } else {
+ echo self::error('WebP is not available');
+ }
+
+
+ die();
+ }
+
+
+ public static function formatBytes($size, $precision = 2)
+ {
+ $base = log($size, 1024);
+ $suffixes = array('', 'K', 'M', 'G', 'T');
+
+ return round(pow(1024, $base - floor($base)), $precision) . $suffixes[(string)floor($base)];
+ }
+
+ public static function error($text)
+ {
+ return 'ERROR: '.$text.'
';
+ }
+
+ public static function success($text)
+ {
+ return 'SUCCESS: '.$text.'
';
+ }
+
+ public static function warning($text)
+ {
+ return 'WARNING: '.$text.'
';
+ }
+
+ public static function renderValidation($result)
+ {
+ echo '';
+ $msgs = $result->getMessages();
+ foreach ($msgs as $msg) {
+ echo self::error($msg['fieldName'].': '.$msg['message']);
+ }
+ echo '
';
+ }
+
+ public static function memstring2bytes($memString)
+ {
+ // Remove non-unit characters from the size
+ $unit = preg_replace('/[^bkmgtpezy]/i', '', $memString);
+ // Remove non-numeric characters from the size
+ $size = preg_replace('/[^0-9\.]/', '', $memString);
+
+ if ($unit) {
+ // Find the position of the unit in the ordered string which is the power
+ // of magnitude to multiply a kilobyte by
+ return round($size * pow(1024, stripos('bkmgtpezy', $unit[0])));
+ }
+
+ return round($size);
+ }
+}
diff --git a/src/Traits/PaginatedListing.php b/src/Traits/PaginatedListing.php
new file mode 100755
index 0000000..3242b45
--- /dev/null
+++ b/src/Traits/PaginatedListing.php
@@ -0,0 +1,18 @@
+getRequest()->requestVars();
+ $vars = array_filter($vars);
+ $vars['page'] = $pageID ? $pageID : '2';
+
+ return $this->Link('?'.http_build_query($vars));
+ }
+}
diff --git a/src/Widgets/BannerWidget.php b/src/Widgets/BannerWidget.php
new file mode 100755
index 0000000..af46ce1
--- /dev/null
+++ b/src/Widgets/BannerWidget.php
@@ -0,0 +1,66 @@
+';
+ private static $table_name = 'BannerWidget';
+
+ private static $has_one = [
+ 'Image' => Image::class,
+ 'Link' => Link::class,
+ ];
+
+ private static $owns = [
+ 'Image',
+ 'Link',
+ ];
+
+ public function getCMSFields()
+ {
+ $fields = parent::getCMSFields();
+
+ $fields->push(UploadField::create('Image', 'Image (minimal width 301px)')
+ ->setAllowedFileCategories(['image/supported']));
+
+ $fields->push(LinkField::create('LinkID', 'Link'));
+
+ return $fields;
+ }
+
+ private $_random;
+ public function Random()
+ {
+ if (!$this->_random) {
+ $this->_random = self::get()->filter('Enabled', true)->sort('RAND()')->first();
+ }
+
+ return $this->_random;
+ }
+
+ public function onBeforeWrite()
+ {
+ $title = $this->getField('Title');
+ $img = $this->Image();
+ if(!$title && $img) {
+ $this->setField('Title', $img->getTitle());
+ }
+
+ parent::onBeforeWrite();
+ }
+}
diff --git a/src/Widgets/ContentWidget.php b/src/Widgets/ContentWidget.php
new file mode 100755
index 0000000..04e7d43
--- /dev/null
+++ b/src/Widgets/ContentWidget.php
@@ -0,0 +1,37 @@
+';
+ private static $table_name = 'ContentWidget';
+
+ private static $db = [
+ 'Text' => 'HTMLText',
+ ];
+
+ public function getCMSFields()
+ {
+ $fields = parent::getCMSFields();
+
+ $fields->push(HTMLEditorField::create('Text'));
+
+ return $fields;
+ }
+}
diff --git a/src/Widgets/ElementWidget.php b/src/Widgets/ElementWidget.php
new file mode 100755
index 0000000..d03fe7e
--- /dev/null
+++ b/src/Widgets/ElementWidget.php
@@ -0,0 +1,60 @@
+';
+ private static $table_name = 'ElementWidget';
+
+ private static $has_one = [
+ 'Element' => BaseElement::class,
+ ];
+
+ public function getCMSFields()
+ {
+ $fields = parent::getCMSFields();
+
+ $fields->push(
+ DropdownField::create(
+ 'ElementID',
+ 'Displayed Element',
+ BaseElement::get()
+ ->filter(['AvailableGlobally' => true])
+ ->exclude(['ClassName' => ElementList::class])
+ )
+ /*TreeDropdownField::create(
+ 'ElementID',
+ 'Displayed Element',
+ SiteTree::class
+ )->setFilterFunction(static function($el){
+ return (bool) $el->getField('ElementalArea')->Elements()->count();
+ })*/
+ );
+
+ return $fields;
+ }
+
+ public function SimpleClassName()
+ {
+ $el = $this->getField('Element');
+ var_dump($el);
+ die();
+ return $el->getSimpleClassName();
+ }
+}
diff --git a/src/Widgets/LinksWidget.php b/src/Widgets/LinksWidget.php
new file mode 100755
index 0000000..522976b
--- /dev/null
+++ b/src/Widgets/LinksWidget.php
@@ -0,0 +1,56 @@
+';
+ private static $table_name = 'LinksWidget';
+
+ private static $many_many = [
+ 'Links' => Link::class,
+ ];
+
+ private static $owns = [
+ 'Links',
+ ];
+
+ public function getCMSFields()
+ {
+ $fields = parent::getCMSFields();
+
+ if($this->ID) {
+ $fields->push(GridField::create(
+ 'Links',
+ '',
+ $this->Links(),
+ GridFieldConfig_RecordEditor::create()
+ ));
+ }else{
+ $fields->push(LiteralField::create(
+ 'Note',
+ 'Note: The widget needs to be saved before adding a link.'
+ .' Enter the Title and click "+ Create" button at the bottom left corner of the screen
')
+ );
+ }
+
+ return $fields;
+ }
+}
diff --git a/src/Widgets/SubmenuWidget.php b/src/Widgets/SubmenuWidget.php
new file mode 100755
index 0000000..eff3a4d
--- /dev/null
+++ b/src/Widgets/SubmenuWidget.php
@@ -0,0 +1,53 @@
+';
+ private static $table_name = 'SubmenuWidget';
+
+ private static $db = [
+ 'TopLevelSubmenu' => 'Boolean(1)',
+ ];
+
+ public function getPage()
+ {
+ $area = $this->Parent();
+ return \Page::get()->filter('SideBarID', $area->ID)->first();
+ }
+
+ public function getSubmenu()
+ {
+ $page = $this->getPage();
+
+ if(!$this->getField('TopLevelSubmenu')) {
+ return $page->Children();
+ }
+
+ return $page->Level(1)->Children();
+ }
+
+ public function getCMSFields()
+ {
+ $fields = parent::getCMSFields();
+
+ $fields->push(CheckboxField::create(
+ 'TopLevelSubmenu',
+ 'Display sub-menu starting from the top level (otherwise current page children will be displayed)'
+ ));
+
+ return $fields;
+ }
+}
diff --git a/src/Widgets/WidgetAreaField.php b/src/Widgets/WidgetAreaField.php
new file mode 100755
index 0000000..83af016
--- /dev/null
+++ b/src/Widgets/WidgetAreaField.php
@@ -0,0 +1,157 @@
+setTypes($blockTypes);
+
+ $config = GridFieldConfig_Base::create();
+
+ $config->getComponentByType(GridFieldDataColumns::class)->setDisplayFields([
+ 'Icon' => '',
+ 'Title' => 'Title',
+ 'Enabled' => 'Enabled',
+ ])->setFieldFormatting([
+ 'Icon' => static function ($v, Widget $item) {
+ return ''.$item::config()->get('icon').'';
+ },
+ 'Enabled' => static function ($v, Widget $item) {
+ return $item->getField('Enabled') ? 'Yes' : 'No';
+ },
+ ]);
+
+ $config->addComponent(new GridFieldEditButton());
+ $config->addComponent(new GridFieldDeleteAction(false));
+ $config->addComponent(new GridFieldDetailForm(null, false, false));
+ $config->addComponent(new GridFieldSortableRows('Sort'));
+
+ if (!empty($blockTypes)) {
+ /** @var GridFieldAddNewMultiClass $adder */
+ $adder = Injector::inst()->create(GridFieldAddNewMultiClass::class);
+ $adder->setClasses($blockTypes);
+ $config->addComponent($adder);
+ }
+
+ // By default, no need for a title on the editor. If there is more than one area then use `setTitle` to describe
+ parent::__construct($name, '', $area->Widgets(), $config);
+
+ $this->area = $area;
+ $this->addExtraClass('element-editor__container no-change-track');
+ }
+
+ /**
+ * @param array $types
+ *
+ * @return $this
+ */
+ public function setTypes($types)
+ {
+ $this->types = $types;
+
+ return $this;
+ }
+
+ /**
+ * @return array
+ */
+ public function getTypes()
+ {
+ $types = $this->types;
+
+ $this->extend('updateGetTypes', $types);
+
+ return $types;
+ }
+
+ /**
+ * @return ElementalArea
+ */
+ public function getArea()
+ {
+ return $this->area;
+ }
+
+ public function saveInto(DataObjectInterface $dataObject)
+ {
+ parent::saveInto($dataObject);
+
+ $elementData = $this->Value();
+ $idPrefixLength = strlen(sprintf(ElementalAreaController::FORM_NAME_TEMPLATE, ''));
+
+ if (!$elementData) {
+ return;
+ }
+
+ foreach ($elementData as $form => $data) {
+ // Extract the ID
+ $elementId = (int) substr($form, $idPrefixLength);
+
+ // @var BaseElement $element
+ $element = $this->getArea()->Widgets()->byID($elementId);
+
+ if (!$element) {
+ // Ignore invalid elements
+ continue;
+ }
+
+ $data = ElementalAreaController::removeNamespacesFromFields($data, $element->ID);
+
+ $element->updateFromFormData($data);
+ $element->write();
+ }
+ }
+}
diff --git a/src/Widgets/WidgetExtension.php b/src/Widgets/WidgetExtension.php
new file mode 100755
index 0000000..0176722
--- /dev/null
+++ b/src/Widgets/WidgetExtension.php
@@ -0,0 +1,50 @@
+ 'Boolean(1)',
+ ];
+
+ public function updateCMSFields(FieldList $fields)
+ {
+ parent::updateCMSFields($fields);
+ // Add a combined field for "Title" and "Displayed" checkbox in a Bootstrap input group
+ $fields->removeByName('ShowTitle');
+ $fields->replaceField(
+ 'Title',
+ TextCheckboxGroupField::create()
+ ->setName('Title')
+ );
+ $fields->push(TreeDropdownField::create(
+ 'MovePageID', 'Move widget to page', SiteTree::class
+ ));
+ }
+
+ public function onBeforeWrite()
+ {
+ $obj = $this->owner;
+ $moveID = $obj->MovePageID;
+ if ($moveID) {
+ $page = \Page::get()->byID($moveID);
+ if($page) {
+ $sidebarID = $page->getField('SideBarID');
+ if($sidebarID) {
+ $obj->setField('ParentID', $sidebarID);
+ }
+ }
+ }
+
+ parent::onBeforeWrite();
+ }
+}
diff --git a/src/Widgets/WidgetPageExtension.php b/src/Widgets/WidgetPageExtension.php
new file mode 100755
index 0000000..9b5f3a4
--- /dev/null
+++ b/src/Widgets/WidgetPageExtension.php
@@ -0,0 +1,49 @@
+findOrMakeTab('Root.Widgets');
+
+ $tab->setTitle('Sidebar');
+
+ $tab->removeByName('SideBar');
+
+ $widgetTypes = WidgetAreaEditor::create('Sidebar')->AvailableWidgets();
+ $available = [];
+ /** @var Widget $type */
+ foreach ($widgetTypes as $type) {
+ $available[get_class($type)] = $type->getCMSTitle();
+ }
+
+ $tab->push(WidgetAreaField::create(
+ 'SideBar',
+ $this->owner->Sidebar(),
+ $available
+ ));
+ }
+
+ public function onBeforeWrite()
+ {
+ parent::onBeforeWrite();
+
+ if (!$this->owner->getField('SideBarID')) {
+ $area = WidgetArea::create();
+ $area->write();
+
+ $this->owner->setField('SideBarID', $area->ID);
+ }
+ }
+}
diff --git a/templates/A2nt/CMSNiceties/Widgets/BannerWidget.ss b/templates/A2nt/CMSNiceties/Widgets/BannerWidget.ss
new file mode 100755
index 0000000..67a35c9
--- /dev/null
+++ b/templates/A2nt/CMSNiceties/Widgets/BannerWidget.ss
@@ -0,0 +1,8 @@
+
+<% if $Link %>
+ <% with $Link %>
+ target="_blank"<% end_if %> class="stretched-link">
+ $Up.Title
+
+ <% end_with %>
+<% end_if %>
diff --git a/templates/A2nt/CMSNiceties/Widgets/ContentWidget.ss b/templates/A2nt/CMSNiceties/Widgets/ContentWidget.ss
new file mode 100755
index 0000000..9080bb9
--- /dev/null
+++ b/templates/A2nt/CMSNiceties/Widgets/ContentWidget.ss
@@ -0,0 +1,3 @@
+<% if $Text %>
+ $Text
+<% end_if %>
diff --git a/templates/A2nt/CMSNiceties/Widgets/ElementWidget.ss b/templates/A2nt/CMSNiceties/Widgets/ElementWidget.ss
new file mode 100755
index 0000000..eb383fc
--- /dev/null
+++ b/templates/A2nt/CMSNiceties/Widgets/ElementWidget.ss
@@ -0,0 +1,9 @@
+<% if $Element %>
+ <% with $Element %>
+
+ <% end_with %>
+<% end_if %>
diff --git a/templates/A2nt/CMSNiceties/Widgets/SubmenuWidget.ss b/templates/A2nt/CMSNiceties/Widgets/SubmenuWidget.ss
new file mode 100755
index 0000000..bfb4240
--- /dev/null
+++ b/templates/A2nt/CMSNiceties/Widgets/SubmenuWidget.ss
@@ -0,0 +1,18 @@
+<% if $Submenu %>
+
+<% end_if %>
diff --git a/templates/BreadcrumbsTemplate.ss b/templates/BreadcrumbsTemplate.ss
new file mode 100755
index 0000000..9663015
--- /dev/null
+++ b/templates/BreadcrumbsTemplate.ss
@@ -0,0 +1,21 @@
+<% if $Pages %>
+
+<% end_if %>
diff --git a/templates/GraphQLPage.ss b/templates/GraphQLPage.ss
new file mode 100755
index 0000000..f563df4
--- /dev/null
+++ b/templates/GraphQLPage.ss
@@ -0,0 +1 @@
+<% include MainContent Layout=$Layout %>
diff --git a/templates/Page.ss b/templates/Page.ss
new file mode 100755
index 0000000..d18ce72
--- /dev/null
+++ b/templates/Page.ss
@@ -0,0 +1,29 @@
+
+
+<%-- manifest="/cache.appcache" --%>
+
+ <% include MetaHead %>
+
+
+ data-default-lng="$Longitude" data-default-lat="$Latitude"<% end_with %>>
+
+ <% include First %>
+
+
+
+
+
+
+ <% include MainContent Layout=$Layout %>
+
+
+
+
+
+ <% include Last %>
+
+
diff --git a/templates/WidgetHolder.ss b/templates/WidgetHolder.ss
new file mode 100755
index 0000000..328562b
--- /dev/null
+++ b/templates/WidgetHolder.ss
@@ -0,0 +1,4 @@
+