feat: VivesPOS landing on Winter CMS 1.2 — theme + plugin + Dockerfile
Some checks are pending
Module sub-split / Sub-split (push) Waiting to run

- Base: wintercms/winter branch 1.2 (full framework)
- Theme vivespos: Canvas 7 + Bootstrap 5 CDN, custom CSS
- Layout: deferred GTM/GA4 tracking, JSON-LD SoftwareApplication
- Partials: hero (offline-first), features, modes (offline/nube toggle),
  screenshots, pricing (3 planes), comparison, FAQ, CTA
- Plugin VivesPOS.Site with ContactForm
- Dockerfile: PHP 8.2 Apache, port 80, healthcheck
- Added winter/wn-pages, blog, sitemap, seo plugins
- Active theme set to vivespos
This commit is contained in:
2026-08-21 19:29:00 -06:00
commit 1f72193a64
3266 changed files with 531480 additions and 0 deletions

View File

@@ -0,0 +1,250 @@
(function ($) {
// Add an option for your plugin.
// $.FroalaEditor.DEFAULTS = $.extend($.FroalaEditor.DEFAULTS, {
// myOption: false
// });
$.FroalaEditor.PLUGINS.figures = function (editor) {
/**
* Insert UI Blocks
*/
function insertElement($el) {
var html = $('<div />').append($el.clone()).remove().html()
// Make sure we have focus.
editor.events.focus(true)
editor.selection.restore()
editor.html.insert(html)
editor.html.cleanEmptyTags()
// Clean up wrapping paragraphs or empty paragraphs
$('figure', editor.$el).each(function() {
var $this = $(this),
$parent = $this.parent('p'),
$next = $this.next('p')
// If block is inserted to a paragraph, insert it afterwards.
if (!!$parent.length) {
$this.insertAfter($parent)
}
// Inserting a figure tag will put an empty paragraph tag
// directly after it, strip these instances out
if (!!$next.length && $.trim($next.text()).length == 0) {
$next.remove()
}
})
editor.undo.saveStep()
}
function _makeUiBlockElement() {
var $node = $('<figure contenteditable="false" tabindex="0" data-ui-block="true">&nbsp;</figure>')
$node.get(0).contentEditable = false
return $node
}
function insertVideo(url, text) {
var $node = _makeUiBlockElement()
$node.attr('data-video', url)
$node.attr('data-label', text)
insertElement($node)
}
function insertAudio(url, text) {
var $node = _makeUiBlockElement()
$node.attr('data-audio', url)
$node.attr('data-label', text)
insertElement($node)
}
/**
* Init UI Blocks
*/
function _initUiBlocks () {
$('[data-video], [data-audio]', editor.$el).each(function() {
$(this)
.addClass('fr-draggable')
.attr({
'data-ui-block': 'true',
'draggable': 'true',
'tabindex': '0'
})
.html('&nbsp;')
this.contentEditable = false
})
}
function _handleUiBlocksKeydown(ev) {
if (ev.key === 'ArrowDown' || ev.key === 'ArrowUp' || ev.key === 'Backspace' || ev.key === 'Delete') {
var $block = $(editor.selection.element())
if ($block.is('br')) {
$block = $block.parent()
}
if (!!$block.length) {
switch (ev.key) {
case 'ArrowUp':
_handleUiBlockCaretIn($block.prev())
break
case 'ArrowDown':
_handleUiBlockCaretIn($block.next())
break
case 'Delete':
_handleUiBlockCaretClearEmpty($block.next(), $block)
break
case 'Backspace':
_handleUiBlockCaretClearEmpty($block.prev(), $block)
break
}
}
}
}
function _handleUiBlockCaretClearEmpty($block, $p) {
if ($block.attr('data-ui-block') !== undefined && $.trim($p.text()).length == 0) {
$p.remove()
_handleUiBlockCaretIn($block)
editor.undo.saveStep()
}
}
function _handleUiBlockCaretIn($block) {
if ($block.attr('data-ui-block') !== undefined) {
$block.focus()
editor.selection.clear()
return true
}
return false
}
function _uiBlockKeyDown(ev, block) {
if (ev.key === 'ArrowDown' || ev.key === 'ArrowUp' || ev.key === 'Enter' || ev.key === 'Backspace' || ev.key === 'Delete') {
switch (ev.key) {
case 'ArrowDown':
_focusUiBlockOrText($(block).next(), true)
break
case 'ArrowUp':
_focusUiBlockOrText($(block).prev(), false)
break
case 'Enter':
var $paragraph = $('<p><br/></p>')
$paragraph.insertAfter(block)
editor.selection.setAfter(block)
editor.selection.restore()
editor.undo.saveStep()
break
case 'Backspace':
case 'Delete':
var $nextFocus = $(block).next(),
gotoStart = true
if ($nextFocus.length == 0) {
$nextFocus = $(block).prev()
gotoStart = false
}
_focusUiBlockOrText($nextFocus, gotoStart)
$(block).remove()
editor.undo.saveStep()
break
}
ev.preventDefault()
}
}
function _focusUiBlockOrText($block, gotoStart) {
if (!!$block.length) {
if (!_handleUiBlockCaretIn($block)) {
if (gotoStart) {
editor.selection.setAtStart($block.get(0))
editor.selection.restore()
}
else {
editor.selection.setAtEnd($block.get(0))
editor.selection.restore()
}
}
}
}
/**
* Keydown
*/
function _onKeydown (ev) {
_handleUiBlocksKeydown(ev)
if (ev.isDefaultPrevented()) {
return false
}
}
function _onFigureKeydown(ev) {
if (ev.target && $(ev.target).attr('data-ui-block') !== undefined) {
_uiBlockKeyDown(ev, ev.target)
}
if (ev.isDefaultPrevented()) {
return false
}
}
/**
* Sync
*/
function _onSync(html) {
var $domTree = $('<div>' + html + '</div>')
$domTree.find('[data-video], [data-audio]').each(function(){
$(this)
.removeAttr('contenteditable data-ui-block tabindex draggable')
.removeClass('fr-draggable fr-dragging')
})
return $domTree.html()
}
/**
* Init.
*/
function _init () {
editor.events.on('initialized', _initUiBlocks)
editor.events.on('html.set', _initUiBlocks)
editor.events.on('html.get', _onSync)
editor.events.on('keydown', _onKeydown)
editor.events.on('destroy', _destroy, true)
editor.$el.on('keydown', 'figure', _onFigureKeydown)
}
/**
* Destroy.
*/
function _destroy () {
editor.$el.off('keydown', 'figure', _onFigureKeydown)
}
return {
_init: _init,
insert: insertElement,
insertVideo: insertVideo,
insertAudio: insertAudio
}
}
})(jQuery);

