#50 [card] pronoun cards - display card
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<img :src="gravatar(user.email)" alt="" class="rounded-circle"/>
|
||||
<img :src="gravatar" alt="" class="rounded-circle"/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
@ -9,12 +9,13 @@
|
|||
export default {
|
||||
props: {
|
||||
user: { required: true },
|
||||
size: { 'default': 128 }
|
||||
},
|
||||
methods: {
|
||||
computed: {
|
||||
gravatar(email, size = 128) {
|
||||
const fallback = `https://avi.avris.it/${size}/${Base64.encode(email).replace(/\+/g, '-').replace(/\//g, '_')}.png`;
|
||||
const fallback = `https://avi.avris.it/${this.size}/${Base64.encode(this.user.username).replace(/\+/g, '-').replace(/\//g, '_')}.png`;
|
||||
|
||||
return `https://www.gravatar.com/avatar/${md5(email)}?d=${encodeURIComponent(fallback)}&s=${size}`;
|
||||
return `https://www.gravatar.com/avatar/${this.user.emailHash || md5(this.user.email)}?d=${encodeURIComponent(fallback)}&s=${this.size}`;
|
||||
}
|
||||
},
|
||||
}
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
<template>
|
||||
<span>
|
||||
<img :src="src" alt=""/>
|
||||
{{ name }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
name: { required: true },
|
||||
src: { required: true },
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
img {
|
||||
height: 1rem;
|
||||
}
|
||||
</style>
|
|
@ -128,14 +128,14 @@
|
|||
});
|
||||
}
|
||||
|
||||
// if (this.config.user.enabled) {
|
||||
// links.push({
|
||||
// link: '/' + this.config.user.route,
|
||||
// icon: 'user',
|
||||
// text: this.user ? '@' + this.user.username : this.$t('user.header'),
|
||||
// textLong: this.user ? '@' + this.user.username : this.$t('user.headerLong'),
|
||||
// });
|
||||
// }
|
||||
if (this.config.user.enabled) {
|
||||
links.push({
|
||||
link: '/' + this.config.user.route,
|
||||
icon: 'user',
|
||||
text: this.user ? '@' + this.user.username : this.$t('user.header'),
|
||||
textLong: this.user ? '@' + this.user.username : this.$t('user.headerLong'),
|
||||
});
|
||||
}
|
||||
|
||||
return links;
|
||||
},
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
<template>
|
||||
<span>
|
||||
<Icon :v="icon()"/>
|
||||
<strong v-if="opinion > 0">
|
||||
<nuxt-link v-if="link" :to="link">{{ word }}</nuxt-link>
|
||||
<span v-else>{{ word }}</span>
|
||||
</strong>
|
||||
<span v-else-if="opinion < 0" class="text-muted">
|
||||
<nuxt-link v-if="link" :to="link">{{ word }}</nuxt-link>
|
||||
<span v-else>{{ word }}</span>
|
||||
</span>
|
||||
<span v-else>
|
||||
<nuxt-link v-if="link" :to="link">{{ word }}</nuxt-link>
|
||||
<span v-else>{{ word }}</span>
|
||||
</span>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
word: { required: true },
|
||||
opinion: { required: true },
|
||||
link: {},
|
||||
},
|
||||
methods: {
|
||||
icon() {
|
||||
const opinion = parseInt(this.opinion);
|
||||
if (opinion > 0) {
|
||||
return 'heart';
|
||||
} else if (opinion === 0) {
|
||||
return 'thumbs-up';
|
||||
}
|
||||
return 'thumbs-down';
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,47 @@
|
|||
<template>
|
||||
<span>
|
||||
<Icon :v="icon" :set="iconSet"/>
|
||||
<a :href="link" target="_blank" rel="noopener">
|
||||
{{text}}
|
||||
</a>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {clearUrl} from "../src/helpers";
|
||||
|
||||
const REGEX_TWITTER = '^https://twitter.com/([^/]+)';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
link: { required: true },
|
||||
},
|
||||
computed: {
|
||||
type() {
|
||||
if (this.link.match(REGEX_TWITTER)) {
|
||||
return 'twitter';
|
||||
}
|
||||
|
||||
return 'other';
|
||||
},
|
||||
icon() {
|
||||
return {
|
||||
twitter: 'twitter',
|
||||
other: 'globe-europe',
|
||||
}[this.type];
|
||||
},
|
||||
iconSet() {
|
||||
return {
|
||||
twitter: 'b',
|
||||
}[this.type] || 'l';
|
||||
},
|
||||
text() {
|
||||
if (this.type === 'twitter') {
|
||||
return this.link.match(REGEX_TWITTER)[1];
|
||||
}
|
||||
|
||||
return clearUrl(this.link);
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
|
@ -625,6 +625,11 @@ user:
|
|||
header: 'Avatar'
|
||||
change: 'Zmień'
|
||||
|
||||
profile:
|
||||
names: 'Imiona'
|
||||
pronouns: 'Zaimki'
|
||||
words: 'Słowa'
|
||||
|
||||
share: 'Udostępnij'
|
||||
|
||||
crud:
|
||||
|
|
|
@ -93,6 +93,7 @@ export default {
|
|||
'/banner': '~/server/banner.js',
|
||||
'/api/nouns': '~/server/nouns.js',
|
||||
'/api/user': '~/server/user.js',
|
||||
'/api/profile': '~/server/profile.js',
|
||||
},
|
||||
axios: {
|
||||
baseURL: process.env.BASE_URL + '/api',
|
||||
|
@ -136,6 +137,8 @@ export default {
|
|||
}
|
||||
routes.push({ path: '/' + config.template.any.route, component: resolve(__dirname, 'routes/any.vue') });
|
||||
|
||||
routes.push({ path: '/@*', component: resolve(__dirname, 'routes/profile.vue') });
|
||||
|
||||
routes.push({ name: 'all', path: '*', component: resolve(__dirname, 'routes/template.vue') });
|
||||
},
|
||||
},
|
||||
|
|
|
@ -0,0 +1,123 @@
|
|||
<template>
|
||||
<div class="container">
|
||||
<template v-if="profile">
|
||||
<h2>
|
||||
<Avatar :user="profile"/>
|
||||
@{{username}}
|
||||
</h2>
|
||||
|
||||
<section v-if="profile.description.trim().length">
|
||||
<p v-for="line in profile.description.split('\n')" class="mb-1">
|
||||
{{ line }}
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section v-if="profile.age || Object.keys(profile.links).length">
|
||||
<ul class="list-inline">
|
||||
<li v-if="profile.age" class="list-inline-item">
|
||||
<Icon v="birthday-cake"/>
|
||||
{{ profile.age }}
|
||||
</li>
|
||||
<li v-for="link in profile.links" class="list-inline-item">
|
||||
<ProfileLink :link="link"/>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section v-if="Object.keys(profile.flags).length">
|
||||
<ul class="list-inline">
|
||||
<li v-for="(name, flag) in profile.flags" class="list-inline-item">
|
||||
<Flag :name="name" :src="`/flags/${flag}.png`"/>
|
||||
</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="(opinion, pronoun) in profile.pronouns"><Opinion :word="buildTemplate(pronoun).name('')" :opinion="opinion" :link="`/${pronoun}`"/></li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h3>
|
||||
<Icon v="scroll-old"/>
|
||||
<T>profile.words</T>
|
||||
</h3>
|
||||
|
||||
<div class="d-flex">
|
||||
<div v-for="group in profile.words" v-if="Object.keys(profile.words).length" class="w-25">
|
||||
<ul class="list-unstyled">
|
||||
<li v-for="(opinion, word) in group"><Opinion :word="word" :opinion="opinion"/></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { head } from "../src/helpers";
|
||||
import { templates } from "~/src/data";
|
||||
import { buildTemplate } from "../src/buildTemplate";
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
username: this.$route.params.pathMatch,
|
||||
profiles: {},
|
||||
}
|
||||
},
|
||||
async asyncData({ app, route }) {
|
||||
return {
|
||||
profiles: await app.$axios.$get(`/profile/get/${route.params.pathMatch}`),
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
buildTemplate(link) {
|
||||
return buildTemplate(templates, link);
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
profile() {
|
||||
for (let locale in this.profiles) {
|
||||
if (locale === this.config.locale) {
|
||||
return this.profiles[locale];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
},
|
||||
head() {
|
||||
return head({
|
||||
title: `@${this.username}`,
|
||||
});
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.avatar {
|
||||
width: 100%;
|
||||
max-width: 5rem;
|
||||
max-height: 5rem;
|
||||
}
|
||||
</style>
|
|
@ -3,15 +3,7 @@ const SQL = require('sql-template-strings');
|
|||
import { ulid } from 'ulid'
|
||||
import authenticate from './authenticate';
|
||||
|
||||
const parseQuery = (queryString) => {
|
||||
const query = {};
|
||||
const pairs = (queryString[0] === '?' ? queryString.substr(1) : queryString).split('&');
|
||||
for (let i = 0; i < pairs.length; i++) {
|
||||
let pair = pairs[i].split('=');
|
||||
query[decodeURIComponent(pair[0])] = decodeURIComponent(pair[1] || '');
|
||||
}
|
||||
return query;
|
||||
}
|
||||
|
||||
|
||||
const getId = url => url.match(/\/([^/]+)$/)[1];
|
||||
|
||||
|
|
|
@ -0,0 +1,65 @@
|
|||
const dbConnection = require('./db');
|
||||
const SQL = require('sql-template-strings');
|
||||
import {buildDict, render} from "../src/helpers";
|
||||
import { ulid } from 'ulid'
|
||||
import authenticate from './authenticate';
|
||||
import md5 from 'js-md5';
|
||||
|
||||
const calcAge = birthday => {
|
||||
if (!birthday) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
const birth = new Date(
|
||||
parseInt(birthday.substring(0, 4)),
|
||||
parseInt(birthday.substring(5, 7)) - 1,
|
||||
parseInt(birthday.substring(8, 10))
|
||||
);
|
||||
|
||||
const diff = now.getTime() - birth.getTime();
|
||||
|
||||
return parseInt(Math.floor(diff / 1000 / 60 / 60 / 24 / 365.25));
|
||||
}
|
||||
|
||||
const buildProfile = profile => {
|
||||
return {
|
||||
id: profile.id,
|
||||
userId: profile.userId,
|
||||
username: profile.username,
|
||||
emailHash: md5(profile.email),
|
||||
names: JSON.parse(profile.names),
|
||||
pronouns: JSON.parse(profile.pronouns),
|
||||
description: profile.description,
|
||||
age: calcAge(profile.birthday),
|
||||
links: JSON.parse(profile.links),
|
||||
flags: JSON.parse(profile.flags),
|
||||
words: JSON.parse(profile.words),
|
||||
};
|
||||
};
|
||||
|
||||
export default async function (req, res, next) {
|
||||
const db = await dbConnection();
|
||||
const user = authenticate(req);
|
||||
|
||||
|
||||
if (req.method === 'GET' && req.url.startsWith('/get/')) {
|
||||
const profiles = await db.all(SQL`
|
||||
SELECT profiles.*, users.username, users.email FROM profiles LEFT JOIN users on users.id == profiles.userId
|
||||
WHERE users.username = ${req.url.substring(5)}
|
||||
AND profiles.active = 1
|
||||
ORDER BY profiles.locale
|
||||
`)
|
||||
return render(res, buildDict(function* () {
|
||||
for (let profile of profiles) {
|
||||
yield [profile.locale, buildProfile(profile)];
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
if (!user || !user.authenticated) {
|
||||
return render(res, {error: 'unauthorised'}, 401);
|
||||
}
|
||||
|
||||
return render(res, { error: 'notfound' }, 404);
|
||||
}
|
|
@ -72,3 +72,20 @@ export const makeId = (length, characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghi
|
|||
|
||||
return result;
|
||||
}
|
||||
|
||||
export const parseQuery = (queryString) => {
|
||||
const query = {};
|
||||
const pairs = (queryString[0] === '?' ? queryString.substr(1) : queryString).split('&');
|
||||
for (let i = 0; i < pairs.length; i++) {
|
||||
let pair = pairs[i].split('=');
|
||||
query[decodeURIComponent(pair[0])] = decodeURIComponent(pair[1] || '');
|
||||
}
|
||||
return query;
|
||||
}
|
||||
|
||||
export const render = (res, content, status = 200) => {
|
||||
res.status = status;
|
||||
res.setHeader('content-type', 'application/json');
|
||||
res.write(JSON.stringify(content));
|
||||
res.end();
|
||||
}
|
||||
|
|
After Width: | Height: | Size: 2.4 KiB |
After Width: | Height: | Size: 8.3 KiB |
After Width: | Height: | Size: 1.9 KiB |
After Width: | Height: | Size: 1.8 KiB |
After Width: | Height: | Size: 11 KiB |
After Width: | Height: | Size: 23 KiB |
After Width: | Height: | Size: 2.0 KiB |