#220 [profile] card images
This commit is contained in:
parent
ba5df7e400
commit
9eaf3f8046
|
@ -13,6 +13,7 @@
|
||||||
/stats.json
|
/stats.json
|
||||||
|
|
||||||
/cache
|
/cache
|
||||||
|
/static/card
|
||||||
|
|
||||||
# Created by .ignore support plugin (hsz.mobi)
|
# Created by .ignore support plugin (hsz.mobi)
|
||||||
### Node template
|
### Node template
|
||||||
|
|
|
@ -61,15 +61,6 @@ body {
|
||||||
max-width: min(90vw, 920px);
|
max-width: min(90vw, 920px);
|
||||||
}
|
}
|
||||||
|
|
||||||
@include media-breakpoint-up('lg', $grid-breakpoints) {
|
|
||||||
body {
|
|
||||||
margin-top: $header-margin;
|
|
||||||
}
|
|
||||||
.sticky-top {
|
|
||||||
top: $header-height - 1px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
section {
|
section {
|
||||||
margin: 2*$spacer 0;
|
margin: 2*$spacer 0;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,182 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="mb-3 d-flex justify-content-between flex-column flex-md-row">
|
||||||
|
<h2 class="text-nowrap">
|
||||||
|
<Avatar :user="profile"/>
|
||||||
|
@{{profile.username}}
|
||||||
|
</h2>
|
||||||
|
<div class="flex-grow-1 text-lg-end">
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section v-if="profile.age || profile.description.trim().length || profile.team">
|
||||||
|
<p v-if="profile.team" class="mb-2">
|
||||||
|
<nuxt-link :to="`/${config.contact.team.route}`" class="badge bg-primary text-white">
|
||||||
|
<Icon v="collective-logo.svg" class="inverted"/>
|
||||||
|
<T>contact.team.member</T>
|
||||||
|
</nuxt-link>
|
||||||
|
</p>
|
||||||
|
<p v-for="line in profile.description.split('\n')" class="mb-1">
|
||||||
|
<Spelling escape :text="line"/>
|
||||||
|
</p>
|
||||||
|
<p v-if="profile.age">
|
||||||
|
<Icon v="birthday-cake"/>
|
||||||
|
{{ profile.age }}
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section v-if="profile.flags.length || Object.keys(profile.customFlags).length">
|
||||||
|
<ul class="list-inline">
|
||||||
|
<li v-for="flag in profile.flags" v-if="allFlags[flag]" class="list-inline-item pr-2">
|
||||||
|
<Flag :name="flag.startsWith('-') ? allFlags[flag] : $translateForPronoun(allFlags[flag], mainPronoun)"
|
||||||
|
:alt="allFlags[flag]"
|
||||||
|
:img="`/flags/${flag}.png`"
|
||||||
|
:terms="terms || []"/>
|
||||||
|
</li>
|
||||||
|
<li v-for="(desc, flag) in profile.customFlags" class="list-inline-item pr-2">
|
||||||
|
<Flag :name="desc"
|
||||||
|
:alt="desc"
|
||||||
|
:img="buildImageUrl(flag, 'flag')"
|
||||||
|
:terms="terms|| []"
|
||||||
|
custom/>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section v-if="profile.links.length">
|
||||||
|
<ul class="list-inline">
|
||||||
|
<li v-for="link in profile.links" class="list-inline-item pr-2">
|
||||||
|
<ProfileLink :link="link"/>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="d-flex">
|
||||||
|
<div class="w-50" v-if="Object.keys(profile.names).length">
|
||||||
|
<h3>
|
||||||
|
<Icon v="signature"/>
|
||||||
|
<T>profile.names</T>
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<ul class="list-unstyled">
|
||||||
|
<li v-for="(opinion, name) in profile.names"><Opinion :word="name" :opinion="opinion"/></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="w-50" v-if="Object.keys(profile.pronouns).length">
|
||||||
|
<h3>
|
||||||
|
<Icon v="tags"/>
|
||||||
|
<T>profile.pronouns</T>
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<ul class="list-unstyled">
|
||||||
|
<li v-for="{link, pronoun, opinion} in pronounOpinions">
|
||||||
|
<Opinion :word="typeof pronoun === 'string' ? pronoun : (pronoun.name(glue) + (pronoun.smallForm ? '/' + pronoun.morphemes[pronoun.smallForm] : ''))" :opinion="opinion" :link="`/${link}`"/>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="clearfix">
|
||||||
|
<h3>
|
||||||
|
<Icon v="scroll-old"/>
|
||||||
|
<T>profile.words</T>
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div v-for="group in profile.words" v-if="Object.keys(profile.words).length" class="float-start w-50 w-md-25">
|
||||||
|
<ul class="list-unstyled">
|
||||||
|
<li v-for="(opinion, word) in group"><Opinion :word="word" :opinion="opinion"/></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<OpinionLegend/>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { pronouns } from "~/src/data";
|
||||||
|
import { buildPronoun } from "../src/buildPronoun";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
profile: { required: true },
|
||||||
|
terms: { 'default': null },
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
allFlags: process.env.FLAGS,
|
||||||
|
glue: ' ' + this.$t('pronouns.or') + ' ',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
pronounOpinions() {
|
||||||
|
const pronounOpinions = [];
|
||||||
|
for (let pronoun in this.profile.pronouns) {
|
||||||
|
if (!this.profile.pronouns.hasOwnProperty(pronoun)) { continue; }
|
||||||
|
|
||||||
|
let link = decodeURIComponent(
|
||||||
|
pronoun
|
||||||
|
.trim()
|
||||||
|
.replace(new RegExp('^' + this.$base), '')
|
||||||
|
.replace(new RegExp('^' + this.$base.replace(/^https?:\/\//, '')), '')
|
||||||
|
.replace(new RegExp('^/'), '')
|
||||||
|
);
|
||||||
|
if (!link.startsWith(':')) {
|
||||||
|
link = link.toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (link === this.config.pronouns.any || link === this.config.pronouns.avoiding) {
|
||||||
|
pronounOpinions.push({
|
||||||
|
link,
|
||||||
|
pronoun: link,
|
||||||
|
opinion: this.profile.pronouns[pronoun],
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pronounEntity = buildPronoun(pronouns, link);
|
||||||
|
|
||||||
|
if (pronounEntity) {
|
||||||
|
pronounOpinions.push({
|
||||||
|
link,
|
||||||
|
pronoun: pronounEntity,
|
||||||
|
opinion: this.profile.pronouns[pronoun],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return pronounOpinions;
|
||||||
|
},
|
||||||
|
mainPronoun() {
|
||||||
|
let mainPronoun = buildPronoun(pronouns, this.config.profile.flags.defaultPronoun);
|
||||||
|
let mainOpinion = -1;
|
||||||
|
for (let {pronoun, opinion} of this.pronounOpinions) {
|
||||||
|
if (typeof pronoun === 'string') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (opinion === 2) {
|
||||||
|
opinion = 0.5;
|
||||||
|
}
|
||||||
|
if (opinion > mainOpinion) {
|
||||||
|
mainPronoun = pronoun;
|
||||||
|
mainOpinion = opinion;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return mainPronoun;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.avatar {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 5rem;
|
||||||
|
max-height: 5rem;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,20 @@
|
||||||
|
<template>
|
||||||
|
<main class="container vh-100">
|
||||||
|
<Nuxt/>
|
||||||
|
</main>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import dark from "../plugins/dark";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
mixins: [dark],
|
||||||
|
mounted() {
|
||||||
|
this.setMode(this.detectDark());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@import "assets/style";
|
||||||
|
</style>
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="d-flex flex-column vh-100">
|
<div class="d-flex flex-column vh-100 body">
|
||||||
<div class="flex-grow-1">
|
<div class="flex-grow-1">
|
||||||
<Header/>
|
<Header/>
|
||||||
<main class="container">
|
<main class="container">
|
||||||
|
@ -56,4 +56,13 @@
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@import "assets/style";
|
@import "assets/style";
|
||||||
@import "~avris-sorter/dist/Sorter.min.css";
|
@import "~avris-sorter/dist/Sorter.min.css";
|
||||||
|
|
||||||
|
@include media-breakpoint-up('lg', $grid-breakpoints) {
|
||||||
|
.body {
|
||||||
|
margin-top: $header-margin;
|
||||||
|
}
|
||||||
|
.sticky-top {
|
||||||
|
top: $header-height - 1px;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -414,6 +414,10 @@ profile:
|
||||||
# Then you can link to it in your bio or email footer.
|
# Then you can link to it in your bio or email footer.
|
||||||
# Just create an account {/account=here}.
|
# Just create an account {/account=here}.
|
||||||
# bannerButton: 'Create a card'
|
# bannerButton: 'Create a card'
|
||||||
|
# TODO
|
||||||
|
card:
|
||||||
|
link: 'Card picture'
|
||||||
|
generating: 'Generation in progress…'
|
||||||
|
|
||||||
share: 'Teilen'
|
share: 'Teilen'
|
||||||
|
|
||||||
|
|
|
@ -497,6 +497,9 @@ profile:
|
||||||
Then you can link to it in your bio or email footer.
|
Then you can link to it in your bio or email footer.
|
||||||
Just create an account {/account=here}.
|
Just create an account {/account=here}.
|
||||||
bannerButton: 'Create a card'
|
bannerButton: 'Create a card'
|
||||||
|
card:
|
||||||
|
link: 'Card picture'
|
||||||
|
generating: 'Generation in progress…'
|
||||||
|
|
||||||
share: 'Share'
|
share: 'Share'
|
||||||
|
|
||||||
|
|
|
@ -423,6 +423,10 @@ profile:
|
||||||
# Then you can link to it in your bio or email footer.
|
# Then you can link to it in your bio or email footer.
|
||||||
# Just create an account {/account=here}.
|
# Just create an account {/account=here}.
|
||||||
# bannerButton: 'Create a card'
|
# bannerButton: 'Create a card'
|
||||||
|
# TODO
|
||||||
|
card:
|
||||||
|
link: 'Card picture'
|
||||||
|
generating: 'Generation in progress…'
|
||||||
|
|
||||||
share: 'Compartir'
|
share: 'Compartir'
|
||||||
|
|
||||||
|
|
|
@ -417,6 +417,10 @@ profile:
|
||||||
# Then you can link to it in your bio or email footer.
|
# Then you can link to it in your bio or email footer.
|
||||||
# Just create an account {/account=here}.
|
# Just create an account {/account=here}.
|
||||||
# bannerButton: 'Create a card'
|
# bannerButton: 'Create a card'
|
||||||
|
# TODO
|
||||||
|
card:
|
||||||
|
link: 'Card picture'
|
||||||
|
generating: 'Generation in progress…'
|
||||||
|
|
||||||
share: 'Deel'
|
share: 'Deel'
|
||||||
|
|
||||||
|
|
|
@ -977,6 +977,9 @@ profile:
|
||||||
Potem możesz do niej podlinkować w swoim bio czy stopce maila.
|
Potem możesz do niej podlinkować w swoim bio czy stopce maila.
|
||||||
Wystarczy, że założysz konto {/konto=tutaj}.
|
Wystarczy, że założysz konto {/konto=tutaj}.
|
||||||
bannerButton: 'Stwórz wizytówkę'
|
bannerButton: 'Stwórz wizytówkę'
|
||||||
|
card:
|
||||||
|
link: 'Obrazek'
|
||||||
|
generating: 'Trwa generowanie obrazka…'
|
||||||
|
|
||||||
census:
|
census:
|
||||||
header: 'Spis'
|
header: 'Spis'
|
||||||
|
|
|
@ -421,6 +421,10 @@ profile:
|
||||||
# Then you can link to it in your bio or email footer.
|
# Then you can link to it in your bio or email footer.
|
||||||
# Just create an account {/account=here}.
|
# Just create an account {/account=here}.
|
||||||
# bannerButton: 'Create a card'
|
# bannerButton: 'Create a card'
|
||||||
|
# TODO
|
||||||
|
card:
|
||||||
|
link: 'Card picture'
|
||||||
|
generating: 'Generation in progress…'
|
||||||
|
|
||||||
share: 'Compartilhar'
|
share: 'Compartilhar'
|
||||||
|
|
||||||
|
|
|
@ -956,6 +956,10 @@ profile:
|
||||||
# Then you can link to it in your bio or email footer.
|
# Then you can link to it in your bio or email footer.
|
||||||
# Just create an account {/account=here}.
|
# Just create an account {/account=here}.
|
||||||
# bannerButton: 'Create a card'
|
# bannerButton: 'Create a card'
|
||||||
|
# TODO
|
||||||
|
card:
|
||||||
|
link: 'Card picture'
|
||||||
|
generating: 'Generation in progress…'
|
||||||
|
|
||||||
census:
|
census:
|
||||||
header: 'Spis'
|
header: 'Spis'
|
||||||
|
|
|
@ -435,6 +435,10 @@ profile:
|
||||||
# Then you can link to it in your bio or email footer.
|
# Then you can link to it in your bio or email footer.
|
||||||
# Just create an account {/account=here}.
|
# Just create an account {/account=here}.
|
||||||
# bannerButton: 'Create a card'
|
# bannerButton: 'Create a card'
|
||||||
|
# TODO
|
||||||
|
card:
|
||||||
|
link: 'Card picture'
|
||||||
|
generating: 'Generation in progress…'
|
||||||
|
|
||||||
share: 'Share'
|
share: 'Share'
|
||||||
|
|
||||||
|
|
|
@ -402,6 +402,10 @@ profile:
|
||||||
# Then you can link to it in your bio or email footer.
|
# Then you can link to it in your bio or email footer.
|
||||||
# Just create an account {/account=here}.
|
# Just create an account {/account=here}.
|
||||||
# bannerButton: 'Create a card'
|
# bannerButton: 'Create a card'
|
||||||
|
# TODO
|
||||||
|
card:
|
||||||
|
link: 'Card picture'
|
||||||
|
generating: 'Generation in progress…'
|
||||||
|
|
||||||
share: '這裡'
|
share: '這裡'
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
-- Up
|
||||||
|
|
||||||
|
ALTER TABLE profiles ADD COLUMN card TEXT NULL;
|
||||||
|
|
||||||
|
-- Down
|
|
@ -230,6 +230,7 @@ export default {
|
||||||
|
|
||||||
if (config.profile.enabled) {
|
if (config.profile.enabled) {
|
||||||
routes.push({path: '/@*', component: resolve(__dirname, 'routes/profile.vue')});
|
routes.push({path: '/@*', component: resolve(__dirname, 'routes/profile.vue')});
|
||||||
|
routes.push({path: '/card/@*', component: resolve(__dirname, 'routes/profileCard.vue')});
|
||||||
if (config.profile.editorEnabled) {
|
if (config.profile.editorEnabled) {
|
||||||
routes.push({path: '/editor', component: resolve(__dirname, 'routes/profileEditor.vue')});
|
routes.push({path: '/editor', component: resolve(__dirname, 'routes/profileEditor.vue')});
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,6 +34,7 @@
|
||||||
"markdown-loader": "^6.0.0",
|
"markdown-loader": "^6.0.0",
|
||||||
"multer": "^1.4.2",
|
"multer": "^1.4.2",
|
||||||
"nuxt": "^2.15.2",
|
"nuxt": "^2.15.2",
|
||||||
|
"pageres": "^6.2.3",
|
||||||
"rtlcss": "^3.1.2",
|
"rtlcss": "^3.1.2",
|
||||||
"sha1": "^1.1.1",
|
"sha1": "^1.1.1",
|
||||||
"sql-template-strings": "^2.2.2",
|
"sql-template-strings": "^2.2.2",
|
||||||
|
|
|
@ -1,40 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<div v-if="profile">
|
<div v-if="profile">
|
||||||
<ClientOnly>
|
|
||||||
<div slot="placeholder" class="my-5 text-center">
|
|
||||||
<Spinner size="5rem"/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-3 d-flex justify-content-between flex-column flex-md-row">
|
|
||||||
<h2 class="text-nowrap">
|
|
||||||
<Avatar :user="profile"/>
|
|
||||||
@{{username}}
|
|
||||||
</h2>
|
|
||||||
<div class="flex-grow-1 text-lg-end">
|
|
||||||
<div>
|
|
||||||
<nuxt-link v-if="$user() && $user().username === username" to="/editor"
|
|
||||||
class="btn btn-outline-primary btn-sm mb-2 mx-1"
|
|
||||||
>
|
|
||||||
<Icon v="edit"/>
|
|
||||||
<T>profile.edit</T>
|
|
||||||
</nuxt-link>
|
|
||||||
<a :href="`https://pronouns.page/@${username}`" v-if="Object.keys(profiles).length > 1 && $user() && $user().username === username"
|
|
||||||
class="btn btn-outline-secondary btn-sm mb-2 mx-1"
|
|
||||||
>
|
|
||||||
<Icon v="external-link"/>
|
|
||||||
pronouns.page/@{{username}}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div v-if="Object.keys(profiles).length > 1">
|
|
||||||
<LocaleLink v-for="(options, locale) in locales" :key="locale" v-if="profiles[locale] !== undefined"
|
|
||||||
:locale="locale" :link="`/@${username}`"
|
|
||||||
:class="['btn', locale === config.locale ? 'btn-primary disabled' : 'btn-outline-primary', 'btn-sm', 'mb-2 mx-1']">
|
|
||||||
{{options.name}}
|
|
||||||
</LocaleLink>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<section v-if="$isGranted('users') && profile.bannedReason">
|
<section v-if="$isGranted('users') && profile.bannedReason">
|
||||||
<div class="alert alert-warning">
|
<div class="alert alert-warning">
|
||||||
<p class="h4">
|
<p class="h4">
|
||||||
|
@ -45,91 +10,48 @@
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section v-if="profile.age || profile.description.trim().length || profile.team">
|
<Profile :profile="profile" :terms="terms">
|
||||||
<p v-if="profile.team" class="mb-2">
|
<div v-if="Object.keys(profiles).length > 1">
|
||||||
<nuxt-link :to="`/${config.contact.team.route}`" class="badge bg-primary text-white">
|
<LocaleLink v-for="(options, locale) in locales" :key="locale" v-if="profiles[locale] !== undefined"
|
||||||
<Icon v="collective-logo.svg" class="inverted"/>
|
:locale="locale" :link="`/@${profile.username}`"
|
||||||
<T>contact.team.member</T>
|
:class="['btn', locale === config.locale ? 'btn-primary disabled' : 'btn-outline-primary', 'btn-sm', 'mb-2 mx-1']">
|
||||||
|
{{options.name}}
|
||||||
|
</LocaleLink>
|
||||||
|
</div>
|
||||||
|
<div v-if="$user() && $user().username === profile.username">
|
||||||
|
<nuxt-link to="/editor"
|
||||||
|
class="btn btn-primary btn-sm mb-2 mx-1"
|
||||||
|
>
|
||||||
|
<Icon v="edit"/>
|
||||||
|
<T>profile.edit</T>
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
</p>
|
<a :href="`https://pronouns.page/@${profile.username}`" v-if="Object.keys(profiles).length > 1"
|
||||||
<p v-for="line in profile.description.split('\n')" class="mb-1">
|
class="btn btn-outline-secondary btn-sm mb-2 mx-1"
|
||||||
<Spelling escape :text="line"/>
|
>
|
||||||
</p>
|
<Icon v="external-link"/>
|
||||||
<p v-if="profile.age">
|
pronouns.page/@{{profile.username}}
|
||||||
<Icon v="birthday-cake"/>
|
</a>
|
||||||
{{ profile.age }}
|
|
||||||
</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section v-if="profile.flags.length || Object.keys(profile.customFlags).length">
|
|
||||||
<ul class="list-inline">
|
|
||||||
<li v-for="flag in profile.flags" v-if="allFlags[flag]" class="list-inline-item pr-2">
|
|
||||||
<Flag :name="flag.startsWith('-') ? allFlags[flag] : $translateForPronoun(allFlags[flag], mainPronoun)"
|
|
||||||
:alt="allFlags[flag]"
|
|
||||||
:img="`/flags/${flag}.png`"
|
|
||||||
:terms="terms"/>
|
|
||||||
</li>
|
|
||||||
<li v-for="(desc, flag) in profile.customFlags" class="list-inline-item pr-2">
|
|
||||||
<Flag :name="desc"
|
|
||||||
:alt="desc"
|
|
||||||
:img="buildImageUrl(flag, 'flag')"
|
|
||||||
:terms="terms"
|
|
||||||
custom/>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section v-if="profile.links.length">
|
|
||||||
<ul class="list-inline">
|
|
||||||
<li v-for="link in profile.links" class="list-inline-item pr-2">
|
|
||||||
<ProfileLink :link="link"/>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="d-flex">
|
|
||||||
<div class="w-50" v-if="Object.keys(profile.names).length">
|
|
||||||
<h3>
|
|
||||||
<Icon v="signature"/>
|
|
||||||
<T>profile.names</T>
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
<ul class="list-unstyled">
|
|
||||||
<li v-for="(opinion, name) in profile.names"><Opinion :word="name" :opinion="opinion"/></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="w-50" v-if="Object.keys(profile.pronouns).length">
|
<div v-if="$user() && $user().username === profile.username">
|
||||||
<h3>
|
<small>
|
||||||
<Icon v="tags"/>
|
<Icon v="id-card"/>
|
||||||
<T>profile.pronouns</T>
|
<T>profile.card.link</T>:
|
||||||
</h3>
|
</small>
|
||||||
|
<template v-if="profile.card">
|
||||||
<ul class="list-unstyled">
|
<a :href="profile.card" target="_blank" rel="noopener"
|
||||||
<li v-for="{link, pronoun, opinion} in pronounOpinions">
|
class="btn btn-outline-success btn-sm mb-2 mx-1">
|
||||||
<Opinion :word="typeof pronoun === 'string' ? pronoun : (pronoun.name(glue) + (pronoun.smallForm ? '/' + pronoun.morphemes[pronoun.smallForm] : ''))" :opinion="opinion" :link="`/${link}`"/>
|
<Icon v="sun"/>
|
||||||
</li>
|
<T>mode.light</T>
|
||||||
</ul>
|
</a>
|
||||||
|
<a :href="profile.card.replace('.png', '-dark.png')" target="_blank" rel="noopener"
|
||||||
|
class="btn btn-outline-success btn-sm mb-2 mx-1">
|
||||||
|
<Icon v="moon"/>
|
||||||
|
<T>mode.dark</T>
|
||||||
|
</a>
|
||||||
|
</template>
|
||||||
|
<small v-else><T>profile.card.generating</T></small>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</Profile>
|
||||||
|
|
||||||
<section class="clearfix">
|
|
||||||
<h3>
|
|
||||||
<Icon v="scroll-old"/>
|
|
||||||
<T>profile.words</T>
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<div v-for="group in profile.words" v-if="Object.keys(profile.words).length" class="float-start w-50 w-md-25">
|
|
||||||
<ul class="list-unstyled">
|
|
||||||
<li v-for="(opinion, word) in group"><Opinion :word="word" :opinion="opinion"/></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section>
|
|
||||||
<OpinionLegend/>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<client-only>
|
<client-only>
|
||||||
<section v-if="$isGranted('users')">
|
<section v-if="$isGranted('users')">
|
||||||
|
@ -148,7 +70,6 @@
|
||||||
<section>
|
<section>
|
||||||
<Share/>
|
<Share/>
|
||||||
</section>
|
</section>
|
||||||
</ClientOnly>
|
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="Object.keys(profiles).length">
|
<div v-else-if="Object.keys(profiles).length">
|
||||||
<h2 class="text-nowrap mb-3">
|
<h2 class="text-nowrap mb-3">
|
||||||
|
@ -170,18 +91,13 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import {head, listToDict} from "../src/helpers";
|
import { head } from "../src/helpers";
|
||||||
import { pronouns } from "~/src/data";
|
|
||||||
import { buildPronoun } from "../src/buildPronoun";
|
|
||||||
import ClientOnly from 'vue-client-only'
|
import ClientOnly from 'vue-client-only'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: { ClientOnly },
|
components: { ClientOnly },
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
profiles: {},
|
|
||||||
glue: ' ' + this.$t('pronouns.or') + ' ',
|
|
||||||
allFlags: process.env.FLAGS,
|
|
||||||
saving: false,
|
saving: false,
|
||||||
terms: [],
|
terms: [],
|
||||||
}
|
}
|
||||||
|
@ -197,6 +113,15 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
profile() {
|
||||||
|
for (let locale in this.profiles) {
|
||||||
|
if (locale === this.config.locale) {
|
||||||
|
return this.profiles[locale];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
},
|
||||||
username() {
|
username() {
|
||||||
const base = this.$route.params.pathMatch;
|
const base = this.$route.params.pathMatch;
|
||||||
|
|
||||||
|
@ -214,70 +139,6 @@
|
||||||
|
|
||||||
return this.profile.username;
|
return this.profile.username;
|
||||||
},
|
},
|
||||||
profile() {
|
|
||||||
for (let locale in this.profiles) {
|
|
||||||
if (locale === this.config.locale) {
|
|
||||||
return this.profiles[locale];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
pronounOpinions() {
|
|
||||||
const pronounOpinions = [];
|
|
||||||
for (let pronoun in this.profile.pronouns) {
|
|
||||||
if (!this.profile.pronouns.hasOwnProperty(pronoun)) { continue; }
|
|
||||||
|
|
||||||
let link = decodeURIComponent(
|
|
||||||
pronoun
|
|
||||||
.trim()
|
|
||||||
.replace(new RegExp('^' + this.$base), '')
|
|
||||||
.replace(new RegExp('^' + this.$base.replace(/^https?:\/\//, '')), '')
|
|
||||||
.replace(new RegExp('^/'), '')
|
|
||||||
);
|
|
||||||
if (!link.startsWith(':')) {
|
|
||||||
link = link.toLowerCase();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (link === this.config.pronouns.any || link === this.config.pronouns.avoiding) {
|
|
||||||
pronounOpinions.push({
|
|
||||||
link,
|
|
||||||
pronoun: link,
|
|
||||||
opinion: this.profile.pronouns[pronoun],
|
|
||||||
});
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const pronounEntity = buildPronoun(pronouns, link);
|
|
||||||
|
|
||||||
if (pronounEntity) {
|
|
||||||
pronounOpinions.push({
|
|
||||||
link,
|
|
||||||
pronoun: pronounEntity,
|
|
||||||
opinion: this.profile.pronouns[pronoun],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return pronounOpinions;
|
|
||||||
},
|
|
||||||
mainPronoun() {
|
|
||||||
let mainPronoun = buildPronoun(pronouns, this.config.profile.flags.defaultPronoun);
|
|
||||||
let mainOpinion = -1;
|
|
||||||
for (let {pronoun, opinion} of this.pronounOpinions) {
|
|
||||||
if (typeof pronoun === 'string') {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (opinion === 2) {
|
|
||||||
opinion = 0.5;
|
|
||||||
}
|
|
||||||
if (opinion > mainOpinion) {
|
|
||||||
mainPronoun = pronoun;
|
|
||||||
mainOpinion = opinion;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return mainPronoun;
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
async ban() {
|
async ban() {
|
||||||
|
@ -305,12 +166,6 @@
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@import "assets/variables";
|
@import "assets/variables";
|
||||||
|
|
||||||
.avatar {
|
|
||||||
width: 100%;
|
|
||||||
max-width: 5rem;
|
|
||||||
max-height: 5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.list-group-item-hoverable {
|
.list-group-item-hoverable {
|
||||||
&:hover {
|
&:hover {
|
||||||
color: $primary;
|
color: $primary;
|
||||||
|
|
|
@ -0,0 +1,41 @@
|
||||||
|
<template>
|
||||||
|
<Profile v-if="profile" :profile="profile" class="pb-3">
|
||||||
|
<nuxt-link to="/">
|
||||||
|
<h1 class="text-nowrap h5">
|
||||||
|
<Icon v="tags"/>
|
||||||
|
<T>title</T>
|
||||||
|
</h1>
|
||||||
|
</nuxt-link>
|
||||||
|
</Profile>
|
||||||
|
<NotFound v-else/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { head } from "../src/helpers";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
layout: 'basic',
|
||||||
|
async asyncData({ app, route }) {
|
||||||
|
return {
|
||||||
|
profiles: await app.$axios.$get(`/profile/get/${encodeURIComponent(route.params.pathMatch)}`),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
profile() {
|
||||||
|
for (let locale in this.profiles) {
|
||||||
|
if (locale === this.config.locale) {
|
||||||
|
return this.profiles[locale];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
head() {
|
||||||
|
return head({
|
||||||
|
title: `@${this.$route.params.pathMatch}`,
|
||||||
|
banner: `api/banner/@${this.$route.params.pathMatch}.png`,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -1,4 +1,4 @@
|
||||||
export default {
|
module.exports = {
|
||||||
region: process.env.AWS_REGION,
|
region: process.env.AWS_REGION,
|
||||||
credentials: {
|
credentials: {
|
||||||
accessKeyId: process.env.AWS_KEY,
|
accessKeyId: process.env.AWS_KEY,
|
||||||
|
|
|
@ -0,0 +1,46 @@
|
||||||
|
require('dotenv').config({ path:__dirname + '/../.env' });
|
||||||
|
const Pageres = require('pageres');
|
||||||
|
const isHighLoadTime = require('./overload');
|
||||||
|
const dbConnection = require('./db');
|
||||||
|
const locales = require('../src/locales');
|
||||||
|
const { ulid } = require('ulid');
|
||||||
|
|
||||||
|
const awsConfig = require('./aws');
|
||||||
|
const S3 = require('aws-sdk/clients/s3');
|
||||||
|
const s3 = new S3(awsConfig);
|
||||||
|
|
||||||
|
const urlBases = {}
|
||||||
|
for (let [code, , url, ] of locales) {
|
||||||
|
urlBases[code] = url + '/card/@'; // 'http://localhost:3000/card/@'
|
||||||
|
}
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
const db = await dbConnection();
|
||||||
|
for (let {id, locale, username} of await db.all('SELECT profiles.id, profiles.locale, users.username FROM profiles LEFT JOIN users on profiles.userId = users.id WHERE profiles.card IS NULL')) {
|
||||||
|
if (isHighLoadTime(locale)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// if (locale !== 'pl' || username !== 'andrea') { continue; }
|
||||||
|
|
||||||
|
const cardId = ulid();
|
||||||
|
const key = `card/${locale}/${username}-${cardId}.png`;
|
||||||
|
|
||||||
|
console.log(locale, username, cardId);
|
||||||
|
|
||||||
|
for (let dark of [false, true]) {
|
||||||
|
const [ buffer ] = await new Pageres({ darkMode: dark })
|
||||||
|
.src(urlBases[locale] + username, ['1024x300'])
|
||||||
|
.run();
|
||||||
|
|
||||||
|
const s3putResponse = await s3.putObject({
|
||||||
|
Key: dark ? key.replace('.png', '-dark.png') : key,
|
||||||
|
Body: buffer,
|
||||||
|
ContentType: 'image/png',
|
||||||
|
ACL: 'public-read',
|
||||||
|
}).promise();
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.get(`UPDATE profiles SET card='https://${awsConfig.params.Bucket}.s3.${awsConfig.region}.amazonaws.com/${key}' WHERE id='${id}'`)
|
||||||
|
// '/card/@${username}.png'
|
||||||
|
}
|
||||||
|
})();
|
|
@ -1,3 +1,5 @@
|
||||||
|
import isHighLoadTime from './overload';
|
||||||
|
|
||||||
const USER_AGENT_BOTS = /bot|crawler|baiduspider|80legs|ia_archiver|voyager|curl|wget|yahoo! slurp|mediapartners-google|facebookexternalhit|twitterbot|whatsapp|php|python/;
|
const USER_AGENT_BOTS = /bot|crawler|baiduspider|80legs|ia_archiver|voyager|curl|wget|yahoo! slurp|mediapartners-google|facebookexternalhit|twitterbot|whatsapp|php|python/;
|
||||||
const USER_AGENT_BROWSERS = /mozilla|msie|gecko|firefox|edge|opera|safari|netscape|konqueror|android/;
|
const USER_AGENT_BROWSERS = /mozilla|msie|gecko|firefox|edge|opera|safari|netscape|konqueror|android/;
|
||||||
|
|
||||||
|
@ -11,27 +13,9 @@ const isBrowser = (userAgent) => {
|
||||||
return isProbablyBrowser || !isProbablyBot;
|
return isProbablyBrowser || !isProbablyBot;
|
||||||
}
|
}
|
||||||
|
|
||||||
const overload = {
|
|
||||||
en: [[17, 23]],
|
|
||||||
};
|
|
||||||
|
|
||||||
const isHighLoad = (timestamp, overloadPeriods) => {
|
|
||||||
if (overloadPeriods === undefined) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let [periodStart, periodEnd] of overloadPeriods) {
|
|
||||||
if (timestamp.getUTCHours() >= periodStart && timestamp.getUTCHours() < periodEnd) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function(req, res, next) {
|
export default function(req, res, next) {
|
||||||
if (process.env.NODE_ENV === 'production') {
|
if (process.env.NODE_ENV === 'production' && !req.url.startsWith('/card/@')) {
|
||||||
res.spa = isBrowser(req.headers['user-agent']) || isHighLoad(new Date, overload[process.env.LOCALE]);
|
res.spa = isBrowser(req.headers['user-agent']) || isHighLoadTime(process.env.LOCALE);
|
||||||
}
|
}
|
||||||
next();
|
next();
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
const overloadPeriods = {
|
||||||
|
en: [[17, 23]],
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = (locale, timestamp = new Date) => {
|
||||||
|
if (overloadPeriods[locale] === undefined) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let [periodStart, periodEnd] of overloadPeriods[locale]) {
|
||||||
|
if (timestamp.getUTCHours() >= periodStart && timestamp.getUTCHours() < periodEnd) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
|
@ -58,6 +58,7 @@ const fetchProfiles = async (db, username, self, isAdmin) => {
|
||||||
footerAreas: profile.footerAreas ? profile.footerAreas.split(',') : [],
|
footerAreas: profile.footerAreas ? profile.footerAreas.split(',') : [],
|
||||||
bannedReason: profile.bannedReason,
|
bannedReason: profile.bannedReason,
|
||||||
team: !!profile.roles,
|
team: !!profile.roles,
|
||||||
|
card: profile.card,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return p;
|
return p;
|
||||||
|
@ -89,7 +90,8 @@ router.post('/profile/save', handleErrorAsync(async (req, res) => {
|
||||||
words = ${JSON.stringify(req.body.words)},
|
words = ${JSON.stringify(req.body.words)},
|
||||||
teamName = ${req.isGranted('users') ? req.body.teamName || null : ''},
|
teamName = ${req.isGranted('users') ? req.body.teamName || null : ''},
|
||||||
footerName = ${req.isGranted('users') ? req.body.footerName || null : ''},
|
footerName = ${req.isGranted('users') ? req.body.footerName || null : ''},
|
||||||
footerAreas = ${req.isGranted('users') ? req.body.footerAreas.join(',').toLowerCase() || null : ''}
|
footerAreas = ${req.isGranted('users') ? req.body.footerAreas.join(',').toLowerCase() || null : ''},
|
||||||
|
card = NULL
|
||||||
WHERE id = ${ids[0]}
|
WHERE id = ${ids[0]}
|
||||||
`);
|
`);
|
||||||
} else {
|
} else {
|
||||||
|
|
Reference in New Issue