View File

@@ -0,0 +1,287 @@
(function ($) {
$.FroalaEditor.PLUGINS.mediaManager = function (editor) {
function onInsertFile() {
new $.wn.mediaManager.popup({
alias: 'ocmediamanager',
cropAndInsertButton: false,
onInsert: function(items) {
if (!items.length) {
$.wn.alert($.wn.lang.get('mediamanager.invalid_file_empty_insert'))
return
}
if (items.length > 1) {
$.wn.alert($.wn.lang.get('mediamanager.invalid_file_single_insert'))
return
}
var link,
text = editor.selection.text(),
textIsEmpty = $.trim(text) === ''
for (var i=0, len=items.length; i<len; i++) {
var text = textIsEmpty ? items[i].title : text
link = items[i].publicUrl
}
// Focus in the editor.
editor.events.focus(true);
editor.selection.restore();
// Insert the link.
editor.html.insert('<a href="' + link + '" id="fr-inserted-file" class="fr-file">' + text + '</a>');
// Get the file.
var $file = editor.$el.find('#fr-inserted-file');
$file.removeAttr('id');
editor.undo.saveStep()
this.hide()
}
})
}
function onInsertImage() {
var $currentImage = editor.image.get(),
selection = editor.selection.get(),
range = editor.selection.ranges(0);
new $.wn.mediaManager.popup({
alias: 'ocmediamanager',
cropAndInsertButton: true,
onInsert: function(items) {
editor.selection.clear();
selection.addRange(range);
if (!items.length) {
$.wn.alert($.wn.lang.get('mediamanager.invalid_image_empty_insert'))
return
}
var imagesInserted = 0
for (var i=0, len=items.length; i<len; i++) {
if (items[i].documentType !== 'image') {
$.wn.alert($.wn.lang.get('mediamanager.invalid_image_invalid_insert', 'The file "'+items[i].title+'" is not an image.'))
continue
}
editor.image.insert(items[i].publicUrl, false, {}, $currentImage)
imagesInserted++
if (imagesInserted == 1) {
$currentImage = null
}
}
if (imagesInserted !== 0) {
this.hide()
editor.undo.saveStep()
}
}
})
}
function onInsertVideo() {
new $.wn.mediaManager.popup({
alias: 'ocmediamanager',
cropAndInsertButton: false,
onInsert: function(items) {
if (!items.length) {
$.wn.alert($.wn.lang.get('mediamanager.invalid_video_empty_insert'))
return
}
if (items.length > 1) {
$.wn.alert($.wn.lang.get('mediamanager.invalid_file_single_insert'))
return
}
var item = items[0]
if (item.documentType !== 'video') {
$.wn.alert($.wn.lang.get('mediamanager.invalid_video_invalid_insert', 'The file "'+item.title+'" is not a video.'))
return
}
var $richEditorNode = editor.$el.closest('[data-control="richeditor"]')
$richEditorNode.richEditor('insertVideo', item.publicUrl, item.title)
this.hide()
}
})
}
function onInsertAudio() {
new $.wn.mediaManager.popup({
alias: 'ocmediamanager',
cropAndInsertButton: false,
onInsert: function(items) {
if (!items.length) {
$.wn.alert($.wn.lang.get('mediamanager.invalid_audio_empty_insert'))
return
}
if (items.length > 1) {
$.wn.alert($.wn.lang.get('mediamanager.invalid_file_single_insert'))
return
}
var item = items[0]
if (item.documentType !== 'audio') {
$.wn.alert($.wn.lang.get('mediamanager.invalid_audio_invalid_insert', 'The file "'+item.title+'" is not an audio file.'))
return
}
var $richEditorNode = editor.$el.closest('[data-control="richeditor"]')
$richEditorNode.richEditor('insertAudio', item.publicUrl, item.title)
this.hide()
}
})
}
function _insertVideoFallback(link) {
var $richEditorNode = editor.$el.closest('[data-control="richeditor"]')
var title = link.substring(link.lastIndexOf('/') + 1)
$richEditorNode.richEditor('insertVideo', link, title)
editor.popups.hide('video.insert')
}
function _insertAudioFallback(link) {
var $richEditorNode = editor.$el.closest('[data-control="richeditor"]')
var title = link.substring(link.lastIndexOf('/') + 1)
$richEditorNode.richEditor('insertAudio', link, title)
editor.popups.hide('audio.insert')
}
/**
* Init.
*/
function _init () {
editor.events.on('destroy', _destroy, true)
editor.events.on('video.linkError', _insertVideoFallback)
editor.events.on('audio.linkError', _insertAudioFallback)
}
/**
* Destroy.
*/
function _destroy () {
}
// Expose public methods. If _init is not public then the plugin won't be initialized.
// Public method can be accessed through the editor API:
// $('.selector').froalaEditor('mediaManager.publicMethod');
return {
_init: _init,
insertFile: onInsertFile,
insertImage: onInsertImage,
insertVideo: onInsertVideo,
insertAudio: onInsertAudio
}
}
if (!$.FE.PLUGINS.link || !$.FE.PLUGINS.file || !$.FE.PLUGINS.image || !$.FE.PLUGINS.video) {
throw new Error('Media manager plugin requires link, file, image and video plugin.');
}
//
// Image
//
$.FE.DEFAULTS.imageInsertButtons.push('mmImageManager');
$.FE.RegisterCommand('mmImageManager', {
title: 'Browse',
undo: false,
focus: false,
callback: function () {
this.mediaManager.insertImage();
},
plugin: 'mediaManager'
})
// Add the font size icon.
$.FE.DefineIcon('mmImageManager', {
NAME: 'folder'
});
//
// File
//
$.FE.DEFAULTS.fileInsertButtons.push('mmFileManager');
$.FE.RegisterCommand('mmFileManager', {
title: 'Browse',
undo: false,
focus: false,
callback: function () {
this.mediaManager.insertFile();
},
plugin: 'mediaManager'
})
// Add the font size icon.
$.FE.DefineIcon('mmFileManager', {
NAME: 'folder'
});
//
// Video
//
$.FE.DEFAULTS.videoInsertButtons.push('mmVideoManager');
$.FE.RegisterCommand('mmVideoManager', {
title: 'Browse',
undo: false,
focus: false,
callback: function () {
this.mediaManager.insertVideo();
},
plugin: 'mediaManager'
})
// Add the font size icon.
$.FE.DefineIcon('mmVideoManager', {
NAME: 'folder'
});
//
// Audio
//
$.FE.DEFAULTS.audioInsertButtons.push('mmAudioManager');
$.FE.RegisterCommand('mmAudioManager', {
title: 'Browse',
undo: false,
focus: false,
callback: function () {
this.mediaManager.insertAudio();
},
plugin: 'mediaManager'
})
// Add the font size icon.
$.FE.DefineIcon('mmAudioManager', {
NAME: 'folder'
});
})(jQuery);

