mirror of
https://gitlab.com/PronounsPage/PronounsPage.git
synced 2025-09-28 15:31:11 -04:00
Merge branch 'opinions'
This commit is contained in:
commit
c3175be874
@ -7,8 +7,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
body[data-theme="dark"] {
|
body[data-theme="dark"] {
|
||||||
$primary-dark: #ff95bb;
|
|
||||||
|
|
||||||
background: initial !important;
|
background: initial !important;
|
||||||
background-color: $dark !important;
|
background-color: $dark !important;
|
||||||
color: $light;
|
color: $light;
|
||||||
@ -332,4 +330,12 @@ body[data-theme="dark"] {
|
|||||||
background-color: darken($code-color, 30%);
|
background-color: darken($code-color, 30%);
|
||||||
border: 1px solid lighten($code-color, 30%);
|
border: 1px solid lighten($code-color, 30%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:not(.reduced-colours) {
|
||||||
|
@each $name, $value in $colours {
|
||||||
|
.colour-#{$name} {
|
||||||
|
color: map-get($value, 'dark') !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
@use "sass:list";
|
@use "sass:list";
|
||||||
|
|
||||||
@import url('https://fonts.googleapis.com/css2?family=Noto+Emoji&display=swap');
|
@import url('https://fonts.googleapis.com/css2?family=Noto+Emoji:wght@700&display=swap');
|
||||||
|
|
||||||
@if list.index($fonts, 'Quicksand') {
|
@if list.index($fonts, 'Quicksand') {
|
||||||
/* quicksand-regular - latin-ext_latin */
|
/* quicksand-regular - latin-ext_latin */
|
||||||
|
@ -246,3 +246,18 @@ form[disabled] {
|
|||||||
border-inline-start: 3px solid $primary;
|
border-inline-start: 3px solid $primary;
|
||||||
padding-inline-start: calc(#{$list-group-item-padding-x} - 2px);
|
padding-inline-start: calc(#{$list-group-item-padding-x} - 2px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bold {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.italics {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
body:not(.reduced-colours) {
|
||||||
|
@each $name, $value in $colours {
|
||||||
|
.colour-#{$name} {
|
||||||
|
color: map-get($value, 'light') !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -33,3 +33,14 @@ $square-button-size: 2.2rem;
|
|||||||
@import "~bootstrap/scss/utilities";
|
@import "~bootstrap/scss/utilities";
|
||||||
|
|
||||||
@import '~@fortawesome/fontawesome-pro/scss/variables';
|
@import '~@fortawesome/fontawesome-pro/scss/variables';
|
||||||
|
|
||||||
|
$primary-dark: #ff95bb;
|
||||||
|
|
||||||
|
$colours: (
|
||||||
|
'pink': ('light': $primary, 'dark': $primary-dark),
|
||||||
|
'red': ('light': $red, 'dark': $red-200),
|
||||||
|
'orange': ('light': $orange-600, 'dark': $orange-300),
|
||||||
|
'green': ('light': $green, 'dark': $green-300),
|
||||||
|
'blue': ('light': $blue-700, 'dark': $blue-200),
|
||||||
|
'grey': ('light': $gray-600, 'dark': $gray-300),
|
||||||
|
);
|
||||||
|
@ -54,7 +54,7 @@ export default {
|
|||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
enabled() {
|
enabled() {
|
||||||
return this.config.ads?.enabled;
|
return this.config.ads?.enabled && process.env.NODE_ENV !== 'development';
|
||||||
},
|
},
|
||||||
visible() {
|
visible() {
|
||||||
return this.enabled && this.consent === undefined;
|
return this.enabled && this.consent === undefined;
|
||||||
|
63
components/IconSelector.vue
Normal file
63
components/IconSelector.vue
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
<template>
|
||||||
|
<div class="bg-light border rounded">
|
||||||
|
<input class="form-control mb-1" v-model="filter" :placeholder="$t('crud.search')" ref="filter"/>
|
||||||
|
<ul class="list-unstyled icons-list p-2 text-center">
|
||||||
|
<li v-for="icon in visibleIcons"
|
||||||
|
class="list-inline-item">
|
||||||
|
<button class="btn btn-outline-dark border-0 my-2" @click.prevent="$emit('change', icon.name)">
|
||||||
|
<Icon :v="icon.name"/>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li v-if="!showAll && visibleIcons.length >= displayLimit" class="list-inline-item">
|
||||||
|
<button class="btn btn-outline-dark border-0 my-2" @click.prevent="showAll = true">
|
||||||
|
<T>crud.loadAll</T>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import icons from '../src/icons';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
styles: { 'default': () => ['light'] },
|
||||||
|
skipIcons: { 'default': () => [] },
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
icons,
|
||||||
|
filter: '',
|
||||||
|
showAll: false,
|
||||||
|
displayLimit: 27,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.$refs.filter.focus();
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
visibleIcons() {
|
||||||
|
return this.icons.filter(this.matches).slice(0, this.showAll ? undefined : this.displayLimit);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
matches(icon) {
|
||||||
|
return !this.skipIcons.includes(icon.name)
|
||||||
|
&& icon.styles.filter(v => this.styles.includes(v)).length > 0
|
||||||
|
&& (
|
||||||
|
this.filter === ''
|
||||||
|
|| icon.searchTerms.filter(t => t.includes(this.filter.toLowerCase())).length > 0
|
||||||
|
)
|
||||||
|
;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.icons-list {
|
||||||
|
height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
</style>
|
108
components/LegendOpinionListInput.vue
Normal file
108
components/LegendOpinionListInput.vue
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
<template>
|
||||||
|
<ListInput v-model="v" :prototype="prototype()" :group="group" :readonly="readonly" :maxlength="maxlength">
|
||||||
|
<template v-slot="s">
|
||||||
|
<button type="button" :class="['btn', readonly ? 'btn-light border' : 'btn-outline-secondary', showIconSelector === s.i ? 'btn-secondary text-white border' : '']" :disabled="readonly"
|
||||||
|
@click="showIconSelector = showIconSelector === s.i ? false : s.i">
|
||||||
|
<Icon :v="s.val.icon"/>
|
||||||
|
</button>
|
||||||
|
<input v-model="s.val.description" class="form-control" :readonly="readonly"
|
||||||
|
@keyup="s.update(s.val)" @paste="$nextTick(() => s.update(s.val))" @change="s.update(s.val)"
|
||||||
|
required maxlength="24"
|
||||||
|
:placeholder="$t('profile.opinions.description')"
|
||||||
|
/>
|
||||||
|
<select :class="['form-control', s.val.colour ? 'colour-' + s.val.colour : 'text-muted']" v-model="s.val.colour" @change="s.update(s.val)" :disabled="readonly">
|
||||||
|
<option v-for="colour in colours" :value="colour">{{$t(`profile.opinions.colours.${colour || '_'}`)}}</option>
|
||||||
|
</select>
|
||||||
|
<select :class="['form-control', s.val.style || 'text-muted']" v-model="s.val.style" @change="s.update(s.val)" :disabled="readonly">
|
||||||
|
<option v-for="st in styles" :value="st">{{$t(`profile.opinions.styles.${st || '_'}`)}}</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<IconSelector v-if="showIconSelector === s.i" class="hanging shadow shadow-lg border"
|
||||||
|
:skipIcons="skipIcons"
|
||||||
|
@change="s.update({...s.val, icon: $event}); showIconSelector = false"/>
|
||||||
|
</template>
|
||||||
|
<template v-slot:validation="s">
|
||||||
|
<p v-if="validation(s.val)" class="small text-danger">
|
||||||
|
<Icon v="exclamation-triangle"/>
|
||||||
|
<span class="ml-1">{{$t(validation(s.val))}}</span>
|
||||||
|
</p>
|
||||||
|
</template>
|
||||||
|
</ListInput>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { colours, styles } from '../src/styling';
|
||||||
|
import opinions from '../src/opinions';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
value: {},
|
||||||
|
group: {},
|
||||||
|
readonly: { type: Boolean },
|
||||||
|
maxlength: { 'default': null },
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
v: this.value,
|
||||||
|
showIconSelector: false,
|
||||||
|
colours,
|
||||||
|
styles,
|
||||||
|
skipIcons: [...Object.values(opinions).map(op => op.icon), 'ad', 'helicopter'],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
v() { this.$emit('input', this.v); },
|
||||||
|
value(v) { this.v = v; }
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
prototype() {
|
||||||
|
return {icon: '', description: '', colour: '', style: ''};
|
||||||
|
},
|
||||||
|
setIcon(icon) {
|
||||||
|
this.v.icon = icon;
|
||||||
|
this.showIconSelector = false;
|
||||||
|
this.$emit('input', this.v);
|
||||||
|
},
|
||||||
|
validation(v) {
|
||||||
|
if (JSON.stringify(v) === JSON.stringify(this.prototype())) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!v.icon) {
|
||||||
|
return 'profile.opinions.validation.missingIcon';
|
||||||
|
}
|
||||||
|
if (!v.description) {
|
||||||
|
return 'profile.opinions.validation.missingDescription';
|
||||||
|
}
|
||||||
|
if (this.v.filter(el => el.icon === v.icon).length > 1) {
|
||||||
|
return 'profile.opinions.validation.duplicateIcon';
|
||||||
|
}
|
||||||
|
if (this.v.filter(el => el.description === v.description).length > 1) {
|
||||||
|
return 'profile.opinions.validation.duplicateDescription';
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import "assets/variables";
|
||||||
|
|
||||||
|
// TODO
|
||||||
|
select > option.small {
|
||||||
|
font-size: $small-font-size !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hanging {
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 500px;
|
||||||
|
z-index: 5000;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
@ -3,22 +3,24 @@
|
|||||||
<li v-for="(v, i) in iVal" ref="items">
|
<li v-for="(v, i) in iVal" ref="items">
|
||||||
<div>
|
<div>
|
||||||
<div class="input-group input-group-sm mb-1">
|
<div class="input-group input-group-sm mb-1">
|
||||||
<button class="btn btn-light border handle" type="button" :aria-label="$t('table.sort')">
|
<button :class="['btn', 'btn-light border', readonly ? '' : 'handle']" type="button" :aria-label="$t('table.sort')" :disabled="readonly">
|
||||||
<Icon v="bars"/>
|
<Icon v="bars"/>
|
||||||
</button>
|
</button>
|
||||||
<slot v-bind:val="iVal[i]" v-bind:update="curry(update)(i)">
|
<slot v-bind:val="iVal[i]" v-bind:update="curry(update)(i)" v-bind:i="i">
|
||||||
<input v-model="iVal[i]" type="text" class="form-control" required/>
|
<input v-model="iVal[i]" type="text" class="form-control" required :readonly="readonly"/>
|
||||||
</slot>
|
</slot>
|
||||||
<button class="btn btn-outline-danger" type="button" @click.prevent="remove(i)" :aria-label="$t('crud.remove')">
|
<button :class="['btn', readonly ? 'btn-light border' : 'btn-outline-danger']" type="button" @click.prevent="remove(i)" :aria-label="$t('crud.remove')" :disabled="readonly">
|
||||||
<Icon v="times"/>
|
<Icon v="times"/>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<slot name="validation" v-bind:val="iVal[i]"></slot>
|
<slot name="validation" v-bind:val="iVal[i]" v-bind:i="i"></slot>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li slot="footer">
|
<li slot="footer">
|
||||||
<button class="btn btn-outline-success w-100 btn-sm" type="button" @click.prevent="add" :aria-label="$t('crud.add')">
|
<button v-if="!readonly && (maxlength === null || iVal.length < maxlength)"
|
||||||
|
class="btn btn-outline-success w-100 btn-sm" type="button"
|
||||||
|
@click.prevent="add" :aria-label="$t('crud.add')">
|
||||||
<Icon v="plus"/>
|
<Icon v="plus"/>
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
@ -37,6 +39,8 @@
|
|||||||
value: {},
|
value: {},
|
||||||
prototype: { 'default': '' },
|
prototype: { 'default': '' },
|
||||||
group: {},
|
group: {},
|
||||||
|
readonly: { type: Boolean },
|
||||||
|
maxlength: { 'default': null },
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
@ -1,52 +1,48 @@
|
|||||||
<template>
|
<template>
|
||||||
|
<span v-if="op" :class="[ op.style, `colour-${op.colour}`]">
|
||||||
|
<Tooltip :text="op.description">
|
||||||
|
<Icon :v="op.icon"/>
|
||||||
|
</Tooltip>
|
||||||
<Twemoji>
|
<Twemoji>
|
||||||
<span>
|
<nuxt-link v-if="link" :to="link" :class="`colour-${op.colour}`"><Spelling :escape="escape" :text="word"/></nuxt-link>
|
||||||
<strong v-if="opinion === 'yes'">
|
|
||||||
<Tooltip :text="$t('profile.opinion.yes')">
|
|
||||||
<Icon v="heart" set="s"/>
|
|
||||||
</Tooltip>
|
|
||||||
<nuxt-link v-if="link" :to="link"><Spelling :escape="escape" :text="word"/></nuxt-link>
|
|
||||||
<span v-else><Spelling :escape="escape" :text="word"/></span>
|
<span v-else><Spelling :escape="escape" :text="word"/></span>
|
||||||
</strong>
|
|
||||||
<span v-else-if="opinion === 'jokingly'">
|
|
||||||
<Tooltip :text="$t('profile.opinion.jokingly')">
|
|
||||||
<Icon v="grin-tongue"/>
|
|
||||||
</Tooltip>
|
|
||||||
<nuxt-link v-if="link" :to="link"><Spelling :escape="escape" :text="word"/></nuxt-link>
|
|
||||||
<span v-else><Spelling :escape="escape" :text="word"/></span>
|
|
||||||
</span>
|
|
||||||
<span v-else-if="opinion === 'close'">
|
|
||||||
<Tooltip :text="$t('profile.opinion.close')">
|
|
||||||
<Icon v="user-friends"/>
|
|
||||||
</Tooltip>
|
|
||||||
<nuxt-link v-if="link" :to="link"><Spelling :escape="escape" :text="word"/></nuxt-link>
|
|
||||||
<span v-else><Spelling :escape="escape" :text="word"/></span>
|
|
||||||
</span>
|
|
||||||
<span v-else-if="opinion === 'meh'">
|
|
||||||
<Tooltip :text="$t('profile.opinion.meh')">
|
|
||||||
<Icon v="thumbs-up"/>
|
|
||||||
</Tooltip>
|
|
||||||
<nuxt-link v-if="link" :to="link"><Spelling :escape="escape" :text="word"/></nuxt-link>
|
|
||||||
<span v-else><Spelling :escape="escape" :text="word"/></span>
|
|
||||||
</span>
|
|
||||||
<span v-else-if="opinion === 'no'" class="text-muted small">
|
|
||||||
<Tooltip :text="$t('profile.opinion.no')">
|
|
||||||
<Icon v="thumbs-down"/>
|
|
||||||
</Tooltip>
|
|
||||||
<nuxt-link v-if="link" :to="link"><Spelling :escape="escape" :text="word"/></nuxt-link>
|
|
||||||
<span v-else><Spelling :escape="escape" :text="word"/></span>
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</Twemoji>
|
</Twemoji>
|
||||||
|
</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import opinions from '../src/opinions';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
word: { required: true },
|
word: { required: true },
|
||||||
opinion: { required: true },
|
opinion: { required: true },
|
||||||
link: {},
|
link: {},
|
||||||
escape: { type: Boolean, 'default': () => true },
|
escape: { type: Boolean, 'default': () => true },
|
||||||
|
customOpinions: { 'default': () => { return {} }},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
op: this.findOpinion(),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
findOpinion() {
|
||||||
|
if (opinions.hasOwnProperty(this.opinion)) {
|
||||||
|
return {
|
||||||
|
...opinions[this.opinion],
|
||||||
|
description: this.$t(`profile.opinion.${this.opinion}`),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let op of Object.values(this.customOpinions)) {
|
||||||
|
if (op.icon === this.opinion) {
|
||||||
|
return op;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,29 +1,36 @@
|
|||||||
<template>
|
<template>
|
||||||
|
<div>
|
||||||
<ul class="list-inline small text-muted text-center mx-4">
|
<ul class="list-inline small text-muted text-center mx-4">
|
||||||
<li class="list-inline-item">
|
<li v-for="(opinion, key) in opinions" class="list-inline-item">
|
||||||
<Icon v="heart"/>
|
<Icon :v="opinion.icon"/>
|
||||||
=
|
=
|
||||||
<T>profile.opinion.yes</T>
|
<T>profile.opinion.{{key}}</T>
|
||||||
</li>
|
|
||||||
<li class="list-inline-item">
|
|
||||||
<Icon v="grin-tongue"/>
|
|
||||||
=
|
|
||||||
<T>profile.opinion.jokingly</T>
|
|
||||||
</li>
|
|
||||||
<li class="list-inline-item">
|
|
||||||
<Icon v="user-friends"/>
|
|
||||||
=
|
|
||||||
<T>profile.opinion.close</T>
|
|
||||||
</li>
|
|
||||||
<li class="list-inline-item">
|
|
||||||
<Icon v="thumbs-up"/>
|
|
||||||
=
|
|
||||||
<T>profile.opinion.meh</T>
|
|
||||||
</li>
|
|
||||||
<li class="list-inline-item">
|
|
||||||
<Icon v="thumbs-down"/>
|
|
||||||
=
|
|
||||||
<T>profile.opinion.no</T>
|
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
<ul v-if="Object.keys(custom).length > 0" class="list-inline small text-muted text-center mx-4">
|
||||||
|
<li class="list-inline-item">
|
||||||
|
<T>profile.opinions.custom</T>
|
||||||
|
</li>
|
||||||
|
<li v-for="(opinion, key) in custom" class="list-inline-item">
|
||||||
|
<Icon :v="opinion.icon"/>
|
||||||
|
=
|
||||||
|
{{opinion.description}}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import opinions from '../src/opinions';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
custom: { 'default': () => { return {} }},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
opinions: opinions,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
@ -1,62 +1,55 @@
|
|||||||
<template>
|
<template>
|
||||||
<ListInput v-model="v" :prototype="{value: '', opinion: 'meh'}" :group="group">
|
<ListInput v-model="v" :prototype="{value: '', opinion: 'meh'}" :group="group">
|
||||||
<template v-slot="s">
|
<template v-slot="s">
|
||||||
<button type="button" :class="['btn', s.val.opinion === 'yes' ? 'btn-primary' : 'btn-outline-secondary', 'btn-thin']"
|
<button type="button" :class="['btn', 'btn-outline-secondary', showOpinionSelector === s.i ? 'btn-secondary text-white border' : (validate(s.val) ? 'btn-outline-danger' : '')]"
|
||||||
:aria-label="$t('profile.opinion.yes')"
|
@click="showOpinionSelector = showOpinionSelector === s.i ? false : s.i">
|
||||||
@click="s.update({...s.val, value: s.val.value, opinion: 'yes'})">
|
<Icon :v="getIcon(s.val.opinion)"/>
|
||||||
<Tooltip :text="$t('profile.opinion.yes')">
|
|
||||||
<Icon v="heart"/>
|
|
||||||
</Tooltip>
|
|
||||||
</button>
|
</button>
|
||||||
<button type="button" :class="['btn', s.val.opinion === 'jokingly' ? 'btn-primary' : 'btn-outline-secondary', 'btn-thin']"
|
<input v-model="s.val.value" :class="['form-control', 'mw-input', validate(s.val) ? 'border-danger' : '']" @keyup="s.update(s.val)" required/>
|
||||||
:aria-label="$t('profile.opinion.jokingly')"
|
|
||||||
@click="s.update({...s.val, value: s.val.value, opinion: 'jokingly'})">
|
<div v-if="showOpinionSelector === s.i" class="bg-light border rounded hanging shadow shadow-lg">
|
||||||
<Tooltip :text="$t('profile.opinion.jokingly')">
|
<ul class="list-unstyled icons-list p-1 text-center mb-0">
|
||||||
<Icon v="grin-tongue"/>
|
<li v-for="(opinion, key) in opinions"
|
||||||
</Tooltip>
|
class="list-inline-item">
|
||||||
|
<button :class="['btn', key === s.val.opinion ? 'btn-dark' : 'btn-outline-dark', 'border-0 my-2']" @click.prevent="s.val.opinion = key; showOpinionSelector = false">
|
||||||
|
<Icon :v="opinion.icon"/>
|
||||||
</button>
|
</button>
|
||||||
<button type="button" :class="['btn', s.val.opinion === 'close' ? 'btn-primary' : 'btn-outline-secondary', 'btn-thin']"
|
</li>
|
||||||
:aria-label="$t('profile.opinion.close')"
|
</ul>
|
||||||
@click="s.update({...s.val, value: s.val.value, opinion: 'close'})">
|
<ul v-if="customOpinions.length" class="list-unstyled icons-list p-1 text-center mb-0">
|
||||||
<Tooltip :text="$t('profile.opinion.close')">
|
<li v-for="opinion in customOpinions"
|
||||||
<Icon v="user-friends"/>
|
class="list-inline-item">
|
||||||
</Tooltip>
|
<button :class="['btn', opinion.icon === s.val.opinion ? 'btn-dark' : 'btn-outline-dark', 'border-0 my-2']" @click.prevent="s.val.opinion = opinion.icon; showOpinionSelector = false">
|
||||||
|
<Icon :v="opinion.icon"/>
|
||||||
</button>
|
</button>
|
||||||
<button type="button" :class="['btn', s.val.opinion === 'meh' ? 'btn-primary' : 'btn-outline-secondary', 'btn-thin']"
|
</li>
|
||||||
:aria-label="$t('profile.opinion.meh')"
|
</ul>
|
||||||
@click="s.update({...s.val, value: s.val.value, opinion: 'meh'})">
|
</div>
|
||||||
<Tooltip :text="$t('profile.opinion.meh')">
|
|
||||||
<Icon v="thumbs-up"/>
|
|
||||||
</Tooltip>
|
|
||||||
</button>
|
|
||||||
<button type="button" :class="['btn', s.val.opinion === 'no' ? 'btn-primary' : 'btn-outline-secondary', 'btn-thin']"
|
|
||||||
:aria-label="$t('profile.opinion.no')"
|
|
||||||
@click="s.update({...s.val, value: s.val.value, opinion: 'no'})">
|
|
||||||
<Tooltip :text="$t('profile.opinion.no')">
|
|
||||||
<Icon v="thumbs-down"/>
|
|
||||||
</Tooltip>
|
|
||||||
</button>
|
|
||||||
<input v-model="s.val.value" :class="['form-control', 'mw-input', invalid(s.val) ? 'border-danger' : '']" @keyup="s.update(s.val)" required/>
|
|
||||||
</template>
|
</template>
|
||||||
<template v-slot:validation="s">
|
<template v-slot:validation="s">
|
||||||
<p v-if="invalid(s.val)" class="small text-danger">
|
<p v-if="validate(s.val)" class="small text-danger">
|
||||||
<Icon v="exclamation-triangle"/>
|
<Icon v="exclamation-triangle"/>
|
||||||
<span class="ml-1">{{$t(validation(s.val.value))}}</span>
|
<span class="ml-1">{{$t(validate(s.val))}}</span>
|
||||||
</p>
|
</p>
|
||||||
</template>
|
</template>
|
||||||
</ListInput>
|
</ListInput>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import opinions from '../src/opinions';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
value: {},
|
value: {},
|
||||||
group: {},
|
group: {},
|
||||||
validation: {},
|
validation: {},
|
||||||
|
customOpinions: { 'default': () => { return [] }},
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
v: this.value,
|
v: this.value,
|
||||||
|
showOpinionSelector: false,
|
||||||
|
opinions,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
@ -64,20 +57,40 @@
|
|||||||
value(v) { this.v = v; }
|
value(v) { this.v = v; }
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
invalid(val) {
|
validate(val) {
|
||||||
return this.validation && val.value && this.validation(val.value);
|
if (!this.getIcon(val.opinion)) {
|
||||||
|
return 'profile.opinions.validation.invalidOpinion';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!val.value) { return null; }
|
||||||
|
|
||||||
|
return this.validation && this.validation(val.value);
|
||||||
|
},
|
||||||
|
getIcon(opinion) {
|
||||||
|
if (opinions.hasOwnProperty(opinion)) {
|
||||||
|
return opinions[opinion].icon;
|
||||||
|
}
|
||||||
|
for (let op of this.customOpinions) {
|
||||||
|
if (op.icon === opinion) {
|
||||||
|
return opinion;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss" scoped>
|
||||||
@import "assets/variables";
|
@import "assets/variables";
|
||||||
|
|
||||||
@include media-breakpoint-down('sm', $grid-breakpoints) {
|
.hanging {
|
||||||
.btn-thin {
|
position: absolute;
|
||||||
padding-left: map-get($spacers, 1) !important;
|
top: 100%;
|
||||||
padding-right: map-get($spacers, 1) !important;
|
left: 0;
|
||||||
}
|
width: 100%;
|
||||||
|
max-width: 300px;
|
||||||
|
z-index: 5000;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -64,7 +64,7 @@
|
|||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<ul class="list-unstyled">
|
<ul class="list-unstyled">
|
||||||
<li v-for="{value: name, opinion} in profile.names"><Opinion :word="convertName(name)" :opinion="opinion" :escape="false"/></li>
|
<li v-for="{value: name, opinion} in profile.names"><Opinion :word="convertName(name)" :opinion="opinion" :escape="false" :customOpinions="profile.opinions"/></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="profile.pronouns.length" :class="['col-6', mainRowCount === 3 ? 'col-lg-4' : 'col-lg-6']">
|
<div v-if="profile.pronouns.length" :class="['col-6', mainRowCount === 3 ? 'col-lg-4' : 'col-lg-6']">
|
||||||
@ -75,7 +75,7 @@
|
|||||||
|
|
||||||
<ul class="list-unstyled">
|
<ul class="list-unstyled">
|
||||||
<li v-for="{link, pronoun, opinion} in pronounOpinions">
|
<li v-for="{link, pronoun, opinion} in pronounOpinions">
|
||||||
<Opinion :word="typeof pronoun === 'string' ? pronoun : pronoun.name(glue)" :opinion="opinion" :link="`/${link}`"/>
|
<Opinion :word="typeof pronoun === 'string' ? pronoun : pronoun.name(glue)" :opinion="opinion" :link="`/${link}`" :customOpinions="profile.opinions"/>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@ -103,14 +103,14 @@
|
|||||||
<div v-for="column in profile.words" v-if="column.values.length" class="col-6 col-lg-3">
|
<div v-for="column in profile.words" v-if="column.values.length" class="col-6 col-lg-3">
|
||||||
<h4 v-if="column.header" class="h6">{{ column.header }}</h4>
|
<h4 v-if="column.header" class="h6">{{ column.header }}</h4>
|
||||||
<ul class="list-unstyled">
|
<ul class="list-unstyled">
|
||||||
<li v-for="{value: word, opinion} in column.values"><Opinion :word="word" :opinion="opinion"/></li>
|
<li v-for="{value: word, opinion} in column.values"><Opinion :word="word" :opinion="opinion" :customOpinions="profile.opinions"/></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<OpinionLegend/>
|
<OpinionLegend :custom="profile.opinions"/>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
27
components/ReducedColoursSwitch.vue
Normal file
27
components/ReducedColoursSwitch.vue
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
<template>
|
||||||
|
<label class="form-check form-switch d-inline-block">
|
||||||
|
<input class="form-check-input" type="checkbox" role="switch" v-model="reducedColours">
|
||||||
|
<T>mode.reducedColours</T>
|
||||||
|
</label>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
reducedColours: false,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
if (!process.client) { return; }
|
||||||
|
|
||||||
|
this.reducedColours = localStorage.getItem('reducedColours') === 'true';
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
reducedColours(v) {
|
||||||
|
document.body.classList.toggle('reduced-colours', v);
|
||||||
|
localStorage.setItem('reducedColours', v);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
@ -24,6 +24,7 @@
|
|||||||
top: -2.2rem;
|
top: -2.2rem;
|
||||||
left: -50%;
|
left: -50%;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
|
font-style: normal;
|
||||||
font-size: .85rem;
|
font-size: .85rem;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
@ -609,6 +609,29 @@ profile:
|
|||||||
or (under construction) by a <code>rel="me"</code> tag pointing back to the card.
|
or (under construction) by a <code>rel="me"</code> tag pointing back to the card.
|
||||||
Our links also include a <code>rel="me"</code> tag, so that external websites can verify your card the other way round too.
|
Our links also include a <code>rel="me"</code> tag, so that external websites can verify your card the other way round too.
|
||||||
column: 'Column'
|
column: 'Column'
|
||||||
|
opinions:
|
||||||
|
header: 'Legend/opinions'
|
||||||
|
description: 'Description…'
|
||||||
|
colours:
|
||||||
|
_: '(Font colour…)'
|
||||||
|
pink: 'Pink'
|
||||||
|
red: 'Red'
|
||||||
|
orange: 'Orange'
|
||||||
|
green: 'Green'
|
||||||
|
blue: 'Blue'
|
||||||
|
grey: 'Grey'
|
||||||
|
styles:
|
||||||
|
_: '(Style…)'
|
||||||
|
bold: 'Bold'
|
||||||
|
italics: 'Italics'
|
||||||
|
small: 'Small'
|
||||||
|
validation:
|
||||||
|
missingIcon: 'Icon is required'
|
||||||
|
missingDescription: 'Description is required'
|
||||||
|
duplicateIcon: 'Icon must be unique'
|
||||||
|
duplicateDescription: 'Description must be unique'
|
||||||
|
invalidOpinion: 'Selected icon was not found in the legend above'
|
||||||
|
custom: 'custom, added by the user:'
|
||||||
|
|
||||||
header: 'Cards'
|
header: 'Cards'
|
||||||
list: 'Your cards'
|
list: 'Your cards'
|
||||||
@ -660,6 +683,7 @@ crud:
|
|||||||
loginRequired: '{/account=Log in} to submit an entry'
|
loginRequired: '{/account=Log in} to submit an entry'
|
||||||
copy: 'Copy link'
|
copy: 'Copy link'
|
||||||
download: 'Download'
|
download: 'Download'
|
||||||
|
loadAll: 'Load all…'
|
||||||
|
|
||||||
footer:
|
footer:
|
||||||
license: >
|
license: >
|
||||||
@ -895,6 +919,7 @@ mode:
|
|||||||
light: 'Light mode'
|
light: 'Light mode'
|
||||||
automatic: 'Automatic'
|
automatic: 'Automatic'
|
||||||
dark: 'Dark mode'
|
dark: 'Dark mode'
|
||||||
|
reducedColours: 'Reduced colours'
|
||||||
|
|
||||||
ban:
|
ban:
|
||||||
reason: 'Ban reason'
|
reason: 'Ban reason'
|
||||||
|
@ -702,6 +702,29 @@ profile:
|
|||||||
or (under construction) by a <code>rel="me"</code> tag pointing back to the card.
|
or (under construction) by a <code>rel="me"</code> tag pointing back to the card.
|
||||||
Our links also include a <code>rel="me"</code> tag, so that external websites can verify your card the other way round too.
|
Our links also include a <code>rel="me"</code> tag, so that external websites can verify your card the other way round too.
|
||||||
column: 'Column'
|
column: 'Column'
|
||||||
|
opinions:
|
||||||
|
header: 'Legend/opinions'
|
||||||
|
description: 'Description…'
|
||||||
|
colours:
|
||||||
|
_: '(Font colour…)'
|
||||||
|
pink: 'Pink'
|
||||||
|
red: 'Red'
|
||||||
|
orange: 'Orange'
|
||||||
|
green: 'Green'
|
||||||
|
blue: 'Blue'
|
||||||
|
grey: 'Grey'
|
||||||
|
styles:
|
||||||
|
_: '(Style…)'
|
||||||
|
bold: 'Bold'
|
||||||
|
italics: 'Italics'
|
||||||
|
small: 'Small'
|
||||||
|
validation:
|
||||||
|
missingIcon: 'Icon is required'
|
||||||
|
missingDescription: 'Description is required'
|
||||||
|
duplicateIcon: 'Icon must be unique'
|
||||||
|
duplicateDescription: 'Description must be unique'
|
||||||
|
invalidOpinion: 'Selected icon was not found in the legend above'
|
||||||
|
custom: 'custom, added by the user:'
|
||||||
|
|
||||||
header: 'Cards'
|
header: 'Cards'
|
||||||
list: 'Your cards'
|
list: 'Your cards'
|
||||||
@ -756,6 +779,7 @@ crud:
|
|||||||
loginRequired: '{/account=Log in} to submit an entry'
|
loginRequired: '{/account=Log in} to submit an entry'
|
||||||
copy: 'Copy link'
|
copy: 'Copy link'
|
||||||
download: 'Download'
|
download: 'Download'
|
||||||
|
loadAll: 'Load all…'
|
||||||
|
|
||||||
footer:
|
footer:
|
||||||
license: >
|
license: >
|
||||||
@ -1007,6 +1031,7 @@ mode:
|
|||||||
light: 'Light mode'
|
light: 'Light mode'
|
||||||
automatic: 'Automatic'
|
automatic: 'Automatic'
|
||||||
dark: 'Dark mode'
|
dark: 'Dark mode'
|
||||||
|
reducedColours: 'Reduced colours'
|
||||||
|
|
||||||
ban:
|
ban:
|
||||||
reason: 'Ban reason'
|
reason: 'Ban reason'
|
||||||
|
@ -52,4 +52,25 @@ export default [
|
|||||||
'user.qr.download',
|
'user.qr.download',
|
||||||
'footer.stats.month',
|
'footer.stats.month',
|
||||||
'profile.wordsColumnHeader',
|
'profile.wordsColumnHeader',
|
||||||
|
'profile.opinions.header',
|
||||||
|
'profile.opinions.description',
|
||||||
|
'profile.opinions.colours._',
|
||||||
|
'profile.opinions.colours.pink',
|
||||||
|
'profile.opinions.colours.red',
|
||||||
|
'profile.opinions.colours.orange',
|
||||||
|
'profile.opinions.colours.green',
|
||||||
|
'profile.opinions.colours.blue',
|
||||||
|
'profile.opinions.colours.grey',
|
||||||
|
'profile.opinions.styles._',
|
||||||
|
'profile.opinions.styles.bold',
|
||||||
|
'profile.opinions.styles.italics',
|
||||||
|
'profile.opinions.styles.small',
|
||||||
|
'profile.opinions.validation.missingIcon',
|
||||||
|
'profile.opinions.validation.missingDescription',
|
||||||
|
'profile.opinions.validation.duplicateIcon',
|
||||||
|
'profile.opinions.validation.duplicateDescription',
|
||||||
|
'profile.opinions.validation.invalidOpinion',
|
||||||
|
'profile.opinions.custom',
|
||||||
|
'mode.reducedColours',
|
||||||
|
'crud.loadAll',
|
||||||
];
|
];
|
||||||
|
@ -1343,6 +1343,29 @@ profile:
|
|||||||
albo (ficzer w budowie) poprzez umieszczenie tagu <code>rel="me"</code> wskazującego z powrotem na wizytówkę.
|
albo (ficzer w budowie) poprzez umieszczenie tagu <code>rel="me"</code> wskazującego z powrotem na wizytówkę.
|
||||||
Nasze linki również umieszczają <code>rel="me"</code>, aby zewnętrzne strony mogły potwierdzić wizytówkę również w odwrotną stronę.
|
Nasze linki również umieszczają <code>rel="me"</code>, aby zewnętrzne strony mogły potwierdzić wizytówkę również w odwrotną stronę.
|
||||||
column: 'Kolumna'
|
column: 'Kolumna'
|
||||||
|
opinions:
|
||||||
|
header: 'Legenda/opinie'
|
||||||
|
description: 'Opis…'
|
||||||
|
colours:
|
||||||
|
_: '(Kolor fontu…)'
|
||||||
|
pink: 'Różowy'
|
||||||
|
red: 'Czerwony'
|
||||||
|
orange: 'Pomarańczowy'
|
||||||
|
green: 'Zielony'
|
||||||
|
blue: 'Niebieski'
|
||||||
|
grey: 'Szary'
|
||||||
|
styles:
|
||||||
|
_: '(Styl tekstu…)'
|
||||||
|
bold: 'Pogrubiony'
|
||||||
|
italics: 'Kursywa'
|
||||||
|
small: 'Mały'
|
||||||
|
validation:
|
||||||
|
missingIcon: 'Ikona jest wymagana'
|
||||||
|
missingDescription: 'Opis jest wymagany'
|
||||||
|
duplicateIcon: 'Ikony muszą być unikalne'
|
||||||
|
duplicateDescription: 'Opishy muszą być unikalne'
|
||||||
|
invalidOpinion: 'Wybrana ikona nie jest dostępna w legendzie powyżej'
|
||||||
|
custom: 'dodane ręcznie:'
|
||||||
|
|
||||||
header: 'Wizytówki'
|
header: 'Wizytówki'
|
||||||
list: 'Twoje wizytówki'
|
list: 'Twoje wizytówki'
|
||||||
@ -1463,6 +1486,7 @@ crud:
|
|||||||
loginRequired: '{/konto=Zaloguj się}, aby zgłosić wpis'
|
loginRequired: '{/konto=Zaloguj się}, aby zgłosić wpis'
|
||||||
copy: 'Skopiuj link'
|
copy: 'Skopiuj link'
|
||||||
download: 'Ściągnij'
|
download: 'Ściągnij'
|
||||||
|
loadAll: 'Załaduj wszystko…'
|
||||||
|
|
||||||
footer:
|
footer:
|
||||||
license: >
|
license: >
|
||||||
@ -1679,6 +1703,7 @@ mode:
|
|||||||
light: 'Tryb dzienny'
|
light: 'Tryb dzienny'
|
||||||
automatic: 'Auto'
|
automatic: 'Auto'
|
||||||
dark: 'Tryb nocny'
|
dark: 'Tryb nocny'
|
||||||
|
reducedColours: 'Zredukowane kolory'
|
||||||
|
|
||||||
ban:
|
ban:
|
||||||
reason: 'Powód blokady'
|
reason: 'Powód blokady'
|
||||||
|
6
migrations/061-profile-opinions.sql
Normal file
6
migrations/061-profile-opinions.sql
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
-- Up
|
||||||
|
|
||||||
|
ALTER TABLE profiles ADD COLUMN opinions TEXT NOT NULL DEFAULT '{}';
|
||||||
|
|
||||||
|
-- Down
|
||||||
|
|
@ -202,10 +202,15 @@ export default {
|
|||||||
config.module.rules.push({
|
config.module.rules.push({
|
||||||
test: /\.md$/,
|
test: /\.md$/,
|
||||||
use: ['html-loader', 'markdown-loader']
|
use: ['html-loader', 'markdown-loader']
|
||||||
|
});
|
||||||
|
config.module.rules.push({
|
||||||
|
test: /\.ya?ml$/,
|
||||||
|
use: 'yaml-loader',
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
env: {
|
env: {
|
||||||
|
ENV: process.env.ENV,
|
||||||
BASE_URL: process.env.BASE_URL,
|
BASE_URL: process.env.BASE_URL,
|
||||||
HOME_URL: process.env.HOME_URL || 'https://pronouns.page',
|
HOME_URL: process.env.HOME_URL || 'https://pronouns.page',
|
||||||
TITLE: title,
|
TITLE: title,
|
||||||
|
@ -65,6 +65,7 @@
|
|||||||
"vuedraggable": "^2.24.3",
|
"vuedraggable": "^2.24.3",
|
||||||
"vuejs-datepicker": "^1.6.2",
|
"vuejs-datepicker": "^1.6.2",
|
||||||
"webpack": "^5.0",
|
"webpack": "^5.0",
|
||||||
|
"yaml-loader": "^0.8.0",
|
||||||
"zh_cn_zh_tw": "^1.0.7"
|
"zh_cn_zh_tw": "^1.0.7"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
@ -135,6 +135,9 @@
|
|||||||
|
|
||||||
<Separator icon="heart"/>
|
<Separator icon="heart"/>
|
||||||
<Support/>
|
<Support/>
|
||||||
|
<div class="text-center my-4 small">
|
||||||
|
<ReducedColoursSwitch/>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</Page>
|
</Page>
|
||||||
<Page v-else-if="user.username">
|
<Page v-else-if="user.username">
|
||||||
|
@ -92,7 +92,12 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<OpinionLegend/>
|
<h3 class="h4">
|
||||||
|
<Icon v="comment-smile"/>
|
||||||
|
<T>profile.opinions.header</T>
|
||||||
|
</h3>
|
||||||
|
<LegendOpinionListInput v-model="defaultOpinions" readonly class="mb-0"/>
|
||||||
|
<LegendOpinionListInput v-model="opinions" :maxlength="5"/>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="form-group">
|
<section class="form-group">
|
||||||
@ -103,7 +108,7 @@
|
|||||||
<p v-if="$te('profile.namesInfo')" class="small text-muted">
|
<p v-if="$te('profile.namesInfo')" class="small text-muted">
|
||||||
<T>profile.namesInfo</T>
|
<T>profile.namesInfo</T>
|
||||||
</p>
|
</p>
|
||||||
<OpinionListInput v-model="names"/>
|
<OpinionListInput v-model="names" :customOpinions="opinions"/>
|
||||||
<PropagateCheckbox field="names" :before="beforeChanges.names" :after="names" v-if="otherProfiles > 0" @change="propagateChanged"/>
|
<PropagateCheckbox field="names" :before="beforeChanges.names" :after="names" v-if="otherProfiles > 0" @change="propagateChanged"/>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@ -120,7 +125,7 @@
|
|||||||
<T>profile.pronounsInfo</T>
|
<T>profile.pronounsInfo</T>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<OpinionListInput v-model="pronouns" :validation="validatePronoun"/>
|
<OpinionListInput v-model="pronouns" :validation="validatePronoun" :customOpinions="opinions"/>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<AdPlaceholder phkey="main-1"/>
|
<AdPlaceholder phkey="main-1"/>
|
||||||
@ -229,7 +234,7 @@
|
|||||||
<T>profile.column</T> {{i + 1}}
|
<T>profile.column</T> {{i + 1}}
|
||||||
</h4>
|
</h4>
|
||||||
<input v-model="words[i].header" class="form-control form-control-sm mb-2" :placeholder="$t('profile.wordsColumnHeader')" maxlength="36"/>
|
<input v-model="words[i].header" class="form-control form-control-sm mb-2" :placeholder="$t('profile.wordsColumnHeader')" maxlength="36"/>
|
||||||
<OpinionListInput v-model="words[i].values" group="words"/>
|
<OpinionListInput v-model="words[i].values" group="words" :customOpinions="opinions"/>
|
||||||
</template>
|
</template>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@ -254,6 +259,7 @@
|
|||||||
import link from '../plugins/link';
|
import link from '../plugins/link';
|
||||||
import {minBirthdate, maxBirthdate, formatDate} from '../src/birthdate';
|
import {minBirthdate, maxBirthdate, formatDate} from '../src/birthdate';
|
||||||
import opinions from '../src/opinions';
|
import opinions from '../src/opinions';
|
||||||
|
import t from '../src/translator';``
|
||||||
|
|
||||||
const defaultWords = config.profile.defaultWords.map(({header, values}) => {
|
const defaultWords = config.profile.defaultWords.map(({header, values}) => {
|
||||||
return {
|
return {
|
||||||
@ -276,6 +282,18 @@
|
|||||||
return Array.isArray(arrayObject) ? arrayObject : Object.values(arrayObject);
|
return Array.isArray(arrayObject) ? arrayObject : Object.values(arrayObject);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const opinionsToForm = (opinions) => buildList(function*() {
|
||||||
|
for (let [key, options] of Object.entries(opinions)) {
|
||||||
|
yield {
|
||||||
|
key,
|
||||||
|
icon: options.icon,
|
||||||
|
description: options.description || t.get(`profile.opinion.${key}`),
|
||||||
|
colour: options.colour || '',
|
||||||
|
style: options.style || '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const buildProfile = (profiles, currentLocale) => {
|
const buildProfile = (profiles, currentLocale) => {
|
||||||
for (let locale in profiles) {
|
for (let locale in profiles) {
|
||||||
if (!profiles.hasOwnProperty(locale)) {
|
if (!profiles.hasOwnProperty(locale)) {
|
||||||
@ -298,6 +316,7 @@
|
|||||||
credentials: profile.credentials,
|
credentials: profile.credentials,
|
||||||
credentialsLevel: profile.credentialsLevel,
|
credentialsLevel: profile.credentialsLevel,
|
||||||
credentialsName: profile.credentialsName,
|
credentialsName: profile.credentialsName,
|
||||||
|
opinions: opinionsToForm(profile.opinions || {}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -322,6 +341,7 @@
|
|||||||
credentials: [],
|
credentials: [],
|
||||||
credentialsLevel: null,
|
credentialsLevel: null,
|
||||||
credentialsName: null,
|
credentialsName: null,
|
||||||
|
opinions: opinionsToForm(profile.opinions || {}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -340,6 +360,7 @@
|
|||||||
credentials: [],
|
credentials: [],
|
||||||
credentialsLevel: null,
|
credentialsLevel: null,
|
||||||
credentialsName: null,
|
credentialsName: null,
|
||||||
|
opinions: [],
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -354,6 +375,7 @@
|
|||||||
},
|
},
|
||||||
propagate: [],
|
propagate: [],
|
||||||
flagsAsterisk: process.env.FLAGS_ASTERISK,
|
flagsAsterisk: process.env.FLAGS_ASTERISK,
|
||||||
|
defaultOpinions: opinionsToForm(opinions),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
async asyncData({ app, store }) {
|
async asyncData({ app, store }) {
|
||||||
@ -384,6 +406,7 @@
|
|||||||
this.saving = true;
|
this.saving = true;
|
||||||
try {
|
try {
|
||||||
await this.$post(`/profile/save`, {
|
await this.$post(`/profile/save`, {
|
||||||
|
opinions: this.opinions,
|
||||||
names: this.names,
|
names: this.names,
|
||||||
pronouns: this.pronouns,
|
pronouns: this.pronouns,
|
||||||
description: this.description,
|
description: this.description,
|
||||||
|
@ -9,6 +9,7 @@ import fs from 'fs';
|
|||||||
import { minBirthdate, maxBirthdate, formatDate, parseDate } from '../../src/birthdate';
|
import { minBirthdate, maxBirthdate, formatDate, parseDate } from '../../src/birthdate';
|
||||||
import {socialProviders} from "../../src/socialProviders";
|
import {socialProviders} from "../../src/socialProviders";
|
||||||
import {downgradeToV1, upgradeToV2} from "../profileV2";
|
import {downgradeToV1, upgradeToV2} from "../profileV2";
|
||||||
|
import { colours, styles } from '../../src/styling';
|
||||||
|
|
||||||
const normalise = s => s.trim().toLowerCase();
|
const normalise = s => s.trim().toLowerCase();
|
||||||
|
|
||||||
@ -66,6 +67,7 @@ const fetchProfiles = async (db, username, self) => {
|
|||||||
for (let profile of profiles) {
|
for (let profile of profiles) {
|
||||||
const links = JSON.parse(profile.links);
|
const links = JSON.parse(profile.links);
|
||||||
p[profile.locale] = {
|
p[profile.locale] = {
|
||||||
|
opinions: JSON.parse(profile.opinions),
|
||||||
names: JSON.parse(profile.names),
|
names: JSON.parse(profile.names),
|
||||||
pronouns: JSON.parse(profile.pronouns),
|
pronouns: JSON.parse(profile.pronouns),
|
||||||
description: profile.description,
|
description: profile.description,
|
||||||
@ -102,6 +104,7 @@ function* isSuspicious(profile) {
|
|||||||
JSON.stringify(profile.pronouns),
|
JSON.stringify(profile.pronouns),
|
||||||
JSON.stringify(profile.names),
|
JSON.stringify(profile.names),
|
||||||
JSON.stringify(profile.words),
|
JSON.stringify(profile.words),
|
||||||
|
JSON.stringify(profile.opinions),
|
||||||
]) {
|
]) {
|
||||||
s = s.toLowerCase().replace(/\s+/g, ' ');
|
s = s.toLowerCase().replace(/\s+/g, ' ');
|
||||||
for (let sus of susRegexes) {
|
for (let sus of susRegexes) {
|
||||||
@ -173,6 +176,22 @@ const sanitiseBirthday = (bd) => {
|
|||||||
return formatDate(bd);
|
return formatDate(bd);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const cleanupOpinions = (opinions) => {
|
||||||
|
const cleanOpinions = {}
|
||||||
|
let i = 0;
|
||||||
|
for (let opinion of opinions) {
|
||||||
|
if (!opinion.icon || !opinion.description || i >= 5) { continue; }
|
||||||
|
cleanOpinions[opinion.icon] = {
|
||||||
|
icon: opinion.icon,
|
||||||
|
description: opinion.description.substring(0, 24),
|
||||||
|
colour: opinion.colour && colours.includes(opinion.colour) ? opinion.colour : undefined,
|
||||||
|
style: opinion.style && styles.includes(opinion.style) ? opinion.style : undefined,
|
||||||
|
}
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
return cleanOpinions;
|
||||||
|
}
|
||||||
|
|
||||||
router.post('/profile/save', handleErrorAsync(async (req, res) => {
|
router.post('/profile/save', handleErrorAsync(async (req, res) => {
|
||||||
if (!req.user) {
|
if (!req.user) {
|
||||||
return res.status(401).json({error: 'Unauthorised'});
|
return res.status(401).json({error: 'Unauthorised'});
|
||||||
@ -188,11 +207,14 @@ router.post('/profile/save', handleErrorAsync(async (req, res) => {
|
|||||||
req.body.customFlags = Object.values(req.body.customFlags);
|
req.body.customFlags = Object.values(req.body.customFlags);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const opinions = cleanupOpinions(req.body.opinions);
|
||||||
|
|
||||||
// TODO just make it a transaction...
|
// TODO just make it a transaction...
|
||||||
const ids = (await req.db.all(SQL`SELECT * FROM profiles WHERE userId = ${req.user.id} AND locale = ${global.config.locale}`)).map(row => row.id);
|
const ids = (await req.db.all(SQL`SELECT * FROM profiles WHERE userId = ${req.user.id} AND locale = ${global.config.locale}`)).map(row => row.id);
|
||||||
if (ids.length) {
|
if (ids.length) {
|
||||||
await req.db.get(SQL`UPDATE profiles
|
await req.db.get(SQL`UPDATE profiles
|
||||||
SET
|
SET
|
||||||
|
opinions = ${JSON.stringify(opinions)},
|
||||||
names = ${JSON.stringify(req.body.names)},
|
names = ${JSON.stringify(req.body.names)},
|
||||||
pronouns = ${JSON.stringify(req.body.pronouns)},
|
pronouns = ${JSON.stringify(req.body.pronouns)},
|
||||||
description = ${req.body.description},
|
description = ${req.body.description},
|
||||||
@ -212,8 +234,8 @@ router.post('/profile/save', handleErrorAsync(async (req, res) => {
|
|||||||
WHERE id = ${ids[0]}
|
WHERE id = ${ids[0]}
|
||||||
`);
|
`);
|
||||||
} else {
|
} else {
|
||||||
await req.db.get(SQL`INSERT INTO profiles (id, userId, locale, names, pronouns, description, birthday, links, flags, customFlags, words, active, teamName, footerName, footerAreas)
|
await req.db.get(SQL`INSERT INTO profiles (id, userId, locale, opinions, names, pronouns, description, birthday, links, flags, customFlags, words, active, teamName, footerName, footerAreas)
|
||||||
VALUES (${ulid()}, ${req.user.id}, ${global.config.locale}, ${JSON.stringify(req.body.names)}, ${JSON.stringify(req.body.pronouns)},
|
VALUES (${ulid()}, ${req.user.id}, ${global.config.locale}, ${JSON.stringify(opinions)}, ${JSON.stringify(req.body.names)}, ${JSON.stringify(req.body.pronouns)},
|
||||||
${req.body.description}, ${sanitiseBirthday(req.body.birthday || null)}, ${JSON.stringify(req.body.links.filter(x => !!x))}, ${JSON.stringify(req.body.flags)}, ${JSON.stringify(req.body.customFlags)},
|
${req.body.description}, ${sanitiseBirthday(req.body.birthday || null)}, ${JSON.stringify(req.body.links.filter(x => !!x))}, ${JSON.stringify(req.body.flags)}, ${JSON.stringify(req.body.customFlags)},
|
||||||
${JSON.stringify(req.body.words)}, 1,
|
${JSON.stringify(req.body.words)}, 1,
|
||||||
${req.isGranted() ? req.body.teamName || null : ''},
|
${req.isGranted() ? req.body.teamName || null : ''},
|
||||||
|
15
src/icons.js
Normal file
15
src/icons.js
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import iconsMetadata from '@fortawesome/fontawesome-pro/metadata/icons.yml';
|
||||||
|
|
||||||
|
const icons = [];
|
||||||
|
for (let [iconName, iconMetadata] of Object.entries(iconsMetadata)) {
|
||||||
|
icons.push({
|
||||||
|
name: iconName,
|
||||||
|
styles: iconMetadata.styles,
|
||||||
|
searchTerms: [
|
||||||
|
...iconMetadata.search.terms.map(t => (t + '').toLowerCase()),
|
||||||
|
iconName.toLowerCase(),
|
||||||
|
iconMetadata.label.toLowerCase(),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
export default icons;
|
@ -1,7 +1,7 @@
|
|||||||
export default {
|
export default {
|
||||||
yes: { value: 3, bold: true },
|
yes: { value: 3, icon: 's:heart', emoji: '❤️', colour: 'pink', style: 'bold' },
|
||||||
jokingly: { value: 1 },
|
jokingly: { value: 1, icon: 'grin-tongue', emoji: '😜', colour: 'orange' },
|
||||||
close: { value: 1 },
|
close: { value: 1, icon: 'user-friends', emoji: '🫂', colour: 'red' },
|
||||||
meh: { value: 0 },
|
meh: { value: 0, icon: 'thumbs-up', emoji: '👍' },
|
||||||
no: { value: -3, small: true, color: 'muted' },
|
no: { value: -3, icon: 'thumbs-down', emoji: '👎', colour: 'grey', style: 'small' },
|
||||||
};
|
};
|
||||||
|
2
src/styling.js
Normal file
2
src/styling.js
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export const colours = ['', 'pink', 'red', 'orange', 'green', 'blue', 'grey'];
|
||||||
|
export const styles = ['', 'bold', 'italics', 'small'];
|
19
yarn.lock
19
yarn.lock
@ -5714,6 +5714,11 @@ isstream@~0.1.2:
|
|||||||
resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a"
|
resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a"
|
||||||
integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=
|
integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=
|
||||||
|
|
||||||
|
javascript-stringify@^2.0.1:
|
||||||
|
version "2.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/javascript-stringify/-/javascript-stringify-2.1.0.tgz#27c76539be14d8bd128219a2d731b09337904e79"
|
||||||
|
integrity sha512-JVAfqNPTvNq3sB/VHQJAFxN/sPgKnsKrCwyRt15zwNCdrMMJDdcEOdubuy+DuJYYdm0ox1J4uzEuYKkN+9yhVg==
|
||||||
|
|
||||||
jest-worker@^26.5.0, jest-worker@^26.6.2:
|
jest-worker@^26.5.0, jest-worker@^26.6.2:
|
||||||
version "26.6.2"
|
version "26.6.2"
|
||||||
resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-26.6.2.tgz#7f72cbc4d643c365e27b9fd775f9d0eaa9c7a8ed"
|
resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-26.6.2.tgz#7f72cbc4d643c365e27b9fd775f9d0eaa9c7a8ed"
|
||||||
@ -10870,11 +10875,25 @@ yallist@^4.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
|
resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
|
||||||
integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==
|
integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==
|
||||||
|
|
||||||
|
yaml-loader@^0.8.0:
|
||||||
|
version "0.8.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/yaml-loader/-/yaml-loader-0.8.0.tgz#c839325e3fdee082b3768b2a21fe34fde5d96f61"
|
||||||
|
integrity sha512-LjeKnTzVBKWiQBeE2L9ssl6WprqaUIxCSNs5tle8PaDydgu3wVFXTbMfsvF2MSErpy9TDVa092n4q6adYwJaWg==
|
||||||
|
dependencies:
|
||||||
|
javascript-stringify "^2.0.1"
|
||||||
|
loader-utils "^2.0.0"
|
||||||
|
yaml "^2.0.0"
|
||||||
|
|
||||||
yaml@^1.10.0:
|
yaml@^1.10.0:
|
||||||
version "1.10.0"
|
version "1.10.0"
|
||||||
resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.0.tgz#3b593add944876077d4d683fee01081bd9fff31e"
|
resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.0.tgz#3b593add944876077d4d683fee01081bd9fff31e"
|
||||||
integrity sha512-yr2icI4glYaNG+KWONODapy2/jDdMSDnrONSjblABjD9B4Z5LgiircSt8m8sRZFNi08kG9Sm0uSHtEmP3zaEGg==
|
integrity sha512-yr2icI4glYaNG+KWONODapy2/jDdMSDnrONSjblABjD9B4Z5LgiircSt8m8sRZFNi08kG9Sm0uSHtEmP3zaEGg==
|
||||||
|
|
||||||
|
yaml@^2.0.0:
|
||||||
|
version "2.1.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.1.3.tgz#9b3a4c8aff9821b696275c79a8bee8399d945207"
|
||||||
|
integrity sha512-AacA8nRULjKMX2DvWvOAdBZMOfQlypSFkjcOcu9FalllIDJ1kvlREzcdIZmidQUqqeMv7jorHjq2HlLv/+c2lg==
|
||||||
|
|
||||||
yargs-parser@^11.1.1:
|
yargs-parser@^11.1.1:
|
||||||
version "11.1.1"
|
version "11.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-11.1.1.tgz#879a0865973bca9f6bab5cbdf3b1c67ec7d3bcf4"
|
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-11.1.1.tgz#879a0865973bca9f6bab5cbdf3b1c67ec7d3bcf4"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user