mirror of
https://github.com/Ocelot-Social-Community/Ocelot-Social.git
synced 2025-12-13 07:46:06 +00:00
Merge branch 'master' of github.com:Human-Connection/Human-Connection into 384-emotions-on-posts
This commit is contained in:
commit
1593eab9cf
@ -78,7 +78,7 @@
|
||||
"metascraper-date": "^5.6.5",
|
||||
"metascraper-description": "^5.6.5",
|
||||
"metascraper-image": "^5.6.5",
|
||||
"metascraper-lang": "^5.6.3",
|
||||
"metascraper-lang": "^5.6.5",
|
||||
"metascraper-lang-detector": "^4.8.5",
|
||||
"metascraper-logo": "^5.6.5",
|
||||
"metascraper-publisher": "^5.6.5",
|
||||
@ -87,6 +87,7 @@
|
||||
"metascraper-url": "^5.6.5",
|
||||
"metascraper-video": "^5.6.5",
|
||||
"metascraper-youtube": "^5.6.3",
|
||||
"minimatch": "^3.0.4",
|
||||
"neo4j-driver": "~1.7.5",
|
||||
"neo4j-graphql-js": "^2.6.3",
|
||||
"neode": "^0.3.0",
|
||||
|
||||
@ -64,7 +64,7 @@ describe('currentUser { notifications }', () => {
|
||||
let post
|
||||
const title = 'Mentioning Al Capone'
|
||||
const content =
|
||||
'Hey <a class="mention" href="/profile/you/al-capone">@al-capone</a> how do you do?'
|
||||
'Hey <a class="mention" data-mention-id="you" href="/profile/you/al-capone">@al-capone</a> how do you do?'
|
||||
|
||||
beforeEach(async () => {
|
||||
const createPostMutation = gql`
|
||||
@ -88,7 +88,7 @@ describe('currentUser { notifications }', () => {
|
||||
|
||||
it('sends you a notification', async () => {
|
||||
const expectedContent =
|
||||
'Hey <a href="/profile/you/al-capone" target="_blank">@al-capone</a> how do you do?'
|
||||
'Hey <a class="mention" data-mention-id="you" href="/profile/you/al-capone" target="_blank">@al-capone</a> how do you do?'
|
||||
const expected = {
|
||||
currentUser: {
|
||||
notifications: [
|
||||
@ -108,14 +108,22 @@ describe('currentUser { notifications }', () => {
|
||||
).resolves.toEqual(expected)
|
||||
})
|
||||
|
||||
describe('who mentions me again', () => {
|
||||
describe('who mentions me many times', () => {
|
||||
beforeEach(async () => {
|
||||
const updatedContent = `${post.content} One more mention to <a href="/profile/you" class="mention">@al-capone</a>`
|
||||
// The response `post.content` contains a link but the XSSmiddleware
|
||||
// should have the `mention` CSS class removed. I discovered this
|
||||
// during development and thought: A feature not a bug! This way we
|
||||
// can encode a re-mentioning of users when you edit your post or
|
||||
// comment.
|
||||
const updatedContent = `
|
||||
One more mention to
|
||||
<a data-mention-id="you" class="mention" href="/profile/you">
|
||||
@al-capone
|
||||
</a>
|
||||
and again:
|
||||
<a data-mention-id="you" class="mention" href="/profile/you">
|
||||
@al-capone
|
||||
</a>
|
||||
and again
|
||||
<a data-mention-id="you" class="mention" href="/profile/you">
|
||||
@al-capone
|
||||
</a>
|
||||
`
|
||||
const updatePostMutation = gql`
|
||||
mutation($id: ID!, $title: String!, $content: String!) {
|
||||
UpdatePost(id: $id, content: $content, title: $title) {
|
||||
@ -136,7 +144,7 @@ describe('currentUser { notifications }', () => {
|
||||
|
||||
it('creates exactly one more notification', async () => {
|
||||
const expectedContent =
|
||||
'Hey <a href="/profile/you/al-capone" target="_blank">@al-capone</a> how do you do? One more mention to <a href="/profile/you" target="_blank">@al-capone</a>'
|
||||
'<br>One more mention to<br><a data-mention-id="you" class="mention" href="/profile/you" target="_blank"><br>@al-capone<br></a><br>and again:<br><a data-mention-id="you" class="mention" href="/profile/you" target="_blank"><br>@al-capone<br></a><br>and again<br><a data-mention-id="you" class="mention" href="/profile/you" target="_blank"><br>@al-capone<br></a><br>'
|
||||
const expected = {
|
||||
currentUser: {
|
||||
notifications: [
|
||||
|
||||
@ -1,20 +1,13 @@
|
||||
import cheerio from 'cheerio'
|
||||
const ID_REGEX = /\/profile\/([\w\-.!~*'"(),]+)/g
|
||||
|
||||
export default function(content) {
|
||||
if (!content) return []
|
||||
const $ = cheerio.load(content)
|
||||
const urls = $('.mention')
|
||||
let userIds = $('a.mention[data-mention-id]')
|
||||
.map((_, el) => {
|
||||
return $(el).attr('href')
|
||||
return $(el).attr('data-mention-id')
|
||||
})
|
||||
.get()
|
||||
const ids = []
|
||||
urls.forEach(url => {
|
||||
let match
|
||||
while ((match = ID_REGEX.exec(url)) != null) {
|
||||
ids.push(match[1])
|
||||
}
|
||||
})
|
||||
return ids
|
||||
userIds = userIds.map(id => id.trim()).filter(id => !!id)
|
||||
return userIds
|
||||
}
|
||||
|
||||
@ -1,5 +1,12 @@
|
||||
import extractMentionedUsers from './extractMentionedUsers'
|
||||
|
||||
const contentWithMentions =
|
||||
'<p>Something inspirational about <a href="/profile/u2" class="not-a-mention" data-mention-id="bobs-id" target="_blank">@bob-der-baumeister</a> and <a href="/profile/u3/jenny-rostock" class="mention" data-mention-id="u3" target="_blank">@jenny-rostock</a>.</p>'
|
||||
const contentEmptyMentions =
|
||||
'<p>Something inspirational about <a href="/profile/u2" data-mention-id="" target="_blank">@bob-der-baumeister</a> and <a href="/profile/u3/jenny-rostock" class="mention" data-mention-id target="_blank">@jenny-rostock</a>.</p>'
|
||||
const contentWithPlainLinks =
|
||||
'<p>Something inspirational about <a class="mention" href="/profile/u2" target="_blank">@bob-der-baumeister</a> and <a href="/profile/u3" target="_blank">@jenny-rostock</a>.</p>'
|
||||
|
||||
describe('extractMentionedUsers', () => {
|
||||
describe('content undefined', () => {
|
||||
it('returns empty array', () => {
|
||||
@ -7,53 +14,17 @@ describe('extractMentionedUsers', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('searches through links', () => {
|
||||
it('ignores links without .mention class', () => {
|
||||
const content =
|
||||
'<p>Something inspirational about <a href="/profile/u2" target="_blank">@bob-der-baumeister</a> and <a href="/profile/u3" target="_blank">@jenny-rostock</a>.</p>'
|
||||
expect(extractMentionedUsers(content)).toEqual([])
|
||||
it('ignores links without .mention class', () => {
|
||||
expect(extractMentionedUsers(contentWithPlainLinks)).toEqual([])
|
||||
})
|
||||
|
||||
describe('given a link with .mention class and `data-mention-id` attribute ', () => {
|
||||
it('extracts ids', () => {
|
||||
expect(extractMentionedUsers(contentWithMentions)).toEqual(['u3'])
|
||||
})
|
||||
|
||||
describe('given a link with .mention class', () => {
|
||||
it('extracts ids', () => {
|
||||
const content =
|
||||
'<p>Something inspirational about <a href="/profile/u2" class="mention" target="_blank">@bob-der-baumeister</a> and <a href="/profile/u3/jenny-rostock" class="mention" target="_blank">@jenny-rostock</a>.</p>'
|
||||
expect(extractMentionedUsers(content)).toEqual(['u2', 'u3'])
|
||||
})
|
||||
|
||||
describe('handles links', () => {
|
||||
it('with slug and id', () => {
|
||||
const content =
|
||||
'<p>Something inspirational about <a href="/profile/u2/bob-der-baumeister" class="mention" target="_blank">@bob-der-baumeister</a> and <a href="/profile/u3/jenny-rostock/" class="mention" target="_blank">@jenny-rostock</a>.</p>'
|
||||
expect(extractMentionedUsers(content)).toEqual(['u2', 'u3'])
|
||||
})
|
||||
|
||||
it('with domains', () => {
|
||||
const content =
|
||||
'<p>Something inspirational about <a href="http://localhost:3000/profile/u2/bob-der-baumeister" class="mention" target="_blank">@bob-der-baumeister</a> and <a href="http://localhost:3000//profile/u3/jenny-rostock/" class="mention" target="_blank">@jenny-rostock</a>.</p>'
|
||||
expect(extractMentionedUsers(content)).toEqual(['u2', 'u3'])
|
||||
})
|
||||
|
||||
it('special characters', () => {
|
||||
const content =
|
||||
'<p>Something inspirational about <a href="http://localhost:3000/profile/u!*(),2/bob-der-baumeister" class="mention" target="_blank">@bob-der-baumeister</a> and <a href="http://localhost:3000//profile/u.~-3/jenny-rostock/" class="mention" target="_blank">@jenny-rostock</a>.</p>'
|
||||
expect(extractMentionedUsers(content)).toEqual(['u!*(),2', 'u.~-3'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('does not crash if', () => {
|
||||
it('`href` contains no user id', () => {
|
||||
const content =
|
||||
'<p>Something inspirational about <a href="/profile" class="mention" target="_blank">@bob-der-baumeister</a> and <a href="/profile/" class="mention" target="_blank">@jenny-rostock</a>.</p>'
|
||||
expect(extractMentionedUsers(content)).toEqual([])
|
||||
})
|
||||
|
||||
it('`href` is empty or invalid', () => {
|
||||
const content =
|
||||
'<p>Something inspirational about <a href="" class="mention" target="_blank">@bob-der-baumeister</a> and <a href="not-a-url" class="mention" target="_blank">@jenny-rostock</a>.</p>'
|
||||
expect(extractMentionedUsers(content)).toEqual([])
|
||||
})
|
||||
})
|
||||
it('ignores empty `data-mention-id` attributes', () => {
|
||||
expect(extractMentionedUsers(contentEmptyMentions)).toEqual([])
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -2,30 +2,16 @@ import walkRecursive from '../helpers/walkRecursive'
|
||||
// import { getByDot, setByDot, getItems, replaceItems } from 'feathers-hooks-common'
|
||||
import sanitizeHtml from 'sanitize-html'
|
||||
// import { isEmpty, intersection } from 'lodash'
|
||||
import cheerio from 'cheerio'
|
||||
import linkifyHtml from 'linkifyjs/html'
|
||||
|
||||
const embedToAnchor = content => {
|
||||
const $ = cheerio.load(content)
|
||||
$('div[data-url-embed]').each((i, el) => {
|
||||
const url = el.attribs['data-url-embed']
|
||||
const aTag = $(`<a href="${url}" target="_blank" data-url-embed="">${url}</a>`)
|
||||
$(el).replaceWith(aTag)
|
||||
})
|
||||
return $('body').html()
|
||||
}
|
||||
|
||||
function clean(dirty) {
|
||||
if (!dirty) {
|
||||
return dirty
|
||||
}
|
||||
|
||||
// Convert embeds to a-tags
|
||||
dirty = embedToAnchor(dirty)
|
||||
dirty = linkifyHtml(dirty)
|
||||
dirty = sanitizeHtml(dirty, {
|
||||
allowedTags: [
|
||||
'iframe',
|
||||
'img',
|
||||
'p',
|
||||
'h3',
|
||||
@ -50,35 +36,24 @@ function clean(dirty) {
|
||||
a: ['href', 'class', 'target', 'data-*', 'contenteditable'],
|
||||
span: ['contenteditable', 'class', 'data-*'],
|
||||
img: ['src'],
|
||||
iframe: ['src', 'class', 'frameborder', 'allowfullscreen'],
|
||||
},
|
||||
allowedIframeHostnames: ['www.youtube.com', 'player.vimeo.com'],
|
||||
parser: {
|
||||
lowerCaseTags: true,
|
||||
},
|
||||
transformTags: {
|
||||
iframe: function(tagName, attribs) {
|
||||
return {
|
||||
tagName: 'a',
|
||||
text: attribs.src,
|
||||
attribs: {
|
||||
href: attribs.src,
|
||||
target: '_blank',
|
||||
'data-url-embed': '',
|
||||
},
|
||||
}
|
||||
},
|
||||
h1: 'h3',
|
||||
h2: 'h3',
|
||||
h3: 'h3',
|
||||
h4: 'h4',
|
||||
h5: 'strong',
|
||||
i: 'em',
|
||||
a: function(tagName, attribs) {
|
||||
a: (tagName, attribs) => {
|
||||
return {
|
||||
tagName: 'a',
|
||||
attribs: {
|
||||
href: attribs.href,
|
||||
...attribs,
|
||||
href: attribs.href || '',
|
||||
target: '_blank',
|
||||
rel: 'noopener noreferrer nofollow',
|
||||
},
|
||||
@ -86,33 +61,6 @@ function clean(dirty) {
|
||||
},
|
||||
b: 'strong',
|
||||
s: 'strike',
|
||||
img: function(tagName, attribs) {
|
||||
const src = attribs.src
|
||||
|
||||
if (!src) {
|
||||
// remove broken images
|
||||
return {}
|
||||
}
|
||||
|
||||
// if (isEmpty(hook.result)) {
|
||||
// const config = hook.app.get('thumbor')
|
||||
// if (config && src.indexOf(config < 0)) {
|
||||
// // download image
|
||||
// // const ThumborUrlHelper = require('../helper/thumbor-helper')
|
||||
// // const Thumbor = new ThumborUrlHelper(config.key || null, config.url || null)
|
||||
// // src = Thumbor
|
||||
// // .setImagePath(src)
|
||||
// // .buildUrl('740x0')
|
||||
// }
|
||||
// }
|
||||
return {
|
||||
tagName: 'img',
|
||||
attribs: {
|
||||
// TODO: use environment variables
|
||||
src: `http://localhost:3050/images?url=${src}`,
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
@ -120,8 +68,6 @@ function clean(dirty) {
|
||||
dirty = dirty
|
||||
// remove all tags with "space only"
|
||||
.replace(/<[a-z-]+>[\s]+<\/[a-z-]+>/gim, '')
|
||||
// remove all iframes
|
||||
.replace(/(<iframe(?!.*?src=(['"]).*?\2)[^>]*)(>)[^>]*\/*>/gim, '')
|
||||
.replace(/[\n]{3,}/gim, '\n\n')
|
||||
.replace(/(\r\n|\n\r|\r|\n)/g, '<br>$1')
|
||||
|
||||
@ -144,8 +90,7 @@ const fields = ['content', 'contentExcerpt']
|
||||
export default {
|
||||
Mutation: async (resolve, root, args, context, info) => {
|
||||
args = walkRecursive(args, fields, clean)
|
||||
const result = await resolve(root, args, context, info)
|
||||
return result
|
||||
return resolve(root, args, context, info)
|
||||
},
|
||||
Query: async (resolve, root, args, context, info) => {
|
||||
const result = await resolve(root, args, context, info)
|
||||
|
||||
26
backend/src/schema/resolvers/embeds/findProvider.js
Normal file
26
backend/src/schema/resolvers/embeds/findProvider.js
Normal file
@ -0,0 +1,26 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import minimatch from 'minimatch'
|
||||
|
||||
let oEmbedProvidersFile = fs.readFileSync(path.join(__dirname, './providers.json'), 'utf8')
|
||||
// some providers allow a format parameter
|
||||
// we need JSON
|
||||
oEmbedProvidersFile = oEmbedProvidersFile.replace(/\{format\}/g, 'json')
|
||||
const oEmbedProviders = JSON.parse(oEmbedProvidersFile)
|
||||
|
||||
export default function(embedUrl) {
|
||||
for (const provider of oEmbedProviders) {
|
||||
for (const endpoint of provider.endpoints) {
|
||||
const { schemes = [], url } = endpoint
|
||||
if (schemes.some(scheme => minimatch(embedUrl, scheme))) return url
|
||||
}
|
||||
const { hostname } = new URL(embedUrl)
|
||||
if (provider.provider_url.includes(hostname)) {
|
||||
const {
|
||||
endpoints: [{ url }],
|
||||
} = provider
|
||||
return url
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
35
backend/src/schema/resolvers/embeds/findProvider.spec.js
Normal file
35
backend/src/schema/resolvers/embeds/findProvider.spec.js
Normal file
@ -0,0 +1,35 @@
|
||||
import findProvider from './findProvider'
|
||||
|
||||
describe('Vimeo', () => {
|
||||
it('matches `https://vimeo.com/showcase/2098620/video/4082288`', () => {
|
||||
expect(findProvider('https://vimeo.com/showcase/2098620/video/4082288')).toEqual(
|
||||
'https://vimeo.com/api/oembed.json',
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('RiffReporter', () => {
|
||||
it('matches `https://www.riffreporter.de/flugbegleiter-koralle/`', () => {
|
||||
expect(findProvider('https://www.riffreporter.de/flugbegleiter-koralle/')).toEqual(
|
||||
'https://www.riffreporter.de/service/oembed',
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Youtube', () => {
|
||||
it('matches `https://www.youtube.com/watch?v=qkdXAtO40Fo`', () => {
|
||||
expect(findProvider('https://www.youtube.com/watch?v=qkdXAtO40Fo')).toEqual(
|
||||
'https://www.youtube.com/oembed',
|
||||
)
|
||||
})
|
||||
|
||||
it('matches `https://youtu.be/qkdXAtO40Fo`', () => {
|
||||
expect(findProvider(`https://youtu.be/qkdXAtO40Fo`)).toEqual('https://www.youtube.com/oembed')
|
||||
})
|
||||
|
||||
it('matches `https://youtu.be/qkdXAtO40Fo?t=41`', () => {
|
||||
expect(findProvider(`https://youtu.be/qkdXAtO40Fo?t=41`)).toEqual(
|
||||
'https://www.youtube.com/oembed',
|
||||
)
|
||||
})
|
||||
})
|
||||
@ -1,12 +1,11 @@
|
||||
import Metascraper from 'metascraper'
|
||||
import fetch from 'node-fetch'
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
|
||||
import { ApolloError } from 'apollo-server'
|
||||
import isEmpty from 'lodash/isEmpty'
|
||||
import isArray from 'lodash/isArray'
|
||||
import mergeWith from 'lodash/mergeWith'
|
||||
import findProvider from './findProvider'
|
||||
|
||||
const error = require('debug')('embed:error')
|
||||
|
||||
@ -30,24 +29,11 @@ const metascraper = Metascraper([
|
||||
// require('./rules/metascraper-embed')()
|
||||
])
|
||||
|
||||
let oEmbedProvidersFile = fs.readFileSync(path.join(__dirname, './providers.json'), 'utf8')
|
||||
|
||||
// some providers allow a format parameter
|
||||
// we need JSON
|
||||
oEmbedProvidersFile = oEmbedProvidersFile.replace('{format}', 'json')
|
||||
|
||||
const oEmbedProviders = JSON.parse(oEmbedProvidersFile)
|
||||
|
||||
const fetchEmbed = async url => {
|
||||
const provider = oEmbedProviders.find(provider => {
|
||||
return provider.provider_url.includes(url.hostname)
|
||||
})
|
||||
if (!provider) return {}
|
||||
const {
|
||||
endpoints: [endpoint],
|
||||
} = provider
|
||||
const endpointUrl = new URL(endpoint.url)
|
||||
endpointUrl.searchParams.append('url', url.href)
|
||||
let endpointUrl = findProvider(url)
|
||||
if (!endpointUrl) return {}
|
||||
endpointUrl = new URL(endpointUrl)
|
||||
endpointUrl.searchParams.append('url', url)
|
||||
endpointUrl.searchParams.append('format', 'json')
|
||||
let json
|
||||
try {
|
||||
@ -70,7 +56,7 @@ const fetchEmbed = async url => {
|
||||
const fetchResource = async url => {
|
||||
const response = await fetch(url)
|
||||
const html = await response.text()
|
||||
const resource = await metascraper({ html, url: url.href })
|
||||
const resource = await metascraper({ html, url })
|
||||
return {
|
||||
sources: ['resource'],
|
||||
...resource,
|
||||
@ -78,12 +64,6 @@ const fetchResource = async url => {
|
||||
}
|
||||
|
||||
export default async function scrape(url) {
|
||||
url = new URL(url)
|
||||
if (url.hostname === 'youtu.be') {
|
||||
// replace youtu.be to get proper results
|
||||
url.hostname = 'youtube.com'
|
||||
}
|
||||
|
||||
const [meta, embed] = await Promise.all([fetchResource(url), fetchEmbed(url)])
|
||||
const output = mergeWith(meta, embed, (objValue, srcValue) => {
|
||||
if (isArray(objValue)) {
|
||||
|
||||
@ -7,4 +7,4 @@ type Tag {
|
||||
taggedCountUnique: Int! @cypher(statement: "MATCH (this)<-[:TAGGED]-(p)<-[:WROTE]-(u:User) RETURN COUNT(DISTINCT u)")
|
||||
deleted: Boolean
|
||||
disabled: Boolean
|
||||
}
|
||||
}
|
||||
|
||||
@ -5828,12 +5828,12 @@ metascraper-lang-detector@^4.8.5:
|
||||
franc "~4.0.0"
|
||||
iso-639-3 "~1.1.0"
|
||||
|
||||
metascraper-lang@^5.6.3:
|
||||
version "5.6.3"
|
||||
resolved "https://registry.yarnpkg.com/metascraper-lang/-/metascraper-lang-5.6.3.tgz#d2d7a20f4145b0785391fffec629e154737fc942"
|
||||
integrity sha512-c13zxmREcB/hDXs7MIxio7RNfVsSzGfixk6FrfQQh3fypmiR84SpeZmQR+G/e2X/BDNwpIydJM62R7BayY709Q==
|
||||
metascraper-lang@^5.6.5:
|
||||
version "5.6.5"
|
||||
resolved "https://registry.yarnpkg.com/metascraper-lang/-/metascraper-lang-5.6.5.tgz#0363514de4bb580f8e571502e7421b2345cd54bf"
|
||||
integrity sha512-mp9fVytxX9eLGNI16CYGEhBXhG3a0/RtjVqtcngSyeVZl7SKxMyGLrGqDgCuoU6Til/n11ujEnu3wlQfbGclIg==
|
||||
dependencies:
|
||||
"@metascraper/helpers" "^5.6.3"
|
||||
"@metascraper/helpers" "^5.6.5"
|
||||
|
||||
metascraper-logo@^5.6.5:
|
||||
version "5.6.5"
|
||||
|
||||
@ -230,7 +230,7 @@ When("I choose {string} as the title of the post", title => {
|
||||
|
||||
When("I type in the following text:", text => {
|
||||
lastPost.content = text.replace("\n", " ");
|
||||
cy.get(".ProseMirror").type(lastPost.content);
|
||||
cy.get(".editor .ProseMirror").type(lastPost.content);
|
||||
});
|
||||
|
||||
Then("the post shows up on the landing page at position {int}", index => {
|
||||
|
||||
@ -30,7 +30,7 @@
|
||||
"faker": "Marak/faker.js#master",
|
||||
"graphql-request": "^1.8.2",
|
||||
"neo4j-driver": "^1.7.5",
|
||||
"neode": "^0.3.0",
|
||||
"neode": "^0.3.1",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"slug": "^1.1.0"
|
||||
}
|
||||
|
||||
@ -45,7 +45,11 @@
|
||||
{{ $t('comment.show.more') }}
|
||||
</a>
|
||||
</div>
|
||||
<div v-if="!isCollapsed" v-html="comment.content" style="padding-left: 40px;" />
|
||||
<content-viewer
|
||||
v-if="!isCollapsed"
|
||||
:content="comment.content"
|
||||
style="padding-left: 40px;"
|
||||
/>
|
||||
<div style="text-align: right; margin-right: 20px; margin-top: -12px;">
|
||||
<a v-if="!isCollapsed" @click="isCollapsed = !isCollapsed" style="padding-left: 40px; ">
|
||||
{{ $t('comment.show.less') }}
|
||||
@ -62,6 +66,7 @@ import gql from 'graphql-tag'
|
||||
import { mapGetters, mapMutations } from 'vuex'
|
||||
import HcUser from '~/components/User'
|
||||
import ContentMenu from '~/components/ContentMenu'
|
||||
import ContentViewer from '~/components/Editor/ContentViewer'
|
||||
import HcEditCommentForm from '~/components/comments/EditCommentForm/EditCommentForm'
|
||||
|
||||
export default {
|
||||
@ -74,6 +79,7 @@ export default {
|
||||
components: {
|
||||
HcUser,
|
||||
ContentMenu,
|
||||
ContentViewer,
|
||||
HcEditCommentForm,
|
||||
},
|
||||
props: {
|
||||
|
||||
32
webapp/components/Editor/ContentViewer.vue
Normal file
32
webapp/components/Editor/ContentViewer.vue
Normal file
@ -0,0 +1,32 @@
|
||||
<template>
|
||||
<editor-content :editor="editor" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import defaultExtensions from './defaultExtensions.js'
|
||||
import { Editor, EditorContent } from 'tiptap'
|
||||
|
||||
export default {
|
||||
name: 'ContentViewer',
|
||||
components: {
|
||||
EditorContent,
|
||||
},
|
||||
props: {
|
||||
content: { type: String, default: '' },
|
||||
doc: { type: Object, default: () => {} },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
editor: new Editor({
|
||||
doc: this.doc,
|
||||
content: this.content,
|
||||
editable: false,
|
||||
extensions: defaultExtensions(this),
|
||||
}),
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.editor.destroy()
|
||||
},
|
||||
}
|
||||
</script>
|
||||
132
webapp/components/Editor/Editor.story.js
Normal file
132
webapp/components/Editor/Editor.story.js
Normal file
@ -0,0 +1,132 @@
|
||||
import { storiesOf } from '@storybook/vue'
|
||||
import { withA11y } from '@storybook/addon-a11y'
|
||||
import HcEditor from '~/components/Editor/Editor.vue'
|
||||
import helpers from '~/storybook/helpers'
|
||||
import Vue from 'vue'
|
||||
|
||||
const embed = {
|
||||
html:
|
||||
'<iframe width="480" height="270" src="https://www.youtube.com/embed/qkdXAtO40Fo?feature=oembed" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>',
|
||||
}
|
||||
|
||||
const plugins = [
|
||||
(app = {}) => {
|
||||
app.$apollo = {
|
||||
mutate: () => {},
|
||||
query: () => {
|
||||
return { data: { embed } }
|
||||
},
|
||||
}
|
||||
Vue.prototype.$apollo = app.$apollo
|
||||
return app
|
||||
},
|
||||
]
|
||||
helpers.init({ plugins })
|
||||
|
||||
const users = [{ id: 1, slug: 'peter' }, { id: 2, slug: 'sandra' }, { id: 3, slug: 'jane' }]
|
||||
|
||||
storiesOf('Editor', module)
|
||||
.addDecorator(withA11y)
|
||||
.addDecorator(storyFn => {
|
||||
const ctx = storyFn()
|
||||
return {
|
||||
components: { ctx },
|
||||
template: `
|
||||
<ds-card style="width: 50%; min-width: 500px; margin: 0 auto;">
|
||||
<ctx />
|
||||
</ds-card>
|
||||
`,
|
||||
}
|
||||
})
|
||||
.addDecorator(helpers.layout)
|
||||
.add('Empty', () => ({
|
||||
components: { HcEditor },
|
||||
store: helpers.store,
|
||||
data: () => ({
|
||||
users,
|
||||
}),
|
||||
template: `<hc-editor :users="users" />`,
|
||||
}))
|
||||
.add('Basic formatting', () => ({
|
||||
components: { HcEditor },
|
||||
store: helpers.store,
|
||||
data: () => ({
|
||||
users,
|
||||
content: `
|
||||
<h3>Basic formatting</h3>
|
||||
<p>
|
||||
Here is some <em>italic</em>, <b>bold</b> and <u>underline</u> text.
|
||||
<br/>
|
||||
Also do we have some <a href="https://human-connection.org">inline links</a> here.
|
||||
</p>
|
||||
<h3>Heading 3</h3>
|
||||
<p>At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.</p>
|
||||
<h4>Heading 4</h4>
|
||||
<p>At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.</p>
|
||||
<h5>Heading 5</h5>
|
||||
<p>At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.</p>
|
||||
|
||||
<h3>Unordered List</h3>
|
||||
<ul>
|
||||
<li><p>Also some list</p></li>
|
||||
<li><p>with</p></li>
|
||||
<li><p>several</p></li>
|
||||
<li><p>points</p></li>
|
||||
</ul>
|
||||
|
||||
<h3>Ordered List</h3>
|
||||
<ol>
|
||||
<li><p>justo</p></li>
|
||||
<li><p>dolores</p></li>
|
||||
<li><p>et ea rebum</p></li>
|
||||
<li><p>kasd gubergren</p></li>
|
||||
</ol>
|
||||
`,
|
||||
}),
|
||||
template: `<hc-editor :users="users" :value="content" />`,
|
||||
}))
|
||||
.add('@Mentions', () => ({
|
||||
components: { HcEditor },
|
||||
store: helpers.store,
|
||||
data: () => ({
|
||||
users,
|
||||
content: `
|
||||
<p>
|
||||
Here you can mention people like
|
||||
<a class="mention" data-mention-id="2" href="/profile/1" target="_blank" contenteditable="false">@sandra</a> and others.
|
||||
Try it out!
|
||||
</p>
|
||||
`,
|
||||
}),
|
||||
template: `<hc-editor :users="users" :value="content" />`,
|
||||
}))
|
||||
.add('#Hashtags', () => ({
|
||||
components: { HcEditor },
|
||||
store: helpers.store,
|
||||
data: () => ({
|
||||
users,
|
||||
content: `
|
||||
<p>
|
||||
This text contains <a href="#" class="hashtag">#hashtags</a> for projects like <a href="https://human-connection.org" class="hashtag">#human-connection</a>
|
||||
Try to add more by typing #.
|
||||
</p>
|
||||
`,
|
||||
}),
|
||||
template: `<hc-editor :users="users" :value="content" />`,
|
||||
}))
|
||||
.add('Embeds', () => ({
|
||||
components: { HcEditor },
|
||||
store: helpers.store,
|
||||
data: () => ({
|
||||
users,
|
||||
content: `
|
||||
<p>
|
||||
The following link should render a youtube video in addition to the link.
|
||||
</p>
|
||||
<a class="embed" href="https://www.youtube.com/watch?v=qkdXAtO40Fo">
|
||||
<em>https://www.youtube.com/watch?v=qkdXAtO40Fo</em>
|
||||
</a>
|
||||
`,
|
||||
}),
|
||||
template: `<hc-editor :users="users" :value="content" />`,
|
||||
}))
|
||||
@ -183,30 +183,16 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import defaultExtensions from './defaultExtensions.js'
|
||||
import linkify from 'linkify-it'
|
||||
import stringHash from 'string-hash'
|
||||
import Fuse from 'fuse.js'
|
||||
import tippy from 'tippy.js'
|
||||
import { Editor, EditorContent, EditorFloatingMenu, EditorMenuBubble } from 'tiptap'
|
||||
import EventHandler from './plugins/eventHandler.js'
|
||||
import {
|
||||
Heading,
|
||||
HardBreak,
|
||||
Blockquote,
|
||||
ListItem,
|
||||
BulletList,
|
||||
OrderedList,
|
||||
HorizontalRule,
|
||||
Placeholder,
|
||||
Bold,
|
||||
Italic,
|
||||
Strike,
|
||||
Underline,
|
||||
Link,
|
||||
History,
|
||||
} from 'tiptap-extensions'
|
||||
import Mention from './nodes/Mention.js'
|
||||
import { History } from 'tiptap-extensions'
|
||||
import Hashtag from './nodes/Hashtag.js'
|
||||
import Mention from './nodes/Mention.js'
|
||||
import { mapGetters } from 'vuex'
|
||||
|
||||
let throttleInputEvent
|
||||
@ -230,24 +216,8 @@ export default {
|
||||
content: this.value || '',
|
||||
doc: this.doc,
|
||||
extensions: [
|
||||
...defaultExtensions(this),
|
||||
new EventHandler(),
|
||||
new Heading(),
|
||||
new HardBreak(),
|
||||
new Blockquote(),
|
||||
new BulletList(),
|
||||
new OrderedList(),
|
||||
new HorizontalRule(),
|
||||
new Bold(),
|
||||
new Italic(),
|
||||
new Strike(),
|
||||
new Underline(),
|
||||
new Link(),
|
||||
new Heading({ levels: [3, 4] }),
|
||||
new ListItem(),
|
||||
new Placeholder({
|
||||
emptyNodeClass: 'is-empty',
|
||||
emptyNodeText: this.placeholder || this.$t('editor.placeholder'),
|
||||
}),
|
||||
new History(),
|
||||
new Mention({
|
||||
// a list of all suggested items
|
||||
@ -493,13 +463,11 @@ export default {
|
||||
selectItem(item) {
|
||||
const typeAttrs = {
|
||||
mention: {
|
||||
// TODO: use router here
|
||||
url: `/profile/${item.id}`,
|
||||
id: item.id,
|
||||
label: item.slug,
|
||||
},
|
||||
hashtag: {
|
||||
// TODO: Fill up with input hashtag in search field
|
||||
url: `/search/hashtag/${item.name}`,
|
||||
id: item.name,
|
||||
label: item.name,
|
||||
},
|
||||
}
|
||||
|
||||
49
webapp/components/Editor/commands/pasteRule.js
Normal file
49
webapp/components/Editor/commands/pasteRule.js
Normal file
@ -0,0 +1,49 @@
|
||||
import { Plugin } from 'prosemirror-state'
|
||||
import { Slice, Fragment } from 'prosemirror-model'
|
||||
|
||||
export default function(regexp, type, getAttrs) {
|
||||
const handler = fragment => {
|
||||
const nodes = []
|
||||
|
||||
fragment.forEach(child => {
|
||||
if (child.isText) {
|
||||
const { text } = child
|
||||
let pos = 0
|
||||
let match
|
||||
|
||||
do {
|
||||
match = regexp.exec(text)
|
||||
if (match) {
|
||||
const start = match.index
|
||||
const end = start + match[0].length
|
||||
const attrs = getAttrs instanceof Function ? getAttrs(match[0]) : getAttrs
|
||||
|
||||
if (start > 0) {
|
||||
nodes.push(child.cut(pos, start))
|
||||
}
|
||||
|
||||
// only difference to `pasteRule` of `tiptap-commands`:
|
||||
// we replace the node instead of adding markup
|
||||
nodes.push(type.create(attrs, child.cut(start, end)))
|
||||
|
||||
pos = end
|
||||
}
|
||||
} while (match)
|
||||
|
||||
if (pos < text.length) {
|
||||
nodes.push(child.cut(pos))
|
||||
}
|
||||
} else {
|
||||
nodes.push(child.copy(handler(child.content)))
|
||||
}
|
||||
})
|
||||
|
||||
return Fragment.fromArray(nodes)
|
||||
}
|
||||
|
||||
return new Plugin({
|
||||
props: {
|
||||
transformPasted: slice => new Slice(handler(slice.content), slice.openStart, slice.openEnd),
|
||||
},
|
||||
})
|
||||
}
|
||||
48
webapp/components/Editor/defaultExtensions.js
Normal file
48
webapp/components/Editor/defaultExtensions.js
Normal file
@ -0,0 +1,48 @@
|
||||
import Embed from '~/components/Editor/nodes/Embed.js'
|
||||
import Link from '~/components/Editor/nodes/Link.js'
|
||||
import Strike from '~/components/Editor/marks/Strike'
|
||||
import Italic from '~/components/Editor/marks/Italic'
|
||||
import Bold from '~/components/Editor/marks/Bold'
|
||||
import EmbedQuery from '~/graphql/EmbedQuery.js'
|
||||
import {
|
||||
Heading,
|
||||
HardBreak,
|
||||
Blockquote,
|
||||
ListItem,
|
||||
BulletList,
|
||||
OrderedList,
|
||||
HorizontalRule,
|
||||
Placeholder,
|
||||
Underline,
|
||||
} from 'tiptap-extensions'
|
||||
|
||||
export default function defaultExtensions(component) {
|
||||
const { placeholder, $t, $apollo } = component
|
||||
return [
|
||||
new Heading(),
|
||||
new HardBreak(),
|
||||
new Blockquote(),
|
||||
new BulletList(),
|
||||
new OrderedList(),
|
||||
new HorizontalRule(),
|
||||
new Bold(),
|
||||
new Italic(),
|
||||
new Strike(),
|
||||
new Underline(),
|
||||
new Link(),
|
||||
new Heading({ levels: [3, 4] }),
|
||||
new ListItem(),
|
||||
new Placeholder({
|
||||
emptyNodeClass: 'is-empty',
|
||||
emptyNodeText: placeholder || $t('editor.placeholder'),
|
||||
}),
|
||||
new Embed({
|
||||
onEmbed: async ({ url }) => {
|
||||
const {
|
||||
data: { embed },
|
||||
} = await $apollo.query({ query: EmbedQuery(), variables: { url } })
|
||||
return embed
|
||||
},
|
||||
}),
|
||||
]
|
||||
}
|
||||
93
webapp/components/Editor/defaultExtensions.spec.js
Normal file
93
webapp/components/Editor/defaultExtensions.spec.js
Normal file
@ -0,0 +1,93 @@
|
||||
import defaultExtensions from './defaultExtensions.js'
|
||||
import { Editor } from 'tiptap'
|
||||
|
||||
let content
|
||||
let createEditor
|
||||
|
||||
describe('defaultExtensions', () => {
|
||||
describe('editor', () => {
|
||||
createEditor = () => {
|
||||
const componentStub = {
|
||||
placeholder: 'placeholder',
|
||||
$t: jest.fn(),
|
||||
$apollo: {
|
||||
mutate: jest.fn(),
|
||||
},
|
||||
}
|
||||
return new Editor({
|
||||
content,
|
||||
extensions: [...defaultExtensions(componentStub)],
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
it('renders', () => {
|
||||
content = ''
|
||||
expect(createEditor().getHTML()).toEqual('<p></p>')
|
||||
})
|
||||
|
||||
describe('`content` contains a mentioning', () => {
|
||||
beforeEach(() => {
|
||||
content =
|
||||
'<p>This is a post content mentioning <a class="mention" data-mention-id="alicias-id" href="/profile/f0628376-e692-4167-bdb4-d521de5a014f" target="_blank">@alicia-luettgen</a>.</p>'
|
||||
})
|
||||
|
||||
it('renders mentioning as link', () => {
|
||||
const editor = createEditor()
|
||||
const expected =
|
||||
'<p>This is a post content mentioning <a href="/profile/f0628376-e692-4167-bdb4-d521de5a014f" rel="noopener noreferrer nofollow">@alicia-luettgen</a>.</p>'
|
||||
expect(editor.getHTML()).toEqual(expected)
|
||||
})
|
||||
})
|
||||
|
||||
describe('`content` contains a hashtag', () => {
|
||||
beforeEach(() => {
|
||||
content =
|
||||
'<p>This is a post content with a hashtag <a class="hashtag" href="/search/hashtag/metoo" target="_blank">#metoo</a>.</p>'
|
||||
})
|
||||
|
||||
it('renders hashtag as link', () => {
|
||||
const editor = createEditor()
|
||||
const expected =
|
||||
'<p>This is a post content with a hashtag <a href="/search/hashtag/metoo" rel="noopener noreferrer nofollow">#metoo</a>.</p>'
|
||||
expect(editor.getHTML()).toEqual(expected)
|
||||
})
|
||||
})
|
||||
|
||||
describe('`content` contains embed code', () => {
|
||||
beforeEach(() => {
|
||||
content =
|
||||
'<p>Baby loves cat: </p><a href="https://www.youtube.com/watch?v=qkdXAtO40Fo" class="embed" target="_blank"></a>'
|
||||
})
|
||||
|
||||
it('recognizes embed code', () => {
|
||||
const editor = createEditor()
|
||||
const expected = {
|
||||
content: [
|
||||
{
|
||||
content: [
|
||||
{
|
||||
text: 'Baby loves cat:',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
type: 'paragraph',
|
||||
},
|
||||
{
|
||||
content: [
|
||||
{
|
||||
attrs: {
|
||||
dataEmbedUrl: 'https://www.youtube.com/watch?v=qkdXAtO40Fo',
|
||||
},
|
||||
type: 'embed',
|
||||
},
|
||||
],
|
||||
type: 'paragraph',
|
||||
},
|
||||
],
|
||||
type: 'doc',
|
||||
}
|
||||
expect(editor.getJSON()).toEqual(expected)
|
||||
})
|
||||
})
|
||||
})
|
||||
7
webapp/components/Editor/marks/Bold.js
Normal file
7
webapp/components/Editor/marks/Bold.js
Normal file
@ -0,0 +1,7 @@
|
||||
import { Bold as TipTapBold } from 'tiptap-extensions'
|
||||
|
||||
export default class Bold extends TipTapBold {
|
||||
pasteRules() {
|
||||
return []
|
||||
}
|
||||
}
|
||||
7
webapp/components/Editor/marks/Italic.js
Normal file
7
webapp/components/Editor/marks/Italic.js
Normal file
@ -0,0 +1,7 @@
|
||||
import { Italic as TipTapItalic } from 'tiptap-extensions'
|
||||
|
||||
export default class Italic extends TipTapItalic {
|
||||
pasteRules() {
|
||||
return []
|
||||
}
|
||||
}
|
||||
7
webapp/components/Editor/marks/Strike.js
Normal file
7
webapp/components/Editor/marks/Strike.js
Normal file
@ -0,0 +1,7 @@
|
||||
import { Strike as TipTapStrike } from 'tiptap-extensions'
|
||||
|
||||
export default class Strike extends TipTapStrike {
|
||||
pasteRules() {
|
||||
return []
|
||||
}
|
||||
}
|
||||
97
webapp/components/Editor/nodes/Embed.js
Normal file
97
webapp/components/Editor/nodes/Embed.js
Normal file
@ -0,0 +1,97 @@
|
||||
import { Node } from 'tiptap'
|
||||
import pasteRule from '../commands/pasteRule'
|
||||
import { compileToFunctions } from 'vue-template-compiler'
|
||||
|
||||
const template = `
|
||||
<a class="embed" :href="dataEmbedUrl" rel="noopener noreferrer nofollow" target="_blank">
|
||||
<div v-if="embedHtml" v-html="embedHtml" />
|
||||
<em> {{ dataEmbedUrl }} </em>
|
||||
</a>
|
||||
`
|
||||
const compiledTemplate = compileToFunctions(template)
|
||||
|
||||
export default class Embed extends Node {
|
||||
get name() {
|
||||
return 'embed'
|
||||
}
|
||||
|
||||
get defaultOptions() {
|
||||
return {
|
||||
onEmbed: () => ({}),
|
||||
}
|
||||
}
|
||||
|
||||
pasteRules({ type, schema }) {
|
||||
return [
|
||||
pasteRule(
|
||||
// source: https://stackoverflow.com/a/3809435
|
||||
/https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)/g,
|
||||
type,
|
||||
url => ({ dataEmbedUrl: url }),
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
get schema() {
|
||||
return {
|
||||
attrs: {
|
||||
dataEmbedUrl: {
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
group: 'inline',
|
||||
inline: true,
|
||||
parseDOM: [
|
||||
{
|
||||
tag: 'a[href].embed',
|
||||
getAttrs: dom => ({
|
||||
dataEmbedUrl: dom.getAttribute('href'),
|
||||
}),
|
||||
},
|
||||
],
|
||||
toDOM: node => [
|
||||
'a',
|
||||
{
|
||||
href: node.attrs.dataEmbedUrl,
|
||||
class: 'embed',
|
||||
target: '_blank',
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
get view() {
|
||||
return {
|
||||
props: ['node', 'updateAttrs', 'options'],
|
||||
data: () => ({
|
||||
embedData: {},
|
||||
}),
|
||||
async created() {
|
||||
if (!this.options) return {}
|
||||
this.embedData = await this.options.onEmbed({ url: this.dataEmbedUrl })
|
||||
},
|
||||
computed: {
|
||||
embedClass() {
|
||||
return this.embedHtml ? 'embed' : ''
|
||||
},
|
||||
embedHtml() {
|
||||
const { html = '' } = this.embedData
|
||||
return html
|
||||
},
|
||||
dataEmbedUrl: {
|
||||
get() {
|
||||
return this.node.attrs.dataEmbedUrl
|
||||
},
|
||||
set(dataEmbedUrl) {
|
||||
this.updateAttrs({
|
||||
dataEmbedUrl,
|
||||
})
|
||||
},
|
||||
},
|
||||
},
|
||||
render(createElement) {
|
||||
return compiledTemplate.render.call(this, createElement)
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
66
webapp/components/Editor/nodes/Embed.spec.js
Normal file
66
webapp/components/Editor/nodes/Embed.spec.js
Normal file
@ -0,0 +1,66 @@
|
||||
import { shallowMount } from '@vue/test-utils'
|
||||
import Embed from './Embed'
|
||||
|
||||
let Wrapper
|
||||
let propsData
|
||||
const someUrl = 'https://www.youtube.com/watch?v=qkdXAtO40Fo'
|
||||
|
||||
describe('Embed.vue', () => {
|
||||
beforeEach(() => {
|
||||
propsData = {}
|
||||
const component = new Embed()
|
||||
Wrapper = ({ mocks, propsData }) => {
|
||||
return shallowMount(component.view, { propsData })
|
||||
}
|
||||
})
|
||||
|
||||
it('renders anchor', () => {
|
||||
propsData = {
|
||||
node: { attrs: { href: someUrl } },
|
||||
}
|
||||
expect(Wrapper({ propsData }).is('a')).toBe(true)
|
||||
})
|
||||
|
||||
describe('given a href', () => {
|
||||
describe('onEmbed returned embed data', () => {
|
||||
beforeEach(() => {
|
||||
propsData.options = {
|
||||
onEmbed: () => ({
|
||||
type: 'video',
|
||||
title: 'Baby Loves Cat',
|
||||
author: 'Merkley Family',
|
||||
publisher: 'YouTube',
|
||||
date: '2015-08-16T00:00:00.000Z',
|
||||
description:
|
||||
'She’s incapable of controlling her limbs when her kitty is around. The obsession grows every day. Ps. That’s a sleep sack she’s in. Not a starfish outfit. Al...',
|
||||
url: someUrl,
|
||||
image: 'https://i.ytimg.com/vi/qkdXAtO40Fo/maxresdefault.jpg',
|
||||
audio: null,
|
||||
video: null,
|
||||
lang: 'de',
|
||||
sources: ['resource', 'oembed'],
|
||||
html:
|
||||
'<iframe width="480" height="270" src="https://www.youtube.com/embed/qkdXAtO40Fo?feature=oembed" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>',
|
||||
}),
|
||||
}
|
||||
})
|
||||
|
||||
it('renders the given html code', async () => {
|
||||
propsData.node = { attrs: { href: 'https://www.youtube.com/watch?v=qkdXAtO40Fo' } }
|
||||
const wrapper = Wrapper({ propsData })
|
||||
await wrapper.html()
|
||||
expect(wrapper.find('div iframe').attributes('src')).toEqual(
|
||||
'https://www.youtube.com/embed/qkdXAtO40Fo?feature=oembed',
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('without embedded html but some meta data instead', () => {
|
||||
it.todo('renders description and link')
|
||||
})
|
||||
|
||||
describe('without any meta data', () => {
|
||||
it.todo('renders a link without `embed` class')
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -18,27 +18,24 @@ export default class Hashtag extends TipTapMention {
|
||||
}
|
||||
|
||||
get schema() {
|
||||
const patchedSchema = super.schema
|
||||
|
||||
patchedSchema.attrs = {
|
||||
url: {},
|
||||
label: {},
|
||||
return {
|
||||
...super.schema,
|
||||
toDOM: node => {
|
||||
return [
|
||||
'a',
|
||||
{
|
||||
class: this.options.mentionClass,
|
||||
href: `/search/hashtag/${node.attrs.id}`,
|
||||
'data-hashtag-id': node.attrs.id,
|
||||
target: '_blank',
|
||||
},
|
||||
`${this.options.matcher.char}${node.attrs.label}`,
|
||||
]
|
||||
},
|
||||
parseDOM: [
|
||||
// simply don't parse mentions from html
|
||||
// just treat them as normal links
|
||||
],
|
||||
}
|
||||
patchedSchema.toDOM = node => {
|
||||
return [
|
||||
'a',
|
||||
{
|
||||
class: this.options.mentionClass,
|
||||
href: node.attrs.url,
|
||||
target: '_blank',
|
||||
// contenteditable: 'true',
|
||||
},
|
||||
`${this.options.matcher.char}${node.attrs.label}`,
|
||||
]
|
||||
}
|
||||
patchedSchema.parseDOM = [
|
||||
// this is not implemented
|
||||
]
|
||||
return patchedSchema
|
||||
}
|
||||
}
|
||||
|
||||
34
webapp/components/Editor/nodes/Link.js
Normal file
34
webapp/components/Editor/nodes/Link.js
Normal file
@ -0,0 +1,34 @@
|
||||
import { Link as TipTapLink } from 'tiptap-extensions'
|
||||
|
||||
export default class Link extends TipTapLink {
|
||||
pasteRules({ type }) {
|
||||
return []
|
||||
}
|
||||
|
||||
get schema() {
|
||||
return {
|
||||
attrs: {
|
||||
href: {
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
inclusive: false,
|
||||
parseDOM: [
|
||||
{
|
||||
tag: 'a[href]:not(.embed)', // do not trigger on embed links
|
||||
getAttrs: dom => ({
|
||||
href: dom.getAttribute('href'),
|
||||
}),
|
||||
},
|
||||
],
|
||||
toDOM: node => [
|
||||
'a',
|
||||
{
|
||||
...node.attrs,
|
||||
rel: 'noopener noreferrer nofollow',
|
||||
},
|
||||
0,
|
||||
],
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -6,26 +6,24 @@ export default class Mention extends TipTapMention {
|
||||
}
|
||||
|
||||
get schema() {
|
||||
const patchedSchema = super.schema
|
||||
|
||||
patchedSchema.attrs = {
|
||||
url: {},
|
||||
label: {},
|
||||
return {
|
||||
...super.schema,
|
||||
toDOM: node => {
|
||||
return [
|
||||
'a',
|
||||
{
|
||||
class: this.options.mentionClass,
|
||||
href: `/profile/${node.attrs.id}`,
|
||||
'data-mention-id': node.attrs.id,
|
||||
target: '_blank',
|
||||
},
|
||||
`${this.options.matcher.char}${node.attrs.label}`,
|
||||
]
|
||||
},
|
||||
parseDOM: [
|
||||
// simply don't parse mentions from html
|
||||
// just treat them as normal links
|
||||
],
|
||||
}
|
||||
patchedSchema.toDOM = node => {
|
||||
return [
|
||||
'a',
|
||||
{
|
||||
class: this.options.mentionClass,
|
||||
href: node.attrs.url,
|
||||
target: '_blank',
|
||||
},
|
||||
`${this.options.matcher.char}${node.attrs.label}`,
|
||||
]
|
||||
}
|
||||
patchedSchema.parseDOM = [
|
||||
// this is not implemented
|
||||
]
|
||||
return patchedSchema
|
||||
}
|
||||
}
|
||||
|
||||
78
webapp/components/PostCard/PostCard.story.js
Normal file
78
webapp/components/PostCard/PostCard.story.js
Normal file
@ -0,0 +1,78 @@
|
||||
import { storiesOf } from '@storybook/vue'
|
||||
import { withA11y } from '@storybook/addon-a11y'
|
||||
import HcPostCard from '~/components/PostCard'
|
||||
import helpers from '~/storybook/helpers'
|
||||
|
||||
helpers.init()
|
||||
|
||||
const post = {
|
||||
id: 'd23a4265-f5f7-4e17-9f86-85f714b4b9f8',
|
||||
title: 'Very nice Post Title',
|
||||
contentExcerpt: '<p>My post content</p>',
|
||||
createdAt: '2019-06-24T22:08:59.304Z',
|
||||
disabled: false,
|
||||
deleted: false,
|
||||
slug: 'very-nice-post-title',
|
||||
image: null,
|
||||
author: {
|
||||
id: 'u3',
|
||||
avatar:
|
||||
'https://steamcdn-a.akamaihd.net/steamcommunity/public/images/avatars/db/dbc9e03ebcc384b920c31542af2d27dd8eea9dc2_full.jpg',
|
||||
slug: 'jenny-rostock',
|
||||
name: 'Rainer Unsinn',
|
||||
disabled: false,
|
||||
deleted: false,
|
||||
contributionsCount: 25,
|
||||
shoutedCount: 5,
|
||||
commentsCount: 39,
|
||||
followedByCount: 2,
|
||||
followedByCurrentUser: true,
|
||||
location: null,
|
||||
badges: [
|
||||
{
|
||||
id: 'b4',
|
||||
key: 'indiegogo_en_bear',
|
||||
icon: '/img/badges/indiegogo_en_bear.svg',
|
||||
__typename: 'Badge',
|
||||
},
|
||||
],
|
||||
__typename: 'User',
|
||||
},
|
||||
commentsCount: 12,
|
||||
categories: [],
|
||||
shoutedCount: 421,
|
||||
__typename: 'Post',
|
||||
}
|
||||
|
||||
storiesOf('Post Card', module)
|
||||
.addDecorator(withA11y)
|
||||
.addDecorator(helpers.layout)
|
||||
.add('without image', () => ({
|
||||
components: { HcPostCard },
|
||||
store: helpers.store,
|
||||
data: () => ({
|
||||
post,
|
||||
}),
|
||||
template: `
|
||||
<hc-post-card
|
||||
:post="post"
|
||||
:width="{ base: '100%', xs: '100%', md: '50%', xl: '33%' }"
|
||||
/>
|
||||
`,
|
||||
}))
|
||||
.add('with image', () => ({
|
||||
components: { HcPostCard },
|
||||
store: helpers.store,
|
||||
data: () => ({
|
||||
post: {
|
||||
...post,
|
||||
image: 'https://unsplash.com/photos/R4y_E5ZQDPg/download',
|
||||
},
|
||||
}),
|
||||
template: `
|
||||
<hc-post-card
|
||||
:post="post"
|
||||
:width="{ base: '100%', xs: '100%', md: '50%', xl: '33%' }"
|
||||
/>
|
||||
`,
|
||||
}))
|
||||
23
webapp/graphql/EmbedQuery.js
Normal file
23
webapp/graphql/EmbedQuery.js
Normal file
@ -0,0 +1,23 @@
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
export default function() {
|
||||
return gql`
|
||||
query($url: String!) {
|
||||
embed(url: $url) {
|
||||
type
|
||||
title
|
||||
author
|
||||
publisher
|
||||
date
|
||||
description
|
||||
url
|
||||
image
|
||||
audio
|
||||
video
|
||||
lang
|
||||
sources
|
||||
html
|
||||
}
|
||||
}
|
||||
`
|
||||
}
|
||||
@ -20,7 +20,7 @@
|
||||
:width="{ base: '85%', sm: '85%', md: '50%', lg: '50%' }"
|
||||
:class="{ 'hide-mobile-menu': !toggleMobileMenu }"
|
||||
>
|
||||
<div id="nav-search-box">
|
||||
<div id="nav-search-box" v-if="isLoggedIn">
|
||||
<search-input
|
||||
id="nav-search"
|
||||
:delay="300"
|
||||
@ -33,6 +33,7 @@
|
||||
</div>
|
||||
</ds-flex-item>
|
||||
<ds-flex-item
|
||||
v-if="isLoggedIn"
|
||||
:width="{ base: '15%', sm: '15%', md: '10%', lg: '10%' }"
|
||||
:class="{ 'hide-mobile-menu': !toggleMobileMenu }"
|
||||
>
|
||||
@ -116,7 +117,7 @@
|
||||
</ds-container>
|
||||
</div>
|
||||
<ds-container style="word-break: break-all">
|
||||
<div class="main-container" :width="{ base: '100%', md: '96%' }">
|
||||
<div class="main-container">
|
||||
<nuxt />
|
||||
</div>
|
||||
</ds-container>
|
||||
@ -125,9 +126,11 @@
|
||||
-
|
||||
<nuxt-link to="/imprint">{{ $t('site.imprint') }}</nuxt-link>
|
||||
‑
|
||||
<nuxt-link to="/terms-and-conditions">{{ $t('site.termsAc') }}</nuxt-link>
|
||||
<nuxt-link to="/terms-and-conditions">{{ $t('site.termsAndConditions') }}</nuxt-link>
|
||||
‑
|
||||
<nuxt-link to="/privacy">{{ $t('site.privacy') }}</nuxt-link>
|
||||
<nuxt-link to="/code-of-conduct">{{ $t('site.code-of-conduct') }}</nuxt-link>
|
||||
‑
|
||||
<nuxt-link to="/data-privacy">{{ $t('site.data-privacy') }}</nuxt-link>
|
||||
‑
|
||||
<nuxt-link to="/changelog">{{ $t('site.changelog') }}</nuxt-link>
|
||||
</div>
|
||||
@ -149,7 +152,6 @@ import HcAvatar from '~/components/Avatar/Avatar.vue'
|
||||
import seo from '~/mixins/seo'
|
||||
import FilterPosts from '~/components/FilterPosts/FilterPosts.vue'
|
||||
import CategoryQuery from '~/graphql/CategoryQuery.js'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Dropdown,
|
||||
@ -266,47 +268,38 @@ export default {
|
||||
align-self: center;
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.main-container {
|
||||
padding-top: 6rem;
|
||||
padding-bottom: 5rem;
|
||||
}
|
||||
|
||||
.main-navigation {
|
||||
a {
|
||||
color: $text-color-soft;
|
||||
}
|
||||
}
|
||||
|
||||
.main-navigation-right {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.main-navigation-right .desktop-view {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.avatar-menu {
|
||||
margin: 2px 0px 0px 5px;
|
||||
}
|
||||
|
||||
.avatar-menu-trigger {
|
||||
user-select: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-left: $space-xx-small;
|
||||
}
|
||||
|
||||
.avatar-menu-popover {
|
||||
padding-top: 0.5rem;
|
||||
padding-bottom: 0.5rem;
|
||||
|
||||
hr {
|
||||
color: $color-neutral-90;
|
||||
background-color: $color-neutral-90;
|
||||
}
|
||||
|
||||
.logout-link {
|
||||
margin-left: -$space-small;
|
||||
margin-right: -$space-small;
|
||||
@ -315,43 +308,35 @@ export default {
|
||||
padding: $space-x-small $space-small;
|
||||
// subtract menu border with from padding
|
||||
padding-left: $space-small - 2;
|
||||
|
||||
color: $text-color-base;
|
||||
|
||||
&:hover {
|
||||
color: $text-color-link-active;
|
||||
}
|
||||
}
|
||||
|
||||
nav {
|
||||
margin-left: -$space-small;
|
||||
margin-right: -$space-small;
|
||||
margin-top: -$space-xx-small;
|
||||
margin-bottom: -$space-xx-small;
|
||||
|
||||
a {
|
||||
padding-left: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (min-width: 960px) {
|
||||
.mobile-hamburger-menu {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 960px) {
|
||||
#nav-search-box,
|
||||
.main-navigation-right {
|
||||
margin: 10px 0px;
|
||||
}
|
||||
|
||||
.hide-mobile-menu {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.ds-footer {
|
||||
text-align: center;
|
||||
position: fixed;
|
||||
|
||||
@ -11,8 +11,8 @@
|
||||
"site": {
|
||||
"made": "Mit ❤ gemacht",
|
||||
"imprint": "Impressum",
|
||||
"termsAc": "Nutzungsbedingungen",
|
||||
"privacy": "Datenschutz",
|
||||
"data-privacy": "Datenschutz",
|
||||
"termsAndConditions": "Nutzungsbedingungen",
|
||||
"changelog": "Änderungen & Verlauf",
|
||||
"contact": "Kontakt",
|
||||
"tribunal": "Registergericht",
|
||||
@ -21,7 +21,8 @@
|
||||
"taxident": "Umsatzsteuer-Identifikationsnummer gemäß § 27 a Umsatzsteuergesetz (Deutschland)",
|
||||
"responsible": "Verantwortlicher gemäß § 55 Abs. 2 RStV ",
|
||||
"bank": "Bankverbindung",
|
||||
"germany": "Deutschland"
|
||||
"germany": "Deutschland",
|
||||
"code-of-conduct": "Verhaltenscodex"
|
||||
},
|
||||
"sorting": {
|
||||
"newest": "Neuste",
|
||||
@ -55,6 +56,7 @@
|
||||
"title": "Mach mit bei Human Connection!",
|
||||
"form": {
|
||||
"description": "Um loszulegen, gib deine E-Mail Adresse ein:",
|
||||
"invitation-code": "Dein Einladungscode lautet: <b>{code}</b>",
|
||||
"errors": {
|
||||
"email-exists": "Es gibt schon ein Benutzerkonto mit dieser E-Mail Adresse!",
|
||||
"invalid-invitation-token": "Es sieht so aus, als ob der Einladungscode schon eingelöst wurde. Jeder Code kann nur einmalig benutzt werden."
|
||||
@ -252,7 +254,8 @@
|
||||
},
|
||||
"comment": {
|
||||
"submit": "Kommentiere",
|
||||
"submitted": "Kommentar Gesendet"
|
||||
"submitted": "Kommentar Gesendet",
|
||||
"updated": "Änderungen gespeichert"
|
||||
}
|
||||
},
|
||||
"comment": {
|
||||
@ -358,17 +361,20 @@
|
||||
"user": {
|
||||
"title": "Nutzer melden",
|
||||
"type": "Nutzer",
|
||||
"message": "Bist du sicher, dass du den Nutzer \"<b>{name}</b>\" melden möchtest?"
|
||||
"message": "Bist du sicher, dass du den Nutzer \"<b>{name}</b>\" melden möchtest?",
|
||||
"error": "Du hast den Benutzer bereits gemeldet!"
|
||||
},
|
||||
"contribution": {
|
||||
"title": "Beitrag melden",
|
||||
"type": "Beitrag",
|
||||
"message": "Bist du sicher, dass du den Beitrag \"<b>{name}</b>\" melden möchtest?"
|
||||
"message": "Bist du sicher, dass du den Beitrag \"<b>{name}</b>\" melden möchtest?",
|
||||
"error": "Du hast den Beitrag bereits gemeldet!"
|
||||
},
|
||||
"comment": {
|
||||
"title": "Kommentar melden",
|
||||
"type": "Kommentar",
|
||||
"message": "Bist du sicher, dass du den Kommentar von \"<b>{name}</b>\" melden möchtest?"
|
||||
"message": "Bist du sicher, dass du den Kommentar von \"<b>{name}</b>\" melden möchtest?",
|
||||
"error": "Du hast den Kommentar bereits gemeldet!"
|
||||
}
|
||||
},
|
||||
"followButton": {
|
||||
@ -378,7 +384,6 @@
|
||||
"shoutButton": {
|
||||
"shouted": "empfohlen"
|
||||
},
|
||||
|
||||
"release": {
|
||||
"submit": "freigeben",
|
||||
"cancel": "Abbrechen",
|
||||
@ -393,7 +398,7 @@
|
||||
"title": "Beitrag freigeben",
|
||||
"type": "Beitrag",
|
||||
"message": "Bist du sicher, dass du den Beitrag \"<b>{name}</b>\" freigeben möchtest?",
|
||||
"error": " Den Beitrag hast du schon gemeldet!"
|
||||
"error": "Den Beitrag hast du schon gemeldet!"
|
||||
},
|
||||
"comment": {
|
||||
"title": "Kommentar freigeben",
|
||||
@ -425,7 +430,119 @@
|
||||
"emoted": "emotiert"
|
||||
}
|
||||
},
|
||||
"terms": {
|
||||
"text": "<div ><ol><li><strong>UNFALLGEFAHR: </strong>Das ist eine Testversion! Alle Daten, Dein Profil und die Server können jederzeit komplett vernichtet, verloren, verbrannt und vielleicht auch in der Nähe von Alpha Centauri synchronisiert werden. Die Benutzung läuft auf eigene Gefahr. Mit kommerziellen Nebenwirkungen ist jedoch nicht zu rechnen.</li><br><li><strong>DU UND DEINE DATEN: </strong>Bitte beachte, dass wir die Inhalte der Alphaversion zu Werbezwecken, Webpräsentationen usw. verwenden, aber wir glauben, dass das auch in Deinem Interesse ist. Am besten keinen Nachnamen eingeben und bei noch mehr Datensparsamkeit ein Profilfoto ohne Identität verwenden. Mehr in unserer <a href='/pages/privacy' target='_blank'>Datenschutzerklärung</a>.</li><br><li><strong>BAUSTELLEN: </strong>Das ist immer noch eine Testversion. Wenn etwas nicht funktioniert, blockiert, irritiert, falsch angezeigt, verbogen oder nicht anklickbar ist, bitten wir dies zu entschuldigen. Fehler, Käfer und Bugs bitte melden! <a href='http://localhost:3000/%22https://human-connection.org/alpha/#bugreport%5C%22' target='_blank'>https://human-connection.org/support</a></li><br><li><strong>VERHALTENSCODEX</strong>: Die Verhaltensregeln dienen als Leitsätze für den persönlichen Auftritt und den Umgang untereinander. Wer als Nutzer im Human Connection Netzwerk aktiv ist, Beiträge verfasst, kommentiert oder mit anderen Nutzern, auch außerhalb des Netzwerkes, Kontakt aufnimmt, erkennt diese Verhaltensregeln als verbindlich an: <a href='https://alpha.human-connection.org/pages/code-of-conduct' target='_blank'>https://alpha.human-connection.org/pages/code-of-conduct</a></li><br><li><strong>MODERATION: </strong>Solange kein Community-Moderationssystem lauffähig ist, entscheidet ein Regenbogen-Einhorn darüber, ob Du körperlich und psychisch stabil genug bist, unsere Testversion zu bedienen. Das Einhorn kann Dich jederzeit von der Alpha entfernen. Also sei nett und lass Regenbogenfutter da!</li><br><li><strong>FAIRNESS: </strong>Sollte Dir die Alphaversion unseres Netzwerks wider Erwarten, egal aus welchen Gründen, nicht gefallen, überweisen wir Dir Deine gespendeten Monatsbeiträge innerhalb der ersten 2 Monate gerne zurück. Einfach Mail an: <a href='mailto:info@human-connection.org' target='_blank'>info@human-connection.org </a><strong>Achtung: Viele Funktionen werden erst nach und nach eingebaut. </strong></li><br><li><strong>FRAGEN?</strong> Die Termine und Links zu den Zoom-Räumen findest Du hier: <a href='http://localhost:3000/%22https://human-connection.org/events-und-news//%22' target='_blank'>https://human-connection.org/veranstaltungen/</a></li><br><li><strong>VON MENSCHEN FÜR MENSCHEN: </strong>Bitte hilf uns weitere monatlichen Spender für Human Connection zu bekommen, damit das Netzwerk so schnell wie möglich offiziell an den Start gehen kann. <a href='http://localhost:3000/%22https://human-connection.org/alpha/#bugreport%5C%22' target='_blank'>https://human-connection.org</a></li></ol><p>Jetzt aber viel Spaß mit der Alpha von Human Connection! Für den ersten Weltfrieden. ♥︎</p><br><p><strong>Herzlichst,</strong></p><p><strong>Euer Human Connection Team</strong></p></div>"
|
||||
"changelog": {
|
||||
"24": "Dialog zum Ändern des Passwortes wurde hinzugefügt.",
|
||||
"23": "Ein Editor zum Schreiben von Beiträgen wurde hinzugefügt.",
|
||||
"22": "Ein Dialog zum Ausblenden von Posts und Kommentaren, die gegen die Nutzungsbedingungen verstoßen wurde für die Nutzung durch Moderatoren hinzugefügt.",
|
||||
"21": "Der \"Empfehlen\" Knopf mit dem Megafon wurde der Artikelansicht hinzugefügt.",
|
||||
"20": "Das automatische Ausrollen der programmierten Software wurde repariert, so dass neue Features immer gleich getestet werden können.",
|
||||
"19": "Alle Software-Repositories wurden in ein großes vereinigt, damit alle gegenseitigen Abhängigkeiten klar sind und durch einen übergreifenden Versionsstand aneinander gekoppelt sind.",
|
||||
"18": "Der Import der Inhalte der heutigen Alpha-Daten in die zukünftige Nitro-Version wurde getestet und automatisiert. Dabei wurden nur Testdaten verwendet.",
|
||||
"17": "Das Webinterface der Nitro wurde hinsichtlich der korrekten Anzeige auf Mobilgeräten wie Handys und Tablets getestet.",
|
||||
"16": "Eine Schaltfläche zum Melden von Inhalten, die den Nutzungsrichtlinien widersprechen, wurde hinzugefügt.",
|
||||
"15": "Eine Moderationswerkzeug, in welchem alle gemeldeten Beiträge für Moderatoren angezeigt werden, wurde hinzugefügt.",
|
||||
"14": "Ein Fehler bei der Benutzer-Authentifizierung wurde behoben, Eingabemöglichkeit von Land, Region und Stadt im 'Über-mich'-Feld in den User-Profilen.",
|
||||
"13": "Einstellungsmöglichkeit der Sprache für Benutzerinterface und Inhalte in der Nitro, so, wie in der Alpha.",
|
||||
"12": "Integration des Nuxt-i18n-Toolkits für Sprachunterstützung bei serverseitig gerenderten Webapps.",
|
||||
"11": "Integration der Lokalise.co-Pipeline für Übersetzungen.",
|
||||
"10": "Verwendete Toolkits und Frameworks auf den neuesten Stand upgedatet.",
|
||||
"9": "[laufend] Icons für Admin-Interfae der Kategorien und Tags hinzugefügt und Anzahl, wie viele User einen Tag benutzen.",
|
||||
"8": "Ersetzen der eingesetzten eslint-Version für die Code-Qualität durch die Aktuelle.",
|
||||
"7": "[laufend] Implementierung der Blacklist auf der Nitro zum Ausfiltern von Inhalten nicht gewünschter User.",
|
||||
"6": "Programmierung einer Funktionalität, dass später User, die keine Spender sind, um eine Spende gebeten werden können, um die Weiterentwicklung besser zu unterstützen",
|
||||
"5": "Programmierung einer Datenbankabfrage, welche User einen nicht abgeschlossenen Anmeldungprozess haben, um diese zwecks Hilfestellung kontaktieren zu können",
|
||||
"4": "Einführung eines Versions-Schemas in der Netzwerk-Anwendung um in der laufenden Anwendung die jeweilige Versionerkennen zu können",
|
||||
"3": "Login für die Nitro-Version mit einem 'aufgehübschten' Anmeldedialog.",
|
||||
"2": "Umstellung der Projektplanung auf Zenhub",
|
||||
"1": "Einführung von Scrum als Vorgehensweise für das Projektmanagement",
|
||||
"0": "Start der Arbeit an der \"Nitro\". Dabei wird die Datenbank und die Serverschnittstelle ausgetauscht:"
|
||||
},
|
||||
"code-of-conduct": {
|
||||
"subheader": "für das Soziale Netzwerk der Human Connection gGmbH",
|
||||
"preamble": {
|
||||
"title": "Präambel",
|
||||
"description": "Human Connection ist ein gemeinnütziges soziales Wissens- und Aktionsnetzwerk der nächsten Generation. Von Menschen – für Menschen. Open Source, fair und transparent. Für positiven lokalen und globalen Wandel in allen Lebensbereichen. Wir gestalten den öffentlichen Austausch von Wissen, Ideen und Projekten völlig neu. Die Funktionen von Human Connection bringen die Menschen zusammen – offline und online – so dass wir die Welt zu einem besseren Ort machen können."
|
||||
},
|
||||
"purpose": {
|
||||
"title": "Zweck",
|
||||
"description": "Mit diesen Verhaltensregeln regeln wir die wesentlichen Grundsätze für das Verhalten in unserem Sozialen Netzwerk. Dabei ist die Menschenrechtscharta der Vereinten Nationen unsere Orientierung und bildet das Herz unseres Werteverständnisses. Die Verhaltensregeln dienen als Leitsätze für den persönlichen Auftritt und den Umgang untereinander. Wer als Nutzer im Human Connection Netzwerk aktiv ist, Beiträge verfasst, kommentiert oder mit anderen Nutzern, auch außerhalb des Netzwerkes, Kontakt aufnimmt, erkennt diese Verhaltensregeln als verbindlich an."
|
||||
},
|
||||
|
||||
"expected-behaviour": {
|
||||
"title": "Erwartetes Verhalten",
|
||||
"description": "Die folgenden Verhaltensweisen werden von allen Community-Mitgliedern erwartet und gefordert:",
|
||||
"list": {
|
||||
"0": "Sei rücksichtsvoll und respektvoll bei dem was Du schreibst und tust.",
|
||||
"1": "Versuche auf andere zuzugehen, bevor ein Konflikt entsteht.",
|
||||
"2": "Vermeide erniedrigende, diskriminierende oder belästigende Verhaltensweisen und Ausdrücke.",
|
||||
"3": "Gehe achtsam mit Deiner Umgebung um. Informiere den Support bei gefährlichen Situationen, wenn eine Person in Not ist oder bei Verstößen gegen diesen Verhaltenskodex, auch wenn sie unbedeutend erscheinen."
|
||||
}
|
||||
},
|
||||
"unacceptable-behaviour": {
|
||||
"title": "Nichtakzeptables Verhalten",
|
||||
"description": "Die folgenden Verhaltensweisen sind in unserer Community inakzeptabel:",
|
||||
"list": {
|
||||
"0": "Diskriminierende Beiträge, Kommentare, Äußerungen oder Beleidigungen, insbesondere solche, die sich auf Geschlecht, sexuelle Orientierung, Rasse, Religion, politische oder weltanschauliche Ausrichtung oder Behinderung beziehen.",
|
||||
"1": "Das Posten oder Verlinken eindeutig pornografischen Materials.",
|
||||
"2": "Verherrlichung oder Verharmlosung grausamer oder unmenschlicher Gewalttätigkeiten.",
|
||||
"3": "Das Veröffentlichen von personenbezogenen Daten anderer ohne deren Einverständnis oder das Androhen dessen (\"Doxing\").",
|
||||
"4": "Absichtliche Einschüchterung, Stalking oder Verfolgung.",
|
||||
"5": "Bewerben von Produkten und Dienstleistungen mit kommerzieller Absicht.",
|
||||
"6": "Strafbares Verhalten bzw. Verstoß gegen deutsches Recht.",
|
||||
"7": "Befürwortung oder Ermutigung zu diesen Verhaltensweisen."
|
||||
}
|
||||
},
|
||||
"consequences": {
|
||||
"title": "Konsequenzen inakzeptablen Verhaltens",
|
||||
"description": "Wenn ein Gemeinschaftsmitglied inakzeptables Verhalten an den Tag legt, können die verantwortlichen Betreiber, Moderatoren und Administratoren des Netzwerks angemessene Maßnahmen ergreifen, u.a.:",
|
||||
"list": {
|
||||
"0": "Aufforderung zum sofortigen Abstellen des inakzeptablen Verhaltens",
|
||||
"1": "Sperren oder Löschen von Kommentaren",
|
||||
"2": "Temporärer Ausschluss aus dem jeweiligen Beitrag",
|
||||
"3": "Sperren bzw. Löschen von Inhalten",
|
||||
"4": "Temporärer Entzug von Schreibrechten",
|
||||
"5": "Vorübergehender Ausschluss aus dem Netzwerk",
|
||||
"6": "Endgültiger Ausschluss aus dem Netzwerk",
|
||||
"7": "Verstöße gegen deutsches Recht können zur Anzeige gebracht werden.",
|
||||
"8": "Meldung von Vorkommnissen"
|
||||
}
|
||||
},
|
||||
"get-help": "Wenn du einem inakzeptablen Verhalten ausgesetzt bist, es miterlebst oder andere Bedenken hast, benachrichtige bitte so schnell wie möglich einen Organisator der Gemeinschaft und verlinke oder verweise auf den entsprechenden Inhalt:"
|
||||
},
|
||||
"termsAndConditions": {
|
||||
"risk": {
|
||||
"title": "Unfallgefahr",
|
||||
"description": "Das ist eine Testversion! Alle Daten, Dein Profil und die Server können jederzeit komplett vernichtet, verloren, verbrannt und vielleicht auch in der Nähe von Alpha Centauri synchronisiert werden. Die Benutzung läuft auf eigene Gefahr. Mit kommerziellen Nebenwirkungen ist jedoch nicht zu rechnen."
|
||||
},
|
||||
"data-privacy": {
|
||||
"title": "Du und deine Daten",
|
||||
"description": "Bitte beachte, dass wir die Inhalte der Alphaversion zu Werbezwecken, Webpräsentationen usw. verwenden, aber wir glauben, dass das auch in Deinem Interesse ist. Am besten keinen Nachnamen eingeben und bei noch mehr Datensparsamkeit ein Profilfoto ohne Identität verwenden. Mehr in unserer <a href=\"/data-privacy\">Datenschutzerklärung</a>"
|
||||
},
|
||||
"work-in-progress": {
|
||||
"title": "Baustellen",
|
||||
"description": "Das ist immer noch eine Testversion. Wenn etwas nicht funktioniert, blockiert, irritiert, falsch angezeigt, verbogen oder nicht anklickbar ist, bitten wir dies zu entschuldigen. Fehler, Käfer und Bugs bitte melden! <a href=\"mailto:support@human-connection.org\" target=\"_blank\">support@human-connection.org</a>"
|
||||
},
|
||||
"code-of-conduct": {
|
||||
"title": "Verhaltenscodex",
|
||||
"description": "<a href=\"/code-of-conduct\">Die Verhaltensregeln </a> dienen als Leitsätze für den persönlichen Auftritt und den Umgang untereinander. Wer als Nutzer im Human Connection Netzwerk aktiv ist, Beiträge verfasst, kommentiert oder mit anderen Nutzern, auch außerhalb des Netzwerkes, Kontakt aufnimmt, erkennt diese Verhaltensregeln als verbindlich an"
|
||||
},
|
||||
"moderation": {
|
||||
"title": "Moderation",
|
||||
"description": "Solange kein Community-Moderationssystem lauffähig ist, entscheidet ein Regenbogen-Einhorn darüber, ob Du körperlich und psychisch stabil genug bist, unsere Testversion zu bedienen. Das Einhorn kann Dich jederzeit von der Alpha entfernen. Also sei nett und lass Regenbogenfutter da!"
|
||||
},
|
||||
"fairness": {
|
||||
"title": "Fairness",
|
||||
"description": "Sollte Dir die Alphaversion unseres Netzwerks wider Erwarten, egal aus welchen Gründen, nicht gefallen, überweisen wir Dir Deine gespendeten Monatsbeiträge innerhalb der ersten 2 Monate gerne zurück. Einfach Mail an: <a href=\"mailto:info@human-connection.org\" target=\"_blank\" s> info@human-connection.org </a> Achtung: Viele Funktionen werden erst nach und nach eingebaut."
|
||||
},
|
||||
"questions": {
|
||||
"title": "Fragen",
|
||||
"description": "Die Termine und Links zu den Zoom-Räumen findest Du hier: <a href=\"https://human-connection.org/events-und-news/\" target=\"_blank\" >https://human-connection.org/veranstaltungen/ </a>"
|
||||
},
|
||||
"human-connection": {
|
||||
"title": "Von Menschen für Menschen:",
|
||||
"description": "Bitte hilf uns weitere monatlichen Spender für Human Connection zu bekommen, damit das Netzwerk so schnell wie möglich offiziell an den Start gehen kann. <a href=\"https://human-connection.org/\" target=\"_blank\"> https://human-connection.org </a>"
|
||||
},
|
||||
"have-fun": "Jetzt aber viel Spaß mit der Alpha von Human Connection! Für den ersten Weltfrieden. ♥︎",
|
||||
"closing": "Herzlichst <br><br> Euer Human Connection Team"
|
||||
}
|
||||
}
|
||||
|
||||
@ -11,8 +11,8 @@
|
||||
"site": {
|
||||
"made": "Made with ❤",
|
||||
"imprint": "Imprint",
|
||||
"termsAc": "Terms and conditions",
|
||||
"privacy": "Data privacy",
|
||||
"termsAndConditions": "Terms and conditions",
|
||||
"data-privacy": "Data privacy",
|
||||
"changelog": "Changes & History",
|
||||
"contact": "Contact",
|
||||
"tribunal": "Registry court",
|
||||
@ -21,7 +21,8 @@
|
||||
"taxident": "USt-ID. according to §27a of the German Sales Tax Law:",
|
||||
"responsible": "responsible for contents of this page (§ 55 Abs. 2 RStV)",
|
||||
"bank": "bank account",
|
||||
"germany": "Germany"
|
||||
"germany": "Germany",
|
||||
"code-of-conduct": "Code of Conduct"
|
||||
},
|
||||
"sorting": {
|
||||
"newest": "Newest",
|
||||
@ -390,17 +391,20 @@
|
||||
"user": {
|
||||
"title": "Release User",
|
||||
"type": "User",
|
||||
"message": "Do you really want to release the user \"<b>{name}</b>\"?"
|
||||
"message": "Do you really want to release the user \"<b>{name}</b>\"?",
|
||||
"error": "You already reported the user!"
|
||||
},
|
||||
"contribution": {
|
||||
"title": "Release Contribution",
|
||||
"type": "Contribution",
|
||||
"message": "Do you really want to release the contribution \"<b>{name}</b>\"?"
|
||||
"message": "Do you really want to release the contribution \"<b>{name}</b>\"?",
|
||||
"error": "You have already reported the contribution!!"
|
||||
},
|
||||
"comment": {
|
||||
"title": "Release Comment",
|
||||
"type": "Comment",
|
||||
"message": "Do you really want to release the comment from \"<b>{name}</b>\"?"
|
||||
"message": "Do you really want to release the comment from \"<b>{name}</b>\"?",
|
||||
"error": "You have already reported the comment!"
|
||||
}
|
||||
},
|
||||
"user": {
|
||||
@ -426,7 +430,118 @@
|
||||
"emoted": "emoted"
|
||||
}
|
||||
},
|
||||
"terms": {
|
||||
"text": "<div><ol><li><strong>RISK OF ACCIDENT:</strong> This is a test version! All data, your profile and the server can be completely destroyed, wiped out, lost, burnt and eventually synchronised near Alpha Centauri at any time. Use on your own risk. Commercial effects are not likely though.</li><br><li><strong>YOU AND YOUR DATA:</strong> Please notice, that the content of the alpha version will be used for publicity and web presentations etc. but we are sure, this is to your interest. If you like use no surnames and if you want to disclose less data use a profile picture without identity. You can find more information in our <a href='/pages/privacy' target='_blank'>privacy policy</a>.</li><br><li><strong>SITE:</strong> This is still a test version. Please excuse if some applications are not working, blocking, irritating, displayed falsely or not able to be clicked on. Please report faults and bugs! <a href='https://human-connection.org/support' target='_blank'>https://human-connection.org/support</a></li><br><li><strong>CODE OF CONDUCT</strong>: The code of conduct serves as guiding principles for our personal appearance and interaction with one another. Anyone who is active as a user in the Human Connection Network, writes articles, comments or contacts other users, including those outside the network, acknowledges these rules of conduct as binding: <a href='https://alpha.human-connection.org/pages/code-of-conduct' target='_blank'>https://alpha.human-connection.org/pages/code-of-conduct</a></li><br><li><strong>MODERATION:</strong> As long as there is no community moderation-system in operation, a rainbow colored unicorn decides, if you are physically and mentally stable enough to operate our test version. The unicorn can delete you from the alpha version at any time. So be so kind and leave rainbow food!</li><br><li><strong>FAIRNESS:</strong> If, against all expectations, our alpha version is not to your liking, we return your monthly payment within the first two months. Please send a mail to: <a href='mailto:info@human-connection.org' target='_blank'>info@human-connection.org</a>. <strong>Attention: Pleace note that more features are build in on regular basis.</strong></li><br><li><strong>QUESTIONS?</strong> You can find the dates and links to our zoom-rooms here: <a href='https://human-connection.org/events-und-news/' target='_blank'>https://human-connection.org/events-und-news/</a></li><br><li><strong>FROM HUMAN BEING TO HUMAN BEING: </strong>Please help us to get new donators for Human Connection, so the network can take off as soon as possible. <a href='https://human-connection.org/' target='_blank'>https://human-connection.org</a></li></ol><br><p>Now have fun with the alpha version of Human Connection! For the first universal peace.<strong> ♥︎</strong></p><p><strong> </strong></p><br><p><strong>Thank you very much, </strong></p><p><strong>your Human Connection Team</strong></p></div>"
|
||||
"changelog": {
|
||||
"24": "Dialog for password change has been added.",
|
||||
"23": "An editor for writing posts has been added.",
|
||||
"22": "A dialogue for blocking content which violates the terms of service had been added for the moderators.",
|
||||
"21": "The 'Shout' button has been added to the article view.",
|
||||
"20": "Automated deployment of the software has been fixed so that new features can always be tested immediately.",
|
||||
"19": "All software repositories on Github have been merged into one to avoid independent version information and incompatible interfaces",
|
||||
"18": "The automated import of data from the Alpha-version to the Nitro had been tested with test data",
|
||||
"17": "The web interface of the Nitro had been tested for correct display on mobile devices.",
|
||||
"16": "A Button for reporting abusive content has been added",
|
||||
"15": "An moderation backend for reported content has been implemented",
|
||||
"14": "A Bug with the user authentication has been fixed",
|
||||
"13": "User can input country, region or city and it is her / his decision how accurate it should be.",
|
||||
"12": "User settings field for setting the interface and preferred content language",
|
||||
"11": "Integration of Nuxt-i18n toolkit for support of server-side rendered pages of webapps",
|
||||
"10": "Integration of Lokalise.co pipeline for translations + Updated used toolkits and frameworks to newest versions",
|
||||
"9": "[in progress] Add icons to categories and tagging (how many users are using this tag, not how often the tag is used generally)",
|
||||
"8": "Updated to actual eslint version for source code quality",
|
||||
"7": "[in progress] Coding a blacklist for Nitro to hide unwanted user's content",
|
||||
"6": "Coding a feature, so users who haven't donated yet, get a chance later to do so",
|
||||
"5": "Coding a database query to identify users, which had not completed the registration dialog",
|
||||
"4": "Introduction of a versioning scheme to identify the running social network code version",
|
||||
"3": "Login for the new Nitro version with a fancier interface.",
|
||||
"2": "Moved project planning to Zenhub",
|
||||
"1": "Decided to use Scrum for project management",
|
||||
"0": "Started work on \"Nitro\". The database and the server interface are replaced:"
|
||||
},
|
||||
"code-of-conduct": {
|
||||
"subheader": "for the social network of the Human Connection gGmbH",
|
||||
"preamble": {
|
||||
"title": "Preamble",
|
||||
"description": "Human Connection is a non-profit social knowledge and action network of the next generation. By people - for people. Open Source, fair and transparent. For positive local and global change in all areas of life. We completely redesign the public exchange of knowledge, ideas and projects. The functions of Human Connection bring people together - offline and online - so that we can make the world a better place."
|
||||
},
|
||||
"purpose": {
|
||||
"title": "Purpose",
|
||||
"description": "With these code of conduct we regulate the essential principles for behavior in our social network. The United Nations Charter of Human Rights is our orientation and forms the heart of our understanding of values. The code of conduct serves as guiding principles for our personal appearance and interaction with one another. Anyone who is active as a user in the Human Connection Network, writes articles, comments or contacts other users, including those outside the network,acknowledges these rules of conduct as binding."
|
||||
},
|
||||
"expected-behaviour": {
|
||||
"title": "Expected Behaviour",
|
||||
"description": "The following behaviors are expected and requested of all community members:",
|
||||
"list": {
|
||||
"0": "Exercise consideration and respect in your speech and actions.",
|
||||
"1": "Attempt collaboration before conflict.",
|
||||
"2": "Refrain from demeaning, discriminatory, or harassing behavior and speech.",
|
||||
"3": "Be mindful of your surroundings and of your fellow participants. Alert community leaders if you notice a dangerous situation, someone in distress, or violations of this Code of Conduct, even if they seem inconsequential."
|
||||
}
|
||||
},
|
||||
"unacceptable-behaviour": {
|
||||
"title": "Unacceptable Behavior",
|
||||
"description": "The following behaviors are unacceptable within our community:",
|
||||
"list": {
|
||||
"0": "Discriminatory posts, comments, utterances or insults, particularly those relating to gender, sexual orientation, race, religion, political or philosophical orientation or disability.",
|
||||
"1": "Posting or linking of clearly pornographic material.",
|
||||
"2": "Glorification or trivialization of cruel or inhuman acts of violence.",
|
||||
"3": "The disclosure of others' personal information without their consent or threat there of (\"doxing\").",
|
||||
"4": "Intentional intimidation, stalking or persecution.",
|
||||
"5": "Advertising products and services with commercial intent.",
|
||||
"6": "Criminal behavior or violation of German law.",
|
||||
"7": "Endorse or encourage such conduct."
|
||||
}
|
||||
},
|
||||
"consequences": {
|
||||
"title": "Consequences of Unacceptable Behavior",
|
||||
"description": "If a community member exhibits unacceptable behaviour, the responsible operators, moderators and administrators of the network may take appropriate measures, including but not limited to:",
|
||||
"list": {
|
||||
"0": "Request for immediate cessation of unacceptable conduct",
|
||||
"1": "Locking or deleting comments",
|
||||
"2": "Temporary exclusion from the respective post or contribution",
|
||||
"3": "Blocking or deleting of content",
|
||||
"4": "Temporary withdrawal of write permissions",
|
||||
"5": "Temporary exclusion from the network",
|
||||
"6": "Final exclusion from the network",
|
||||
"7": "Violations of German law can be reported.",
|
||||
"8": "Advocacy or encouragement to these behaviors."
|
||||
}
|
||||
},
|
||||
"get-help": "If you are subject to or witness unacceptable behavior, or have any other concerns, please notify a community organizer as soon as possible and link or refer to the corresponding content:"
|
||||
},
|
||||
"termsAndConditions": {
|
||||
"risk": {
|
||||
"title": "Risk of accident",
|
||||
"description": "This is a test version! All data, your profile and the server can be completely destroyed, wiped out, lost, burnt and eventually synchronised near Alpha Centauri at any time. Use on your own risk. Commercial effects are not likely though."
|
||||
},
|
||||
"data-privacy": {
|
||||
"title": "You and your data",
|
||||
"description": "Please note that the content of the alpha version will be used for public web presentations etc. but we are sure, this is in your interest. Avoid real names and use anonymous profile pictures without your face. You can find more information in our <a href=\"/data-privacy\">data privacy policy</a>"
|
||||
},
|
||||
"work-in-progress": {
|
||||
"title": "Work in progress",
|
||||
"description": "This is still a test version. Please excuse if some applications are not working, blocking, irritating, displayed falsely or not able to be clicked on. Please report faults and bugs! <a href=\"mailto:support@human-connection.org\" target=\"_blank\">mailto:support@human-connection.org</a>"
|
||||
},
|
||||
"code-of-conduct": {
|
||||
"title": "Code of conduct",
|
||||
"description": "The <a href=\"/code-of-conduct\">code of conduct</a> serves as guiding principles for our personal appearance and interaction with one another. Anyone who is active as a user in the Human Connection Network, writes articles, comments or contacts other users, including those outside the network, acknowledges these rules of conduct as binding."
|
||||
},
|
||||
"moderation": {
|
||||
"title": "Moderation",
|
||||
"description": "As long as there is no community moderation-system in operation, a rainbow colored unicorn decides, if you are physically and mentally stable enough to operate our test version. The unicorn can delete you from the alpha version at any time. So be so kind and leave rainbow food!"
|
||||
},
|
||||
"fairness": {
|
||||
"title": "Fairness",
|
||||
"description": "If, against all expectations, our alpha version is not to your liking, we return your monthly payment within the first two months. Please send a mail to: <a href=\"mailto:info@human-connection.org\" target=\"_blank\"> info@human-connection.org </a> Note that more features are added on a regular basis."
|
||||
},
|
||||
"questions": {
|
||||
"title": "Questions",
|
||||
"description": "You can find the dates and links to our zoom-rooms here: <a href=\"https://human-connection.org/en/events/\" target=\"_blank\" >https://human-connection.org/en/events/ </a>"
|
||||
},
|
||||
"human-connection": {
|
||||
"title": "By humans for humans",
|
||||
"description": "Please help us to get new donators for Human Connection, so the network can take off as soon as possible. <a href=\"https://human-connection.org/\" target=\"_blank\"> https://human-connection.org </a>"
|
||||
},
|
||||
"have-fun": "Now have fun with the alpha version of Human Connection! For the first universal peace. ♥︎",
|
||||
"closing": "Thank you very much <br> <br> your Human Connection Team"
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
"made": "Con ❤ realizado",
|
||||
"imprint": "Pie de imprenta",
|
||||
"termsAc": "términos y condiciones",
|
||||
"privacy": "protección de datos",
|
||||
"data-privacy": "protección de datos",
|
||||
"changelog": "Cambios e historia",
|
||||
"contact": "Contacto",
|
||||
"tribunal": "tribunal de registro",
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
"made": "Avec ❤ fait",
|
||||
"imprint": "Mentions légales",
|
||||
"termsAc": "modalités et conditions",
|
||||
"privacy": "protection des données",
|
||||
"data-privacy": "protection des données",
|
||||
"changelog": "Changements et historique",
|
||||
"contact": "Contacter",
|
||||
"tribunal": "tribunal de registre",
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
"made": "Con ❤ fatto",
|
||||
"imprint": "Impressum",
|
||||
"termsAc": "Condizioni d'uso",
|
||||
"privacy": "protezione dei dati",
|
||||
"data-privacy": "protezione dei dati",
|
||||
"changelog": "Cambiamenti e storia",
|
||||
"contact": "Contatto",
|
||||
"tribunal": "registro tribunale",
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
"made": "Met ❤ gemaakt",
|
||||
"imprint": "Afdruk",
|
||||
"termsAc": "Gebruiksvoorwaarden",
|
||||
"privacy": "gegevensbescherming",
|
||||
"data-privacy": "gegevensbescherming",
|
||||
"changelog": "Veranderingen & Geschiedenis",
|
||||
"contact": "contact",
|
||||
"tribunal": "registerrechtbank",
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
"made": "Z ❤ zrobiony",
|
||||
"imprint": "Nadruk",
|
||||
"termsAc": "Warunki użytkowania",
|
||||
"privacy": "ochrona danych",
|
||||
"data-privacy": "ochrona danych",
|
||||
"changelog": "Zmiany i historia",
|
||||
"contact": "Kontakt",
|
||||
"tribunal": "sąd rejestrowy",
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
"made": "Com ❤ feito",
|
||||
"imprint": "Impressão",
|
||||
"termsAc": "termos e condições",
|
||||
"privacy": "protecção de dados",
|
||||
"data-privacy": "protecção de dados",
|
||||
"changelog": "Mudanças e Histórico",
|
||||
"contact": "Contato",
|
||||
"tribunal": "tribunal",
|
||||
|
||||
@ -39,6 +39,11 @@ module.exports = {
|
||||
'registration-verify-code',
|
||||
'registration-create-user-account',
|
||||
'pages-slug',
|
||||
'imprint',
|
||||
'terms-and-conditions',
|
||||
'code-of-conduct',
|
||||
'data-privacy',
|
||||
'changelog',
|
||||
],
|
||||
// pages to keep alive
|
||||
keepAlivePages: ['index'],
|
||||
|
||||
@ -12,6 +12,7 @@
|
||||
"scripts": {
|
||||
"dev": "cross-env NODE_ENV=development nodemon server/index.js --watch server",
|
||||
"dev:styleguide": "cross-env STYLEGUIDE_DEV=true yarn dev",
|
||||
"storybook": "start-storybook -p 3002 -c storybook/",
|
||||
"build": "nuxt build",
|
||||
"start": "cross-env node server/index.js",
|
||||
"generate": "nuxt generate",
|
||||
@ -76,13 +77,16 @@
|
||||
"vue-count-to": "~1.0.13",
|
||||
"vue-izitoast": "1.1.2",
|
||||
"vue-sweetalert-icons": "~3.2.0",
|
||||
"vuex-i18n": "~1.13.0",
|
||||
"vuex-i18n": "~1.13.1",
|
||||
"zxcvbn": "^4.4.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "~7.5.5",
|
||||
"@babel/plugin-syntax-dynamic-import": "^7.2.0",
|
||||
"@babel/preset-env": "~7.5.5",
|
||||
"@storybook/addon-a11y": "^5.1.9",
|
||||
"@storybook/addon-actions": "^5.1.9",
|
||||
"@storybook/vue": "~5.1.9",
|
||||
"@vue/cli-shared-utils": "~3.10.0",
|
||||
"@vue/eslint-config-prettier": "~5.0.0",
|
||||
"@vue/server-test-utils": "~1.0.0-beta.29",
|
||||
@ -90,6 +94,10 @@
|
||||
"babel-core": "~7.0.0-bridge.0",
|
||||
"babel-eslint": "~10.0.2",
|
||||
"babel-jest": "~24.8.0",
|
||||
"babel-loader": "~8.0.6",
|
||||
"babel-preset-vue": "~2.0.2",
|
||||
"css-loader": "~2.1.1",
|
||||
"core-js": "~2.6.9",
|
||||
"eslint": "~5.16.0",
|
||||
"eslint-config-prettier": "~6.0.0",
|
||||
"eslint-config-standard": "~12.0.0",
|
||||
@ -109,8 +117,12 @@
|
||||
"nodemon": "~1.19.1",
|
||||
"prettier": "~1.18.2",
|
||||
"sass-loader": "~7.1.0",
|
||||
"style-loader": "~0.23.1",
|
||||
"style-resources-loader": "~1.2.1",
|
||||
"tippy.js": "^4.3.5",
|
||||
"vue-jest": "~3.0.4",
|
||||
"vue-svg-loader": "~0.12.0"
|
||||
"vue-loader": "~15.7.0",
|
||||
"vue-svg-loader": "~0.12.0",
|
||||
"vue-template-compiler": "^2.6.10"
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,16 +5,78 @@
|
||||
</ds-space>
|
||||
|
||||
<ds-container>
|
||||
<ds-space margin-top="large">
|
||||
<ds-text>{{ $t('site.changelog') }}</ds-text>
|
||||
<ds-text>...</ds-text>
|
||||
</ds-space>
|
||||
<strong>
|
||||
{{ Date.parse('2019-03') | date('MMMM yyyy') }}
|
||||
</strong>
|
||||
<ul>
|
||||
<li>{{ $t('changelog.24') }}</li>
|
||||
<li>{{ $t('changelog.23') }}</li>
|
||||
<li>{{ $t('changelog.22') }}</li>
|
||||
<li>{{ $t('changelog.21') }}</li>
|
||||
<li>{{ $t('changelog.20') }}</li>
|
||||
</ul>
|
||||
<strong>
|
||||
{{ Date.parse('2019-02') | date('MMMM yyyy') }}
|
||||
</strong>
|
||||
<ul>
|
||||
<li>{{ $t('changelog.19') }}</li>
|
||||
<li>{{ $t('changelog.18') }}</li>
|
||||
<li>{{ $t('changelog.17') }}</li>
|
||||
</ul>
|
||||
<strong>
|
||||
{{ Date.parse('2019-01') | date('MMMM yyyy') }}
|
||||
</strong>
|
||||
<ul>
|
||||
<li>{{ $t('changelog.16') }}</li>
|
||||
<li>{{ $t('changelog.15') }}</li>
|
||||
<li>{{ $t('changelog.14') }}</li>
|
||||
<li>{{ $t('changelog.13') }}</li>
|
||||
<li>{{ $t('changelog.12') }}</li>
|
||||
<li>{{ $t('changelog.11') }}</li>
|
||||
<li>{{ $t('changelog.10') }}</li>
|
||||
</ul>
|
||||
<strong>
|
||||
{{ Date.parse('2018-12') | date('MMMM yyyy') }}
|
||||
</strong>
|
||||
<ul>
|
||||
<li>{{ $t('changelog.9') }}</li>
|
||||
<li>{{ $t('changelog.8') }}</li>
|
||||
<li>{{ $t('changelog.7') }}</li>
|
||||
</ul>
|
||||
<strong>
|
||||
{{ Date.parse('2018-11') | date('MMMM yyyy') }}
|
||||
</strong>
|
||||
<ul>
|
||||
<li>{{ $t('changelog.6') }}</li>
|
||||
<li>{{ $t('changelog.5') }}</li>
|
||||
<li>{{ $t('changelog.4') }}</li>
|
||||
<li>{{ $t('changelog.3') }}</li>
|
||||
<li>{{ $t('changelog.2') }}</li>
|
||||
<li>{{ $t('changelog.1') }}</li>
|
||||
</ul>
|
||||
<strong>
|
||||
{{ Date.parse('2018-10') | date('MMMM yyyy') }}
|
||||
</strong>
|
||||
<ul>
|
||||
<li>
|
||||
{{ $t('changelog.0') }}
|
||||
<br />
|
||||
<a
|
||||
class="hc-editor-link-blot"
|
||||
href="https://www.youtube.com/watch?v=8j2H0vnQEoQ"
|
||||
target="_blank"
|
||||
>
|
||||
https://www.youtube.com/watch?v=8j2H0vnQEoQ
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</ds-container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
layout: 'default',
|
||||
head() {
|
||||
return {
|
||||
title: this.$t('site.changelog'),
|
||||
|
||||
64
webapp/pages/code-of-conduct.vue
Normal file
64
webapp/pages/code-of-conduct.vue
Normal file
@ -0,0 +1,64 @@
|
||||
<template>
|
||||
<div>
|
||||
<ds-space>
|
||||
<ds-heading tag="h2">{{ $t('site.code-of-conduct') }}</ds-heading>
|
||||
<p>{{ $t('code-of-conduct.subheader') }}</p>
|
||||
</ds-space>
|
||||
|
||||
<ds-container>
|
||||
<div v-for="section in sections" :key="section">
|
||||
<strong>{{ $t(`code-of-conduct.${section}.title`) }}</strong>
|
||||
<p>{{ $t(`code-of-conduct.${section}.description`) }}</p>
|
||||
</div>
|
||||
|
||||
<br />
|
||||
|
||||
<div v-for="section in listSections" :key="section.key">
|
||||
<strong>{{ $t(`code-of-conduct.${section.key}.title`) }}</strong>
|
||||
<p>{{ $t(`code-of-conduct.${section.key}.description`) }}</p>
|
||||
<ul>
|
||||
<li v-for="i in section.items" :key="i">
|
||||
{{ $t(`code-of-conduct.${section.key}.list.${i}`) }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
{{ $t('code-of-conduct.get-help') }}
|
||||
<a class="hc-editor-link-blot" href="moderation@human-connection.org" target="_blank">
|
||||
moderation@human-connection.org
|
||||
</a>
|
||||
</p>
|
||||
<br />
|
||||
</ds-container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
head() {
|
||||
return {
|
||||
title: this.$t('site.code-of-conduct'),
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
sections: ['preamble', 'purpose'],
|
||||
listSections: [
|
||||
{
|
||||
key: 'expected-behaviour',
|
||||
items: [...Array(4).keys()],
|
||||
},
|
||||
{
|
||||
key: 'unacceptable-behaviour',
|
||||
items: [...Array(8).keys()],
|
||||
},
|
||||
{
|
||||
key: 'consequences',
|
||||
items: [...Array(9).keys()],
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@ -1,11 +1,11 @@
|
||||
<template>
|
||||
<div>
|
||||
<ds-space>
|
||||
<ds-heading tag="h2">{{ $t('site.privacy') }}</ds-heading>
|
||||
<ds-heading tag="h2">{{ $t('site.data-privacy') }}</ds-heading>
|
||||
</ds-space>
|
||||
<ds-container>
|
||||
<ds-space margin-top="large">
|
||||
<ds-text>{{ $t('site.privacy') }}</ds-text>
|
||||
<ds-text>{{ $t('site.data-privacy') }}</ds-text>
|
||||
<ds-text>...</ds-text>
|
||||
</ds-space>
|
||||
</ds-container>
|
||||
@ -16,7 +16,7 @@
|
||||
export default {
|
||||
head() {
|
||||
return {
|
||||
title: this.$t('site.privacy'),
|
||||
title: this.$t('site.data-privacy'),
|
||||
}
|
||||
},
|
||||
}
|
||||
@ -23,9 +23,7 @@
|
||||
</ds-flex-item>
|
||||
<ds-flex-item :width="{ base: '100%', sm: '50%' }" centered>
|
||||
<ds-space margin="small">
|
||||
<ds-text size="small">
|
||||
{{ $t('login.copy') }}
|
||||
</ds-text>
|
||||
<ds-text size="small">{{ $t('login.copy') }}</ds-text>
|
||||
</ds-space>
|
||||
<form :disabled="pending" @submit.prevent="onSubmit">
|
||||
<ds-input
|
||||
@ -46,9 +44,7 @@
|
||||
type="password"
|
||||
/>
|
||||
<ds-space class="password-reset-link" margin-bottom="large">
|
||||
<nuxt-link to="/password-reset/request">
|
||||
{{ $t('login.forgotPassword') }}
|
||||
</nuxt-link>
|
||||
<nuxt-link to="/password-reset/request">{{ $t('login.forgotPassword') }}</nuxt-link>
|
||||
</ds-space>
|
||||
<ds-button
|
||||
:loading="pending"
|
||||
@ -84,7 +80,7 @@ export default {
|
||||
components: {
|
||||
LocaleSwitch,
|
||||
},
|
||||
layout: 'blank',
|
||||
layout: 'default',
|
||||
data() {
|
||||
return {
|
||||
ready: false,
|
||||
|
||||
@ -6,9 +6,7 @@
|
||||
<img style="width: 200px;" src="/img/sign-up/onourjourney.png" alt="Human Connection" />
|
||||
</ds-space>
|
||||
<ds-space style="text-align: center;" margin-top="small" margin-bottom="xxx-small" centered>
|
||||
<ds-heading tag="h3" soft>
|
||||
Logging out...
|
||||
</ds-heading>
|
||||
<ds-heading tag="h3" soft>Logging out...</ds-heading>
|
||||
</ds-space>
|
||||
</ds-flex-item>
|
||||
</ds-flex>
|
||||
@ -17,7 +15,7 @@
|
||||
|
||||
<script>
|
||||
export default {
|
||||
layout: 'blank',
|
||||
layout: 'default',
|
||||
async beforeCreate() {
|
||||
await this.$store.dispatch('auth/logout')
|
||||
this.$router.replace('/')
|
||||
|
||||
@ -20,10 +20,7 @@
|
||||
<ds-space margin-bottom="small" />
|
||||
<ds-heading tag="h3" no-margin>{{ post.title }}</ds-heading>
|
||||
<ds-space margin-bottom="small" />
|
||||
<!-- Content -->
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<!-- TODO: replace editor content with tiptap render view -->
|
||||
<div class="content hc-editor-content" v-html="post.content" />
|
||||
<content-viewer class="content" :content="post.content" />
|
||||
<!-- eslint-enable vue/no-v-html -->
|
||||
<ds-space margin="xx-large" />
|
||||
<!-- Categories -->
|
||||
@ -78,6 +75,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ContentViewer from '~/components/Editor/ContentViewer'
|
||||
import HcCategory from '~/components/Category'
|
||||
import HcTag from '~/components/Tag'
|
||||
import ContentMenu from '~/components/ContentMenu'
|
||||
@ -104,6 +102,7 @@ export default {
|
||||
HcCommentForm,
|
||||
HcCommentList,
|
||||
HcEmotionsButtons,
|
||||
ContentViewer,
|
||||
},
|
||||
head() {
|
||||
return {
|
||||
|
||||
@ -1,22 +1,46 @@
|
||||
<template>
|
||||
<div>
|
||||
<ds-space>
|
||||
<ds-heading tag="h2">{{ $t('site.termsAc') }}</ds-heading>
|
||||
<ds-heading tag="h2">{{ $t('site.termsAndConditions') }}</ds-heading>
|
||||
</ds-space>
|
||||
<ds-container>
|
||||
<ds-space margin-top="large">
|
||||
<ds-text>{{ $t('site.termsAc') }}</ds-text>
|
||||
<ds-text v-html="$t('terms.text')"></ds-text>
|
||||
</ds-space>
|
||||
<div>
|
||||
<ol>
|
||||
<li v-for="section in sections" :key="section">
|
||||
<strong>{{ $t(`termsAndConditions.${section}.title`) }}:</strong>
|
||||
<p v-html="$t(`termsAndConditions.${section}.description`)" />
|
||||
</li>
|
||||
</ol>
|
||||
<p>{{ $t('termsAndConditions.have-fun') }}</p>
|
||||
<br />
|
||||
<p>
|
||||
<strong v-html="$t('termsAndConditions.closing')" />
|
||||
</p>
|
||||
</div>
|
||||
</ds-container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
layout: 'default',
|
||||
head() {
|
||||
return {
|
||||
title: this.$t('site.termsAc'),
|
||||
title: this.$t('site.termsAndConditions'),
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
sections: [
|
||||
'risk',
|
||||
'data-privacy',
|
||||
'work-in-progress',
|
||||
'code-of-conduct',
|
||||
'moderation',
|
||||
'fairness',
|
||||
'questions',
|
||||
'human-connection',
|
||||
],
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
3
webapp/storybook/addons.js
Normal file
3
webapp/storybook/addons.js
Normal file
@ -0,0 +1,3 @@
|
||||
import '@storybook/addon-actions/register'
|
||||
import '@storybook/addon-a11y/register'
|
||||
// import '@storybook/addon-links/register'
|
||||
32
webapp/storybook/config.js
Normal file
32
webapp/storybook/config.js
Normal file
@ -0,0 +1,32 @@
|
||||
import { configure } from '@storybook/vue'
|
||||
import Vue from 'vue'
|
||||
import Vuex from 'vuex'
|
||||
import { action } from '@storybook/addon-actions'
|
||||
|
||||
Vue.use(Vuex)
|
||||
Vue.component('nuxt-link', {
|
||||
props: ['to'],
|
||||
methods: {
|
||||
log() {
|
||||
action('link clicked')(this.to)
|
||||
},
|
||||
},
|
||||
template: '<a href="#" @click.prevent="log()"><slot>NuxtLink</slot></a>',
|
||||
})
|
||||
Vue.component('no-ssr', {
|
||||
render() {
|
||||
return this.$slots.default
|
||||
},
|
||||
})
|
||||
Vue.component('v-popover', {
|
||||
template: '<div><slot>Popover Content</slot></div>',
|
||||
})
|
||||
|
||||
// Automatically import all files ending in *.stories.js
|
||||
const req = require.context('../components', true, /.story.js$/)
|
||||
|
||||
function loadStories() {
|
||||
req.keys().forEach(req)
|
||||
}
|
||||
|
||||
configure(loadStories, module)
|
||||
57
webapp/storybook/helpers.js
Normal file
57
webapp/storybook/helpers.js
Normal file
@ -0,0 +1,57 @@
|
||||
import Vue from 'vue'
|
||||
import Vuex from 'vuex'
|
||||
import vuexI18n from 'vuex-i18n/dist/vuex-i18n.umd.js'
|
||||
import Styleguide from '@human-connection/styleguide'
|
||||
import Filters from '~/plugins/vue-filters'
|
||||
import layout from './layout.vue'
|
||||
|
||||
const helpers = {
|
||||
init(options = {}) {
|
||||
Vue.use(Vuex)
|
||||
Vue.use(Styleguide)
|
||||
Vue.use(Filters)
|
||||
|
||||
Vue.use(vuexI18n.plugin, helpers.store)
|
||||
Vue.i18n.add('en', require('~/locales/en.json'))
|
||||
Vue.i18n.add('de', require('~/locales/de.json'))
|
||||
Vue.i18n.set('en')
|
||||
Vue.i18n.fallback('en')
|
||||
|
||||
const { plugins = [] } = options
|
||||
plugins.forEach(plugin => Vue.use(plugin))
|
||||
},
|
||||
store: new Vuex.Store({
|
||||
modules: {
|
||||
auth: {
|
||||
namespaced: true,
|
||||
getters: {
|
||||
user(state) {
|
||||
return { id: 1, name: 'admin' }
|
||||
},
|
||||
},
|
||||
},
|
||||
editor: {
|
||||
namespaced: true,
|
||||
getters: {
|
||||
placeholder(state) {
|
||||
return 'Leave your inspirational thoughts...'
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
layout(storyFn) {
|
||||
const ctx = storyFn()
|
||||
return {
|
||||
components: { ctx, layout },
|
||||
template: `
|
||||
<layout>
|
||||
<ds-flex>
|
||||
<ctx />
|
||||
</ds-flex>
|
||||
</layout>`,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
export default helpers
|
||||
14
webapp/storybook/layout.vue
Normal file
14
webapp/storybook/layout.vue
Normal file
@ -0,0 +1,14 @@
|
||||
<template>
|
||||
<ds-container class="container">
|
||||
<slot />
|
||||
</ds-container>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../node_modules/@human-connection/styleguide/dist/system.css';
|
||||
@import '~/assets/styles/main.scss';
|
||||
|
||||
.container {
|
||||
padding: 5rem;
|
||||
}
|
||||
</style>
|
||||
43
webapp/storybook/webpack.config.js
Normal file
43
webapp/storybook/webpack.config.js
Normal file
@ -0,0 +1,43 @@
|
||||
const path = require('path')
|
||||
const nuxtConf = require('../nuxt.config')
|
||||
const srcDir = `../${nuxtConf.srcDir || ''}`
|
||||
const rootDir = `../${nuxtConf.rootDir || ''}`
|
||||
|
||||
// Export a function. Accept the base config as the only param.
|
||||
module.exports = async ({ config, mode }) => {
|
||||
// `mode` has a value of 'DEVELOPMENT' or 'PRODUCTION'
|
||||
// You can change the configuration based on that.
|
||||
// 'PRODUCTION' is used when building the static version of storybook.
|
||||
|
||||
// Make whatever fine-grained changes you need
|
||||
config.module.rules.push({
|
||||
test: /\.scss$/,
|
||||
use: [
|
||||
{ loader: 'style-loader' },
|
||||
{ loader: 'css-loader', options: { sourceMap: true } },
|
||||
{ loader: 'sass-loader', options: { sourceMap: true } },
|
||||
{
|
||||
loader: 'style-resources-loader',
|
||||
options: {
|
||||
patterns: [
|
||||
path.resolve(
|
||||
__dirname,
|
||||
'../node_modules/@human-connection/styleguide/dist/shared.scss',
|
||||
),
|
||||
],
|
||||
injector: 'prepend',
|
||||
},
|
||||
},
|
||||
],
|
||||
include: path.resolve(__dirname, '../'),
|
||||
})
|
||||
|
||||
config.resolve.alias = {
|
||||
...config.resolve.alias,
|
||||
'~~': path.resolve(__dirname, rootDir),
|
||||
'~': path.resolve(__dirname, srcDir),
|
||||
}
|
||||
|
||||
// Return the altered config
|
||||
return config
|
||||
}
|
||||
2541
webapp/yarn.lock
2541
webapp/yarn.lock
File diff suppressed because it is too large
Load Diff
@ -3579,10 +3579,10 @@ neo4j-driver@^1.6.3, neo4j-driver@^1.7.5:
|
||||
text-encoding-utf-8 "^1.0.2"
|
||||
uri-js "^4.2.2"
|
||||
|
||||
neode@^0.3.0:
|
||||
version "0.3.0"
|
||||
resolved "https://registry.yarnpkg.com/neode/-/neode-0.3.0.tgz#a4a41234fac23236db6b589ec2b505ad6e5fd832"
|
||||
integrity sha512-V6uQhap7FDwbeC+mH6JEI352QSou4Ukj7vs/bGZSrVlMZKVS8vs/mbQYXoFdCkmQJuUtJWqO9wmtWg5GjCaNDQ==
|
||||
neode@^0.3.1:
|
||||
version "0.3.1"
|
||||
resolved "https://registry.yarnpkg.com/neode/-/neode-0.3.1.tgz#d40147bf20d6951b69c9d392fbdd322aeca07816"
|
||||
integrity sha512-SdaJmdjQ3PWOH6W1H8Xgd2CLyJs+BPPXPt0jOVNs7naeQH8nWPP6ixDqI6NWDCxwecTdNl//fpAicB9I6hCwEw==
|
||||
dependencies:
|
||||
"@hapi/joi" "^15.1.0"
|
||||
dotenv "^4.0.0"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user