feat: add new 'waterpark' theme, based on wuttaweb w/ vue2 + buefy

hoping to eventually replace the 'default' view with this one, if all
goes well.  definitely needs more testing and is not exposed as an
option yet, unless configured
This commit is contained in:
Lance Edgar 2024-08-20 13:46:40 -05:00
parent 1ec1eba496
commit 59bd58aca7
23 changed files with 937 additions and 44 deletions

View file

@ -0,0 +1,486 @@
## -*- coding: utf-8; -*-
<%inherit file="wuttaweb:templates/base.mako" />
<%namespace name="base_meta" file="/base_meta.mako" />
<%namespace file="/formposter.mako" import="declare_formposter_mixin" />
<%namespace file="/grids/filter-components.mako" import="make_grid_filter_components" />
<%namespace name="page_help" file="/page_help.mako" />
<%def name="base_styles()">
${parent.base_styles()}
<style>
.filters .filter-fieldname .field,
.filters .filter-fieldname .field label {
width: 100%;
}
.filters .filter-fieldname,
.filters .filter-fieldname .field label,
.filters .filter-fieldname .button {
justify-content: left;
}
.filters .filter-verb .select,
.filters .filter-verb .select select {
width: 100%;
}
% if filter_fieldname_width is not Undefined:
.filters .filter-fieldname,
.filters .filter-fieldname .button {
min-width: ${filter_fieldname_width};
}
.filters .filter-verb {
min-width: ${filter_verb_width};
}
% endif
</style>
</%def>
<%def name="before_content()">
## TODO: this must come before the self.body() call..but why?
${declare_formposter_mixin()}
</%def>
<%def name="render_navbar_brand()">
<div class="navbar-brand">
<a class="navbar-item" href="${url('home')}"
v-show="!menuSearchActive">
${base_meta.header_logo()}
<div id="global-header-title">
${base_meta.global_title()}
</div>
</a>
<div v-show="menuSearchActive"
class="navbar-item">
<b-autocomplete ref="menuSearchAutocomplete"
v-model="menuSearchTerm"
:data="menuSearchFilteredData"
field="label"
open-on-focus
keep-first
icon-pack="fas"
clearable
@keydown.native="menuSearchKeydown"
@select="menuSearchSelect">
</b-autocomplete>
</div>
<a role="button" class="navbar-burger" data-target="navbar-menu" aria-label="menu" aria-expanded="false">
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
</a>
</div>
</%def>
<%def name="render_navbar_start()">
<div class="navbar-start">
<div v-if="menuSearchData.length"
class="navbar-item">
<b-button type="is-primary"
size="is-small"
@click="menuSearchInit()">
<span><i class="fa fa-search"></i></span>
</b-button>
</div>
% for topitem in menus:
% if topitem['is_link']:
${h.link_to(topitem['title'], topitem['url'], target=topitem['target'], class_='navbar-item')}
% else:
<div class="navbar-item has-dropdown is-hoverable">
<a class="navbar-link">${topitem['title']}</a>
<div class="navbar-dropdown">
% for item in topitem['items']:
% if item['is_menu']:
<% item_hash = id(item) %>
<% toggle = 'menu_{}_shown'.format(item_hash) %>
<div>
<a class="navbar-link" @click.prevent="toggleNestedMenu('${item_hash}')">
${item['title']}
</a>
</div>
% for subitem in item['items']:
% if subitem['is_sep']:
<hr class="navbar-divider" v-show="${toggle}">
% else:
${h.link_to("{}".format(subitem['title']), subitem['url'], class_='navbar-item nested', target=subitem['target'], **{'v-show': toggle})}
% endif
% endfor
% else:
% if item['is_sep']:
<hr class="navbar-divider">
% else:
${h.link_to(item['title'], item['url'], class_='navbar-item', target=item['target'])}
% endif
% endif
% endfor
</div>
</div>
% endif
% endfor
</div>
</%def>
<%def name="render_theme_picker()">
% if expose_theme_picker and request.has_perm('common.change_app_theme'):
<div class="level-item">
${h.form(url('change_theme'), method="post", ref='themePickerForm')}
${h.csrf_token(request)}
<input type="hidden" name="referrer" :value="referrer" />
<div style="display: flex; align-items: center; gap: 0.5rem;">
<span>Theme:</span>
<b-select name="theme"
v-model="globalTheme"
@input="changeTheme()">
% for option in theme_picker_options:
<option value="${option.value}">
${option.label}
</option>
% endfor
</b-select>
</div>
${h.end_form()}
</div>
% endif
</%def>
<%def name="render_feedback_button()">
<div class="level-item">
<page-help
% if can_edit_help:
@configure-fields-help="configureFieldsHelp = true"
% endif
/>
</div>
% if request.has_perm('common.feedback'):
<feedback-form
action="${url('feedback')}"
:message="feedbackMessage">
</feedback-form>
% endif
</%def>
<%def name="render_this_page_component()">
<this-page @change-content-title="changeContentTitle"
% if can_edit_help:
:configure-fields-help="configureFieldsHelp"
% endif
/>
</%def>
<%def name="render_vue_templates()">
${parent.render_vue_templates()}
${page_help.render_template()}
${page_help.declare_vars()}
% if request.has_perm('common.feedback'):
<script type="text/x-template" id="feedback-template">
<div>
<div class="level-item">
<b-button type="is-primary"
@click="showFeedback()"
icon-pack="fas"
icon-left="comment">
Feedback
</b-button>
</div>
<b-modal has-modal-card
:active.sync="showDialog">
<div class="modal-card">
<header class="modal-card-head">
<p class="modal-card-title">User Feedback</p>
</header>
<section class="modal-card-body">
<p class="block">
Questions, suggestions, comments, complaints, etc.
<span class="red">regarding this website</span> are
welcome and may be submitted below.
</p>
<b-field label="User Name">
<b-input v-model="userName"
% if request.user:
disabled
% endif
>
</b-input>
</b-field>
<b-field label="Referring URL">
<b-input
v-model="referrer"
disabled="true">
</b-input>
</b-field>
<b-field label="Message">
<b-input type="textarea"
v-model="message"
ref="textarea">
</b-input>
</b-field>
% if config.get_bool('tailbone.feedback_allows_reply'):
<div class="level">
<div class="level-left">
<div class="level-item">
<b-checkbox v-model="pleaseReply"
@input="pleaseReplyChanged">
Please email me back{{ pleaseReply ? " at: " : "" }}
</b-checkbox>
</div>
<div class="level-item" v-show="pleaseReply">
<b-input v-model="userEmail"
ref="userEmail">
</b-input>
</div>
</div>
</div>
% endif
</section>
<footer class="modal-card-foot">
<b-button @click="showDialog = false">
Cancel
</b-button>
<b-button type="is-primary"
icon-pack="fas"
icon-left="paper-plane"
@click="sendFeedback()"
:disabled="sendingFeedback || !message.trim()">
{{ sendingFeedback ? "Working, please wait..." : "Send Message" }}
</b-button>
</footer>
</div>
</b-modal>
</div>
</script>
<script>
const FeedbackForm = {
template: '#feedback-template',
mixins: [SimpleRequestMixin],
props: [
'action',
'message',
],
methods: {
showFeedback() {
this.referrer = location.href
this.showDialog = true
this.$nextTick(function() {
this.$refs.textarea.focus()
})
},
% if config.get_bool('tailbone.feedback_allows_reply'):
pleaseReplyChanged(value) {
this.$nextTick(() => {
this.$refs.userEmail.focus()
})
},
% endif
sendFeedback() {
this.sendingFeedback = true
const params = {
referrer: this.referrer,
user: this.userUUID,
user_name: this.userName,
% if config.get_bool('tailbone.feedback_allows_reply'):
please_reply_to: this.pleaseReply ? this.userEmail : null,
% endif
message: this.message.trim(),
}
this.simplePOST(this.action, params, response => {
this.$buefy.toast.open({
message: "Message sent! Thank you for your feedback.",
type: 'is-info',
duration: 4000, // 4 seconds
})
this.showDialog = false
// clear out message, in case they need to send another
this.message = ""
this.sendingFeedback = false
}, response => { // failure
this.sendingFeedback = false
})
},
}
}
const FeedbackFormData = {
referrer: null,
userUUID: null,
userName: null,
userEmail: null,
% if config.get_bool('tailbone.feedback_allows_reply'):
pleaseReply: false,
% endif
showDialog: false,
sendingFeedback: false,
}
</script>
% endif
</%def>
<%def name="modify_vue_vars()">
${parent.modify_vue_vars()}
<script>
##############################
## menu search
##############################
WholePageData.menuSearchActive = false
WholePageData.menuSearchTerm = ''
WholePageData.menuSearchData = ${json.dumps(global_search_data or [])|n}
WholePage.computed.menuSearchFilteredData = function() {
if (!this.menuSearchTerm.length) {
return this.menuSearchData
}
const terms = []
for (let term of this.menuSearchTerm.toLowerCase().split(' ')) {
term = term.trim()
if (term) {
terms.push(term)
}
}
if (!terms.length) {
return this.menuSearchData
}
// all terms must match
return this.menuSearchData.filter((option) => {
const label = option.label.toLowerCase()
for (const term of terms) {
if (label.indexOf(term) < 0) {
return false
}
}
return true
})
}
WholePage.methods.globalKey = function(event) {
// Ctrl+8 opens menu search
if (event.target.tagName == 'BODY') {
if (event.ctrlKey && event.key == '8') {
this.menuSearchInit()
}
}
}
WholePage.mounted = function() {
window.addEventListener('keydown', this.globalKey)
for (let hook of this.mountedHooks) {
hook(this)
}
}
WholePage.beforeDestroy = function() {
window.removeEventListener('keydown', this.globalKey)
}
WholePage.methods.menuSearchInit = function() {
this.menuSearchTerm = ''
this.menuSearchActive = true
this.$nextTick(() => {
this.$refs.menuSearchAutocomplete.focus()
})
}
WholePage.methods.menuSearchKeydown = function(event) {
// ESC will dismiss searchbox
if (event.which == 27) {
this.menuSearchActive = false
}
}
WholePage.methods.menuSearchSelect = function(option) {
location.href = option.url
}
##############################
## theme picker
##############################
% if expose_theme_picker and request.has_perm('common.change_app_theme'):
WholePageData.globalTheme = ${json.dumps(theme or None)|n}
## WholePageData.referrer = location.href
WholePage.methods.changeTheme = function() {
this.$refs.themePickerForm.submit()
}
% endif
##############################
## feedback
##############################
% if request.has_perm('common.feedback'):
WholePageData.feedbackMessage = ""
% if request.user:
FeedbackFormData.userUUID = ${json.dumps(request.user.uuid)|n}
FeedbackFormData.userName = ${json.dumps(str(request.user))|n}
% endif
% endif
##############################
## edit fields help
##############################
% if can_edit_help:
WholePageData.configureFieldsHelp = false
% endif
</script>
</%def>
<%def name="make_vue_components()">
${parent.make_vue_components()}
${h.javascript_link(request.static_url('tailbone:static/js/tailbone.buefy.datepicker.js') + f'?ver={tailbone.__version__}')}
${h.javascript_link(request.static_url('tailbone:static/js/tailbone.buefy.numericinput.js') + f'?ver={tailbone.__version__}')}
${h.javascript_link(request.static_url('tailbone:static/js/tailbone.buefy.oncebutton.js') + f'?ver={tailbone.__version__}')}
${h.javascript_link(request.static_url('tailbone:static/js/tailbone.buefy.timepicker.js') + f'?ver={tailbone.__version__}')}
${make_grid_filter_components()}
${page_help.make_component()}
% if request.has_perm('common.feedback'):
<script>
FeedbackForm.data = function() { return FeedbackFormData }
Vue.component('feedback-form', FeedbackForm)
</script>
% endif
</%def>