View File

@@ -0,0 +1,109 @@
//
// Page links
//
var richeditorPageLinksPlugin
function richeditorPageLinksSelectPage($form) {
richeditorPageLinksPlugin.setLinkValueFromPopup($form)
}
$.FroalaEditor.DEFAULTS = $.extend($.FroalaEditor.DEFAULTS, {
pageLinksHandler: 'onLoadPageLinksForm'
});
$.FroalaEditor.DEFAULTS.key = 'JA6B2B5A1qB1F1F4D3I1A15A11D3E6B5dVh1VCQWa1EOQFe1NCb1==';
(function ($) {
$.FroalaEditor.PLUGINS.pageLinks = function (editor) {
function setLinkValueFromPopup($form) {
var $select = $('select[name=pagelink]', $form)
var link = {
text: $('option:selected', $select).text().trim(),
href: $select.val()
}
// Wait for popup to close
setTimeout(function() {
editor.popups.show('link.insert')
setLinkValue(link)
}, 300)
}
function setLinkValue(link) {
var $popup = editor.popups.get('link.insert');
var text_inputs = $popup.find('input.fr-link-attr[type="text"]');
var check_inputs = $popup.find('input.fr-link-attr[type="checkbox"]');
var $input;
var i;
for (i = 0; i < text_inputs.length; i++) {
$input = $(text_inputs[i]);
var name = $input.attr('name');
var value = link[name];
// Only change the text of the link to be inserted if it has not already been set
if (name === 'text') {
if ($input.val().length === 0) {
$input.val(value);
}
} else {
$input.val(value);
}
}
for (i = 0; i < check_inputs.length; i++) {
$input = $(check_inputs[i]);
$input.prop('checked', $input.data('checked') == link[$input.attr('name')]);
}
// Restore selection, so that the link gets inserted properly.
editor.selection.restore();
}
function insertLink() {
richeditorPageLinksPlugin = this
editor.$el.popup({
handler: editor.opts.pageLinksHandler
}).one('shown.oc.popup.pageLinks', function () {
// Save the current selection so it can be restored after popup is closed.
editor.selection.save()
})
}
/**
* Init.
*/
function _init () {
}
return {
_init: _init,
setLinkValueFromPopup: setLinkValueFromPopup,
setLinkValue: setLinkValue,
insertLink: insertLink
}
}
$.FE.DEFAULTS.linkInsertButtons = ['linkBack', '|', 'linkPageLinks']
$.FE.RegisterCommand('linkPageLinks', {
title: 'Choose Link',
undo: false,
focus: false,
callback: function () {
this.pageLinks.insertLink()
},
plugin: 'pageLinks'
})
// Add the font size icon.
$.FE.DefineIcon('linkPageLinks', {
NAME: 'search'
});
})(jQuery);