#50 [card] pronoun cards - display card

This commit is contained in:
Avris 2020-10-16 20:17:50 +02:00
parent d49156537a
commit 9f252c10c1
18 changed files with 333 additions and 21 deletions

View File

@ -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}`;
}
},
}

21
components/Flag.vue Normal file
View File

@ -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>

View File

@ -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;
},

38
components/Opinion.vue Normal file
View File

@ -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>

View File

@ -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>

View File

@ -625,6 +625,11 @@ user:
header: 'Avatar'
change: 'Zmień'
profile:
names: 'Imiona'
pronouns: 'Zaimki'
words: 'Słowa'
share: 'Udostępnij'
crud:

View File

@ -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') });
},
},

123
routes/profile.vue Normal file
View File

@ -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>

View File

@ -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];

65
server/profile.js Normal file
View File

@ -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);
}

View File

@ -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();
}

BIN
static/flags/agender.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

BIN
static/flags/european.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

BIN
static/flags/nonbinary.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

BIN
static/flags/pansexual.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB