Merge branch 'master' of github.com:Human-Connection/Human-Connection into 1017-send-out-notifications-on-create-omment

This commit is contained in:
Matt Rider 2019-08-08 09:10:18 +02:00
commit 21aa729edb
62 changed files with 4459 additions and 589 deletions

View File

@ -55,7 +55,7 @@
"cheerio": "~1.0.0-rc.3", "cheerio": "~1.0.0-rc.3",
"cors": "~2.8.5", "cors": "~2.8.5",
"cross-env": "~5.2.0", "cross-env": "~5.2.0",
"date-fns": "2.0.0-beta.3", "date-fns": "2.0.0-beta.4",
"debug": "~4.1.1", "debug": "~4.1.1",
"dotenv": "~8.0.0", "dotenv": "~8.0.0",
"express": "^4.17.1", "express": "^4.17.1",
@ -78,7 +78,7 @@
"metascraper-date": "^5.6.5", "metascraper-date": "^5.6.5",
"metascraper-description": "^5.6.5", "metascraper-description": "^5.6.5",
"metascraper-image": "^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-lang-detector": "^4.8.5",
"metascraper-logo": "^5.6.5", "metascraper-logo": "^5.6.5",
"metascraper-publisher": "^5.6.5", "metascraper-publisher": "^5.6.5",
@ -87,9 +87,10 @@
"metascraper-url": "^5.6.5", "metascraper-url": "^5.6.5",
"metascraper-video": "^5.6.5", "metascraper-video": "^5.6.5",
"metascraper-youtube": "^5.6.3", "metascraper-youtube": "^5.6.3",
"minimatch": "^3.0.4",
"neo4j-driver": "~1.7.5", "neo4j-driver": "~1.7.5",
"neo4j-graphql-js": "^2.6.3", "neo4j-graphql-js": "^2.6.3",
"neode": "^0.3.0", "neode": "^0.3.1",
"node-fetch": "~2.6.0", "node-fetch": "~2.6.0",
"nodemailer": "^6.3.0", "nodemailer": "^6.3.0",
"npm-run-all": "~4.1.5", "npm-run-all": "~4.1.5",

View File

@ -64,7 +64,7 @@ describe('currentUser { notifications }', () => {
let post let post
const title = 'Mentioning Al Capone' const title = 'Mentioning Al Capone'
const content = 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 () => { beforeEach(async () => {
const createPostMutation = gql` const createPostMutation = gql`
@ -88,7 +88,7 @@ describe('currentUser { notifications }', () => {
it('sends you a notification', async () => { it('sends you a notification', async () => {
const expectedContent = 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 = { const expected = {
currentUser: { currentUser: {
notifications: [ notifications: [
@ -108,14 +108,22 @@ describe('currentUser { notifications }', () => {
).resolves.toEqual(expected) ).resolves.toEqual(expected)
}) })
describe('who mentions me again', () => { describe('who mentions me many times', () => {
beforeEach(async () => { beforeEach(async () => {
const updatedContent = `${post.content} One more mention to <a href="/profile/you" class="mention">@al-capone</a>` const updatedContent = `
// The response `post.content` contains a link but the XSSmiddleware One more mention to
// should have the `mention` CSS class removed. I discovered this <a data-mention-id="you" class="mention" href="/profile/you">
// during development and thought: A feature not a bug! This way we @al-capone
// can encode a re-mentioning of users when you edit your post or </a>
// comment. 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` const updatePostMutation = gql`
mutation($id: ID!, $title: String!, $content: String!) { mutation($id: ID!, $title: String!, $content: String!) {
UpdatePost(id: $id, content: $content, title: $title) { UpdatePost(id: $id, content: $content, title: $title) {
@ -136,7 +144,7 @@ describe('currentUser { notifications }', () => {
it('creates exactly one more notification', async () => { it('creates exactly one more notification', async () => {
const expectedContent = 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 = { const expected = {
currentUser: { currentUser: {
notifications: [ notifications: [

View File

@ -1,20 +1,13 @@
import cheerio from 'cheerio' import cheerio from 'cheerio'
const ID_REGEX = /\/profile\/([\w\-.!~*'"(),]+)/g
export default function(content) { export default function(content) {
if (!content) return [] if (!content) return []
const $ = cheerio.load(content) const $ = cheerio.load(content)
const urls = $('.mention') let userIds = $('a.mention[data-mention-id]')
.map((_, el) => { .map((_, el) => {
return $(el).attr('href') return $(el).attr('data-mention-id')
}) })
.get() .get()
const ids = [] userIds = userIds.map(id => id.trim()).filter(id => !!id)
urls.forEach(url => { return userIds
let match
while ((match = ID_REGEX.exec(url)) != null) {
ids.push(match[1])
}
})
return ids
} }

View File

@ -1,5 +1,12 @@
import extractMentionedUsers from './extractMentionedUsers' 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('extractMentionedUsers', () => {
describe('content undefined', () => { describe('content undefined', () => {
it('returns empty array', () => { it('returns empty array', () => {
@ -7,53 +14,17 @@ describe('extractMentionedUsers', () => {
}) })
}) })
describe('searches through links', () => {
it('ignores links without .mention class', () => { it('ignores links without .mention class', () => {
const content = expect(extractMentionedUsers(contentWithPlainLinks)).toEqual([])
'<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([])
}) })
describe('given a link with .mention class', () => { describe('given a link with .mention class and `data-mention-id` attribute ', () => {
it('extracts ids', () => { it('extracts ids', () => {
const content = expect(extractMentionedUsers(contentWithMentions)).toEqual(['u3'])
'<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('ignores empty `data-mention-id` attributes', () => {
it('with slug and id', () => { expect(extractMentionedUsers(contentEmptyMentions)).toEqual([])
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([])
})
})
}) })
}) })
}) })

View File

@ -2,30 +2,16 @@ import walkRecursive from '../helpers/walkRecursive'
// import { getByDot, setByDot, getItems, replaceItems } from 'feathers-hooks-common' // import { getByDot, setByDot, getItems, replaceItems } from 'feathers-hooks-common'
import sanitizeHtml from 'sanitize-html' import sanitizeHtml from 'sanitize-html'
// import { isEmpty, intersection } from 'lodash' // import { isEmpty, intersection } from 'lodash'
import cheerio from 'cheerio'
import linkifyHtml from 'linkifyjs/html' 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) { function clean(dirty) {
if (!dirty) { if (!dirty) {
return dirty return dirty
} }
// Convert embeds to a-tags
dirty = embedToAnchor(dirty)
dirty = linkifyHtml(dirty) dirty = linkifyHtml(dirty)
dirty = sanitizeHtml(dirty, { dirty = sanitizeHtml(dirty, {
allowedTags: [ allowedTags: [
'iframe',
'img', 'img',
'p', 'p',
'h3', 'h3',
@ -50,35 +36,24 @@ function clean(dirty) {
a: ['href', 'class', 'target', 'data-*', 'contenteditable'], a: ['href', 'class', 'target', 'data-*', 'contenteditable'],
span: ['contenteditable', 'class', 'data-*'], span: ['contenteditable', 'class', 'data-*'],
img: ['src'], img: ['src'],
iframe: ['src', 'class', 'frameborder', 'allowfullscreen'],
}, },
allowedIframeHostnames: ['www.youtube.com', 'player.vimeo.com'], allowedIframeHostnames: ['www.youtube.com', 'player.vimeo.com'],
parser: { parser: {
lowerCaseTags: true, lowerCaseTags: true,
}, },
transformTags: { transformTags: {
iframe: function(tagName, attribs) {
return {
tagName: 'a',
text: attribs.src,
attribs: {
href: attribs.src,
target: '_blank',
'data-url-embed': '',
},
}
},
h1: 'h3', h1: 'h3',
h2: 'h3', h2: 'h3',
h3: 'h3', h3: 'h3',
h4: 'h4', h4: 'h4',
h5: 'strong', h5: 'strong',
i: 'em', i: 'em',
a: function(tagName, attribs) { a: (tagName, attribs) => {
return { return {
tagName: 'a', tagName: 'a',
attribs: { attribs: {
href: attribs.href, ...attribs,
href: attribs.href || '',
target: '_blank', target: '_blank',
rel: 'noopener noreferrer nofollow', rel: 'noopener noreferrer nofollow',
}, },
@ -86,33 +61,6 @@ function clean(dirty) {
}, },
b: 'strong', b: 'strong',
s: 'strike', 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 dirty = dirty
// remove all tags with "space only" // remove all tags with "space only"
.replace(/<[a-z-]+>[\s]+<\/[a-z-]+>/gim, '') .replace(/<[a-z-]+>[\s]+<\/[a-z-]+>/gim, '')
// remove all iframes
.replace(/(<iframe(?!.*?src=(['"]).*?\2)[^>]*)(>)[^>]*\/*>/gim, '')
.replace(/[\n]{3,}/gim, '\n\n') .replace(/[\n]{3,}/gim, '\n\n')
.replace(/(\r\n|\n\r|\r|\n)/g, '<br>$1') .replace(/(\r\n|\n\r|\r|\n)/g, '<br>$1')
@ -144,8 +90,7 @@ const fields = ['content', 'contentExcerpt']
export default { export default {
Mutation: async (resolve, root, args, context, info) => { Mutation: async (resolve, root, args, context, info) => {
args = walkRecursive(args, fields, clean) args = walkRecursive(args, fields, clean)
const result = await resolve(root, args, context, info) return resolve(root, args, context, info)
return result
}, },
Query: async (resolve, root, args, context, info) => { Query: async (resolve, root, args, context, info) => {
const result = await resolve(root, args, context, info) const result = await resolve(root, args, context, info)

View 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
}

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

View File

@ -1,12 +1,11 @@
import Metascraper from 'metascraper' import Metascraper from 'metascraper'
import fetch from 'node-fetch' import fetch from 'node-fetch'
import fs from 'fs'
import path from 'path'
import { ApolloError } from 'apollo-server' import { ApolloError } from 'apollo-server'
import isEmpty from 'lodash/isEmpty' import isEmpty from 'lodash/isEmpty'
import isArray from 'lodash/isArray' import isArray from 'lodash/isArray'
import mergeWith from 'lodash/mergeWith' import mergeWith from 'lodash/mergeWith'
import findProvider from './findProvider'
const error = require('debug')('embed:error') const error = require('debug')('embed:error')
@ -30,24 +29,11 @@ const metascraper = Metascraper([
// require('./rules/metascraper-embed')() // 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 fetchEmbed = async url => {
const provider = oEmbedProviders.find(provider => { let endpointUrl = findProvider(url)
return provider.provider_url.includes(url.hostname) if (!endpointUrl) return {}
}) endpointUrl = new URL(endpointUrl)
if (!provider) return {} endpointUrl.searchParams.append('url', url)
const {
endpoints: [endpoint],
} = provider
const endpointUrl = new URL(endpoint.url)
endpointUrl.searchParams.append('url', url.href)
endpointUrl.searchParams.append('format', 'json') endpointUrl.searchParams.append('format', 'json')
let json let json
try { try {
@ -70,7 +56,7 @@ const fetchEmbed = async url => {
const fetchResource = async url => { const fetchResource = async url => {
const response = await fetch(url) const response = await fetch(url)
const html = await response.text() const html = await response.text()
const resource = await metascraper({ html, url: url.href }) const resource = await metascraper({ html, url })
return { return {
sources: ['resource'], sources: ['resource'],
...resource, ...resource,
@ -78,12 +64,6 @@ const fetchResource = async url => {
} }
export default async function scrape(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 [meta, embed] = await Promise.all([fetchResource(url), fetchEmbed(url)])
const output = mergeWith(meta, embed, (objValue, srcValue) => { const output = mergeWith(meta, embed, (objValue, srcValue) => {
if (isArray(objValue)) { if (isArray(objValue)) {

View File

@ -972,7 +972,7 @@
url-regex "~4.1.1" url-regex "~4.1.1"
video-extensions "~1.1.0" video-extensions "~1.1.0"
"@metascraper/helpers@^5.6.3", "@metascraper/helpers@^5.6.5": "@metascraper/helpers@^5.6.5":
version "5.6.5" version "5.6.5"
resolved "https://registry.yarnpkg.com/@metascraper/helpers/-/helpers-5.6.5.tgz#6f42bd1a8e3243e051e7bb067145125cd6b37e09" resolved "https://registry.yarnpkg.com/@metascraper/helpers/-/helpers-5.6.5.tgz#6f42bd1a8e3243e051e7bb067145125cd6b37e09"
integrity sha512-j9qxXqZ9k/uNkABlsVjNN2Z5pVtukDmZMZ0ACsob+m5o8/yo87GvRf/UJfTPtog9vZ/QwkLav5Hhl+10NC7QLw== integrity sha512-j9qxXqZ9k/uNkABlsVjNN2Z5pVtukDmZMZ0ACsob+m5o8/yo87GvRf/UJfTPtog9vZ/QwkLav5Hhl+10NC7QLw==
@ -2814,10 +2814,10 @@ data-urls@^1.0.0:
whatwg-mimetype "^2.2.0" whatwg-mimetype "^2.2.0"
whatwg-url "^7.0.0" whatwg-url "^7.0.0"
date-fns@2.0.0-beta.3: date-fns@2.0.0-beta.4:
version "2.0.0-beta.3" version "2.0.0-beta.4"
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.0.0-beta.3.tgz#2e28f5af945930f774ddd778e184d68227101d55" resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.0.0-beta.4.tgz#3e1bf33a15da69481f81972c4a50aad762a81f2c"
integrity sha512-z5O262BvHPhwUvA1weXH+AZodygnZUcORERw8hjwBUrRPGrAo2e/rjXfC8Ykf1OGJZGDuLnK/WXbEZBIc0exGQ== integrity sha512-xekjYm7ZDBuzePM/GBodhi3hW3P8dd2RbuIOLBjet2E6EGFR82wHTTXCSGuDEoapqlDvsx88ymRsq85lbM7dDw==
debug@2.6.9, debug@^2.1.2, debug@^2.2.0, debug@^2.3.3, debug@^2.6.8, debug@^2.6.9: debug@2.6.9, debug@^2.1.2, debug@^2.2.0, debug@^2.3.3, debug@^2.6.8, debug@^2.6.9:
version "2.6.9" version "2.6.9"
@ -5828,12 +5828,12 @@ metascraper-lang-detector@^4.8.5:
franc "~4.0.0" franc "~4.0.0"
iso-639-3 "~1.1.0" iso-639-3 "~1.1.0"
metascraper-lang@^5.6.3: metascraper-lang@^5.6.5:
version "5.6.3" version "5.6.5"
resolved "https://registry.yarnpkg.com/metascraper-lang/-/metascraper-lang-5.6.3.tgz#d2d7a20f4145b0785391fffec629e154737fc942" resolved "https://registry.yarnpkg.com/metascraper-lang/-/metascraper-lang-5.6.5.tgz#0363514de4bb580f8e571502e7421b2345cd54bf"
integrity sha512-c13zxmREcB/hDXs7MIxio7RNfVsSzGfixk6FrfQQh3fypmiR84SpeZmQR+G/e2X/BDNwpIydJM62R7BayY709Q== integrity sha512-mp9fVytxX9eLGNI16CYGEhBXhG3a0/RtjVqtcngSyeVZl7SKxMyGLrGqDgCuoU6Til/n11ujEnu3wlQfbGclIg==
dependencies: dependencies:
"@metascraper/helpers" "^5.6.3" "@metascraper/helpers" "^5.6.5"
metascraper-logo@^5.6.5: metascraper-logo@^5.6.5:
version "5.6.5" version "5.6.5"
@ -6143,10 +6143,10 @@ neo4j-graphql-js@^2.6.3:
lodash "^4.17.11" lodash "^4.17.11"
neo4j-driver "^1.7.3" neo4j-driver "^1.7.3"
neode@^0.3.0: neode@^0.3.1:
version "0.3.0" version "0.3.1"
resolved "https://registry.yarnpkg.com/neode/-/neode-0.3.0.tgz#a4a41234fac23236db6b589ec2b505ad6e5fd832" resolved "https://registry.yarnpkg.com/neode/-/neode-0.3.1.tgz#d40147bf20d6951b69c9d392fbdd322aeca07816"
integrity sha512-V6uQhap7FDwbeC+mH6JEI352QSou4Ukj7vs/bGZSrVlMZKVS8vs/mbQYXoFdCkmQJuUtJWqO9wmtWg5GjCaNDQ== integrity sha512-SdaJmdjQ3PWOH6W1H8Xgd2CLyJs+BPPXPt0jOVNs7naeQH8nWPP6ixDqI6NWDCxwecTdNl//fpAicB9I6hCwEw==
dependencies: dependencies:
"@hapi/joi" "^15.1.0" "@hapi/joi" "^15.1.0"
dotenv "^4.0.0" dotenv "^4.0.0"

View File

@ -230,7 +230,7 @@ When("I choose {string} as the title of the post", title => {
When("I type in the following text:", text => { When("I type in the following text:", text => {
lastPost.content = text.replace("\n", " "); 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 => { Then("the post shows up on the landing page at position {int}", index => {

View File

@ -30,7 +30,7 @@
"faker": "Marak/faker.js#master", "faker": "Marak/faker.js#master",
"graphql-request": "^1.8.2", "graphql-request": "^1.8.2",
"neo4j-driver": "^1.7.5", "neo4j-driver": "^1.7.5",
"neode": "^0.3.0", "neode": "^0.3.1",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"slug": "^1.1.0" "slug": "^1.1.0"
} }

View File

@ -45,7 +45,11 @@
{{ $t('comment.show.more') }} {{ $t('comment.show.more') }}
</a> </a>
</div> </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;"> <div style="text-align: right; margin-right: 20px; margin-top: -12px;">
<a v-if="!isCollapsed" @click="isCollapsed = !isCollapsed" style="padding-left: 40px; "> <a v-if="!isCollapsed" @click="isCollapsed = !isCollapsed" style="padding-left: 40px; ">
{{ $t('comment.show.less') }} {{ $t('comment.show.less') }}
@ -62,6 +66,7 @@ import gql from 'graphql-tag'
import { mapGetters, mapMutations } from 'vuex' import { mapGetters, mapMutations } from 'vuex'
import HcUser from '~/components/User' import HcUser from '~/components/User'
import ContentMenu from '~/components/ContentMenu' import ContentMenu from '~/components/ContentMenu'
import ContentViewer from '~/components/Editor/ContentViewer'
import HcEditCommentForm from '~/components/comments/EditCommentForm/EditCommentForm' import HcEditCommentForm from '~/components/comments/EditCommentForm/EditCommentForm'
export default { export default {
@ -74,6 +79,7 @@ export default {
components: { components: {
HcUser, HcUser,
ContentMenu, ContentMenu,
ContentViewer,
HcEditCommentForm, HcEditCommentForm,
}, },
props: { props: {

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

View 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" />`,
}))

View File

@ -183,30 +183,16 @@
</template> </template>
<script> <script>
import defaultExtensions from './defaultExtensions.js'
import linkify from 'linkify-it' import linkify from 'linkify-it'
import stringHash from 'string-hash' import stringHash from 'string-hash'
import Fuse from 'fuse.js' import Fuse from 'fuse.js'
import tippy from 'tippy.js' import tippy from 'tippy.js'
import { Editor, EditorContent, EditorFloatingMenu, EditorMenuBubble } from 'tiptap' import { Editor, EditorContent, EditorFloatingMenu, EditorMenuBubble } from 'tiptap'
import EventHandler from './plugins/eventHandler.js' import EventHandler from './plugins/eventHandler.js'
import { import { History } from 'tiptap-extensions'
Heading,
HardBreak,
Blockquote,
ListItem,
BulletList,
OrderedList,
HorizontalRule,
Placeholder,
Bold,
Italic,
Strike,
Underline,
Link,
History,
} from 'tiptap-extensions'
import Mention from './nodes/Mention.js'
import Hashtag from './nodes/Hashtag.js' import Hashtag from './nodes/Hashtag.js'
import Mention from './nodes/Mention.js'
import { mapGetters } from 'vuex' import { mapGetters } from 'vuex'
let throttleInputEvent let throttleInputEvent
@ -389,24 +375,8 @@ export default {
content: this.value || '', content: this.value || '',
doc: this.doc, doc: this.doc,
extensions: [ extensions: [
...defaultExtensions(this),
new EventHandler(), 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 History(),
...optionalExtensions, ...optionalExtensions,
], ],
@ -505,13 +475,11 @@ export default {
selectItem(item) { selectItem(item) {
const typeAttrs = { const typeAttrs = {
mention: { mention: {
// TODO: use router here id: item.id,
url: `/profile/${item.id}`,
label: item.slug, label: item.slug,
}, },
hashtag: { hashtag: {
// TODO: Fill up with input hashtag in search field id: item.name,
url: `/search/hashtag/${item.name}`,
label: item.name, label: item.name,
}, },
} }

View 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),
},
})
}

View 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
},
}),
]
}

View 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)
})
})
})

View File

@ -0,0 +1,7 @@
import { Bold as TipTapBold } from 'tiptap-extensions'
export default class Bold extends TipTapBold {
pasteRules() {
return []
}
}

View File

@ -0,0 +1,7 @@
import { Italic as TipTapItalic } from 'tiptap-extensions'
export default class Italic extends TipTapItalic {
pasteRules() {
return []
}
}

View File

@ -0,0 +1,7 @@
import { Strike as TipTapStrike } from 'tiptap-extensions'
export default class Strike extends TipTapStrike {
pasteRules() {
return []
}
}

View 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)
},
}
}
}

View 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:
'Shes incapable of controlling her limbs when her kitty is around. The obsession grows every day. Ps. Thats a sleep sack shes 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')
})
})
})

View File

@ -18,27 +18,24 @@ export default class Hashtag extends TipTapMention {
} }
get schema() { get schema() {
const patchedSchema = super.schema return {
...super.schema,
patchedSchema.attrs = { toDOM: node => {
url: {},
label: {},
}
patchedSchema.toDOM = node => {
return [ return [
'a', 'a',
{ {
class: this.options.mentionClass, class: this.options.mentionClass,
href: node.attrs.url, href: `/search/hashtag/${node.attrs.id}`,
'data-hashtag-id': node.attrs.id,
target: '_blank', target: '_blank',
// contenteditable: 'true',
}, },
`${this.options.matcher.char}${node.attrs.label}`, `${this.options.matcher.char}${node.attrs.label}`,
] ]
} },
patchedSchema.parseDOM = [ parseDOM: [
// this is not implemented // simply don't parse mentions from html
] // just treat them as normal links
return patchedSchema ],
}
} }
} }

View 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,
],
}
}
}

View File

@ -6,26 +6,24 @@ export default class Mention extends TipTapMention {
} }
get schema() { get schema() {
const patchedSchema = super.schema return {
...super.schema,
patchedSchema.attrs = { toDOM: node => {
url: {},
label: {},
}
patchedSchema.toDOM = node => {
return [ return [
'a', 'a',
{ {
class: this.options.mentionClass, class: this.options.mentionClass,
href: node.attrs.url, href: `/profile/${node.attrs.id}`,
'data-mention-id': node.attrs.id,
target: '_blank', target: '_blank',
}, },
`${this.options.matcher.char}${node.attrs.label}`, `${this.options.matcher.char}${node.attrs.label}`,
] ]
} },
patchedSchema.parseDOM = [ parseDOM: [
// this is not implemented // simply don't parse mentions from html
] // just treat them as normal links
return patchedSchema ],
}
} }
} }

View File

@ -28,13 +28,8 @@
</template> </template>
<script> <script>
import { SweetalertIcon } from 'vue-sweetalert-icons'
export default { export default {
name: 'ConfirmModal', name: 'ConfirmModal',
components: {
SweetalertIcon,
},
props: { props: {
name: { type: String, default: '' }, name: { type: String, default: '' },
type: { type: String, required: true }, type: { type: String, required: true },

View File

@ -27,13 +27,9 @@
<script> <script>
import gql from 'graphql-tag' import gql from 'graphql-tag'
import { SweetalertIcon } from 'vue-sweetalert-icons'
export default { export default {
name: 'ReportModal', name: 'ReportModal',
components: {
SweetalertIcon,
},
props: { props: {
name: { type: String, default: '' }, name: { type: String, default: '' },
type: { type: String, required: true }, type: { type: String, required: true },

View File

@ -54,12 +54,10 @@
<script> <script>
import PasswordStrength from '../Password/Strength' import PasswordStrength from '../Password/Strength'
import gql from 'graphql-tag' import gql from 'graphql-tag'
import { SweetalertIcon } from 'vue-sweetalert-icons'
import PasswordForm from '~/components/utils/PasswordFormHelper' import PasswordForm from '~/components/utils/PasswordFormHelper'
export default { export default {
components: { components: {
SweetalertIcon,
PasswordStrength, PasswordStrength,
}, },
props: { props: {

View File

@ -48,12 +48,8 @@
<script> <script>
import gql from 'graphql-tag' import gql from 'graphql-tag'
import { SweetalertIcon } from 'vue-sweetalert-icons'
export default { export default {
components: {
SweetalertIcon,
},
data() { data() {
return { return {
formData: { formData: {

View 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%' }"
/>
`,
}))

View File

@ -71,7 +71,6 @@
<script> <script>
import gql from 'graphql-tag' import gql from 'graphql-tag'
import PasswordStrength from '../Password/Strength' import PasswordStrength from '../Password/Strength'
import { SweetalertIcon } from 'vue-sweetalert-icons'
import PasswordForm from '~/components/utils/PasswordFormHelper' import PasswordForm from '~/components/utils/PasswordFormHelper'
export const SignupVerificationMutation = gql` export const SignupVerificationMutation = gql`
@ -86,7 +85,6 @@ export const SignupVerificationMutation = gql`
export default { export default {
components: { components: {
PasswordStrength, PasswordStrength,
SweetalertIcon,
}, },
data() { data() {
const passwordForm = PasswordForm({ translate: this.$t }) const passwordForm = PasswordForm({ translate: this.$t })

View File

@ -56,7 +56,6 @@
<script> <script>
import gql from 'graphql-tag' import gql from 'graphql-tag'
import { SweetalertIcon } from 'vue-sweetalert-icons'
export const SignupMutation = gql` export const SignupMutation = gql`
mutation($email: String!) { mutation($email: String!) {
@ -73,9 +72,6 @@ export const SignupByInvitationMutation = gql`
} }
` `
export default { export default {
components: {
SweetalertIcon,
},
props: { props: {
token: { type: String, default: null }, token: { type: String, default: null },
}, },

View 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
}
}
`
}

View File

@ -20,7 +20,7 @@
:width="{ base: '85%', sm: '85%', md: '50%', lg: '50%' }" :width="{ base: '85%', sm: '85%', md: '50%', lg: '50%' }"
:class="{ 'hide-mobile-menu': !toggleMobileMenu }" :class="{ 'hide-mobile-menu': !toggleMobileMenu }"
> >
<div id="nav-search-box"> <div id="nav-search-box" v-if="isLoggedIn">
<search-input <search-input
id="nav-search" id="nav-search"
:delay="300" :delay="300"
@ -33,6 +33,7 @@
</div> </div>
</ds-flex-item> </ds-flex-item>
<ds-flex-item <ds-flex-item
v-if="isLoggedIn"
:width="{ base: '15%', sm: '15%', md: '10%', lg: '10%' }" :width="{ base: '15%', sm: '15%', md: '10%', lg: '10%' }"
:class="{ 'hide-mobile-menu': !toggleMobileMenu }" :class="{ 'hide-mobile-menu': !toggleMobileMenu }"
> >
@ -116,7 +117,7 @@
</ds-container> </ds-container>
</div> </div>
<ds-container style="word-break: break-all"> <ds-container style="word-break: break-all">
<div class="main-container" :width="{ base: '100%', md: '96%' }"> <div class="main-container">
<nuxt /> <nuxt />
</div> </div>
</ds-container> </ds-container>
@ -125,9 +126,11 @@
&nbsp;-&nbsp; &nbsp;-&nbsp;
<nuxt-link to="/imprint">{{ $t('site.imprint') }}</nuxt-link> <nuxt-link to="/imprint">{{ $t('site.imprint') }}</nuxt-link>
&nbsp;&nbsp; &nbsp;&nbsp;
<nuxt-link to="/terms-and-conditions">{{ $t('site.termsAc') }}</nuxt-link> <nuxt-link to="/terms-and-conditions">{{ $t('site.termsAndConditions') }}</nuxt-link>
&nbsp;&nbsp; &nbsp;&nbsp;
<nuxt-link to="/privacy">{{ $t('site.privacy') }}</nuxt-link> <nuxt-link to="/code-of-conduct">{{ $t('site.code-of-conduct') }}</nuxt-link>
&nbsp;&nbsp;
<nuxt-link to="/data-privacy">{{ $t('site.data-privacy') }}</nuxt-link>
&nbsp;&nbsp; &nbsp;&nbsp;
<nuxt-link to="/changelog">{{ $t('site.changelog') }}</nuxt-link> <nuxt-link to="/changelog">{{ $t('site.changelog') }}</nuxt-link>
</div> </div>
@ -149,7 +152,6 @@ import HcAvatar from '~/components/Avatar/Avatar.vue'
import seo from '~/mixins/seo' import seo from '~/mixins/seo'
import FilterPosts from '~/components/FilterPosts/FilterPosts.vue' import FilterPosts from '~/components/FilterPosts/FilterPosts.vue'
import CategoryQuery from '~/graphql/CategoryQuery.js' import CategoryQuery from '~/graphql/CategoryQuery.js'
export default { export default {
components: { components: {
Dropdown, Dropdown,
@ -266,47 +268,38 @@ export default {
align-self: center; align-self: center;
display: inline-flex; display: inline-flex;
} }
.main-container { .main-container {
padding-top: 6rem; padding-top: 6rem;
padding-bottom: 5rem; padding-bottom: 5rem;
} }
.main-navigation { .main-navigation {
a { a {
color: $text-color-soft; color: $text-color-soft;
} }
} }
.main-navigation-right { .main-navigation-right {
display: flex; display: flex;
flex: 1; flex: 1;
} }
.main-navigation-right .desktop-view { .main-navigation-right .desktop-view {
float: right; float: right;
} }
.avatar-menu { .avatar-menu {
margin: 2px 0px 0px 5px; margin: 2px 0px 0px 5px;
} }
.avatar-menu-trigger { .avatar-menu-trigger {
user-select: none; user-select: none;
display: flex; display: flex;
align-items: center; align-items: center;
padding-left: $space-xx-small; padding-left: $space-xx-small;
} }
.avatar-menu-popover { .avatar-menu-popover {
padding-top: 0.5rem; padding-top: 0.5rem;
padding-bottom: 0.5rem; padding-bottom: 0.5rem;
hr { hr {
color: $color-neutral-90; color: $color-neutral-90;
background-color: $color-neutral-90; background-color: $color-neutral-90;
} }
.logout-link { .logout-link {
margin-left: -$space-small; margin-left: -$space-small;
margin-right: -$space-small; margin-right: -$space-small;
@ -315,43 +308,35 @@ export default {
padding: $space-x-small $space-small; padding: $space-x-small $space-small;
// subtract menu border with from padding // subtract menu border with from padding
padding-left: $space-small - 2; padding-left: $space-small - 2;
color: $text-color-base; color: $text-color-base;
&:hover { &:hover {
color: $text-color-link-active; color: $text-color-link-active;
} }
} }
nav { nav {
margin-left: -$space-small; margin-left: -$space-small;
margin-right: -$space-small; margin-right: -$space-small;
margin-top: -$space-xx-small; margin-top: -$space-xx-small;
margin-bottom: -$space-xx-small; margin-bottom: -$space-xx-small;
a { a {
padding-left: 12px; padding-left: 12px;
} }
} }
} }
@media only screen and (min-width: 960px) { @media only screen and (min-width: 960px) {
.mobile-hamburger-menu { .mobile-hamburger-menu {
display: none; display: none;
} }
} }
@media only screen and (max-width: 960px) { @media only screen and (max-width: 960px) {
#nav-search-box, #nav-search-box,
.main-navigation-right { .main-navigation-right {
margin: 10px 0px; margin: 10px 0px;
} }
.hide-mobile-menu { .hide-mobile-menu {
display: none; display: none;
} }
} }
.ds-footer { .ds-footer {
text-align: center; text-align: center;
position: fixed; position: fixed;

View File

@ -11,8 +11,8 @@
"site": { "site": {
"made": "Mit &#10084; gemacht", "made": "Mit &#10084; gemacht",
"imprint": "Impressum", "imprint": "Impressum",
"termsAc": "Nutzungsbedingungen", "data-privacy": "Datenschutz",
"privacy": "Datenschutz", "termsAndConditions": "Nutzungsbedingungen",
"changelog": "Änderungen & Verlauf", "changelog": "Änderungen & Verlauf",
"contact": "Kontakt", "contact": "Kontakt",
"tribunal": "Registergericht", "tribunal": "Registergericht",
@ -21,7 +21,8 @@
"taxident": "Umsatzsteuer-Identifikationsnummer gemäß § 27 a Umsatzsteuergesetz (Deutschland)", "taxident": "Umsatzsteuer-Identifikationsnummer gemäß § 27 a Umsatzsteuergesetz (Deutschland)",
"responsible": "Verantwortlicher gemäß § 55 Abs. 2 RStV ", "responsible": "Verantwortlicher gemäß § 55 Abs. 2 RStV ",
"bank": "Bankverbindung", "bank": "Bankverbindung",
"germany": "Deutschland" "germany": "Deutschland",
"code-of-conduct": "Verhaltenscodex"
}, },
"sorting": { "sorting": {
"newest": "Neuste", "newest": "Neuste",
@ -55,6 +56,7 @@
"title": "Mach mit bei Human Connection!", "title": "Mach mit bei Human Connection!",
"form": { "form": {
"description": "Um loszulegen, gib deine E-Mail Adresse ein:", "description": "Um loszulegen, gib deine E-Mail Adresse ein:",
"invitation-code": "Dein Einladungscode lautet: <b>{code}</b>",
"errors": { "errors": {
"email-exists": "Es gibt schon ein Benutzerkonto mit dieser E-Mail Adresse!", "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." "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": { "comment": {
"submit": "Kommentiere", "submit": "Kommentiere",
"submitted": "Kommentar gesendet" "submitted": "Kommentar Gesendet",
"updated": "Änderungen gespeichert"
} }
}, },
"comment": { "comment": {
@ -358,17 +361,20 @@
"user": { "user": {
"title": "Nutzer melden", "title": "Nutzer melden",
"type": "Nutzer", "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": { "contribution": {
"title": "Beitrag melden", "title": "Beitrag melden",
"type": "Beitrag", "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": { "comment": {
"title": "Kommentar melden", "title": "Kommentar melden",
"type": "Kommentar", "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": { "followButton": {
@ -378,7 +384,6 @@
"shoutButton": { "shoutButton": {
"shouted": "empfohlen" "shouted": "empfohlen"
}, },
"release": { "release": {
"submit": "freigeben", "submit": "freigeben",
"cancel": "Abbrechen", "cancel": "Abbrechen",
@ -417,7 +422,119 @@
"infoSelectedNoOfMaxCategories": "{chosen} von {max} Kategorien ausgewählt" "infoSelectedNoOfMaxCategories": "{chosen} von {max} Kategorien ausgewählt"
} }
}, },
"terms": { "changelog": {
"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>" "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"
} }
} }

View File

@ -11,8 +11,8 @@
"site": { "site": {
"made": "Made with &#10084;", "made": "Made with &#10084;",
"imprint": "Imprint", "imprint": "Imprint",
"termsAc": "Terms and conditions", "termsAndConditions": "Terms and conditions",
"privacy": "Data privacy", "data-privacy": "Data privacy",
"changelog": "Changes & History", "changelog": "Changes & History",
"contact": "Contact", "contact": "Contact",
"tribunal": "Registry court", "tribunal": "Registry court",
@ -21,7 +21,8 @@
"taxident": "USt-ID. according to §27a of the German Sales Tax Law:", "taxident": "USt-ID. according to §27a of the German Sales Tax Law:",
"responsible": "responsible for contents of this page (§ 55 Abs. 2 RStV)", "responsible": "responsible for contents of this page (§ 55 Abs. 2 RStV)",
"bank": "bank account", "bank": "bank account",
"germany": "Germany" "germany": "Germany",
"code-of-conduct": "Code of Conduct"
}, },
"sorting": { "sorting": {
"newest": "Newest", "newest": "Newest",
@ -390,17 +391,20 @@
"user": { "user": {
"title": "Release User", "title": "Release User",
"type": "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": { "contribution": {
"title": "Release Contribution", "title": "Release Contribution",
"type": "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": { "comment": {
"title": "Release Comment", "title": "Release Comment",
"type": "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": { "user": {
@ -418,7 +422,118 @@
"infoSelectedNoOfMaxCategories": "{chosen} of {max} categories selected" "infoSelectedNoOfMaxCategories": "{chosen} of {max} categories selected"
} }
}, },
"terms": { "changelog": {
"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>" "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"
} }
} }

View File

@ -6,7 +6,7 @@
"made": "Con &#10084; realizado", "made": "Con &#10084; realizado",
"imprint": "Pie de imprenta", "imprint": "Pie de imprenta",
"termsAc": "términos y condiciones", "termsAc": "términos y condiciones",
"privacy": "protección de datos", "data-privacy": "protección de datos",
"changelog": "Cambios e historia", "changelog": "Cambios e historia",
"contact": "Contacto", "contact": "Contacto",
"tribunal": "tribunal de registro", "tribunal": "tribunal de registro",

View File

@ -6,7 +6,7 @@
"made": "Avec &#10084; fait", "made": "Avec &#10084; fait",
"imprint": "Mentions légales", "imprint": "Mentions légales",
"termsAc": "modalités et conditions", "termsAc": "modalités et conditions",
"privacy": "protection des données", "data-privacy": "protection des données",
"changelog": "Changements et historique", "changelog": "Changements et historique",
"contact": "Contacter", "contact": "Contacter",
"tribunal": "tribunal de registre", "tribunal": "tribunal de registre",

View File

@ -3,7 +3,7 @@
"made": "Con &#10084; fatto", "made": "Con &#10084; fatto",
"imprint": "Impressum", "imprint": "Impressum",
"termsAc": "Condizioni d'uso", "termsAc": "Condizioni d'uso",
"privacy": "protezione dei dati", "data-privacy": "protezione dei dati",
"changelog": "Cambiamenti e storia", "changelog": "Cambiamenti e storia",
"contact": "Contatto", "contact": "Contatto",
"tribunal": "registro tribunale", "tribunal": "registro tribunale",

View File

@ -3,7 +3,7 @@
"made": "Met &#10084; gemaakt", "made": "Met &#10084; gemaakt",
"imprint": "Afdruk", "imprint": "Afdruk",
"termsAc": "Gebruiksvoorwaarden", "termsAc": "Gebruiksvoorwaarden",
"privacy": "gegevensbescherming", "data-privacy": "gegevensbescherming",
"changelog": "Veranderingen & Geschiedenis", "changelog": "Veranderingen & Geschiedenis",
"contact": "contact", "contact": "contact",
"tribunal": "registerrechtbank", "tribunal": "registerrechtbank",

View File

@ -6,7 +6,7 @@
"made": "Z &#10084; zrobiony", "made": "Z &#10084; zrobiony",
"imprint": "Nadruk", "imprint": "Nadruk",
"termsAc": "Warunki użytkowania", "termsAc": "Warunki użytkowania",
"privacy": "ochrona danych", "data-privacy": "ochrona danych",
"changelog": "Zmiany i historia", "changelog": "Zmiany i historia",
"contact": "Kontakt", "contact": "Kontakt",
"tribunal": "sąd rejestrowy", "tribunal": "sąd rejestrowy",
@ -16,18 +16,17 @@
"responsible": "Odpowiedzialny zgodnie z § 55 Abs. 2 RStV (Niemcy)", "responsible": "Odpowiedzialny zgodnie z § 55 Abs. 2 RStV (Niemcy)",
"bank": "rachunek bankowy", "bank": "rachunek bankowy",
"germany": "Niemcy" "germany": "Niemcy"
}, },
"login": { "login": {
"copy": "Jeśli masz już konto Human Connection, zaloguj się tutaj.", "copy": "Jeśli masz już konto Human Connection, zaloguj się tutaj.",
"login": "logowanie", "login": "Logowanie",
"logout": "Wyloguj się", "logout": "Wyloguj się",
"email": "Twój adres e-mail", "email": "Twój adres e-mail",
"password": "Twoje hasło", "password": "Twoje hasło",
"forgotPassword": "Zapomniałeś hasła?", "forgotPassword": "Zapomniałeś hasła?",
"moreInfo": "Co to jest Human Connection?", "moreInfo": "Co to jest Human Connection?",
"moreInfoURL": "https://human-connection.org/pl/", "moreInfoURL": "https://human-connection.org/en/",
"moreInfoHint": "na stronę prezentacji", "moreInfoHint": "idź po więcej szczegółów",
"hello": "Cześć" "hello": "Cześć"
}, },
"password-reset": { "password-reset": {
@ -51,7 +50,7 @@
} }
}, },
"editor": { "editor": {
"placeholder": "Zostaw swoje inspirujące myśli...." "placeholder": "Napisz coś inspirującego..."
}, },
"profile": { "profile": {
"name": "Mój profil", "name": "Mój profil",
@ -61,27 +60,27 @@
"following": "Obserwowani", "following": "Obserwowani",
"shouted": "Krzyknij", "shouted": "Krzyknij",
"commented": "Skomentuj", "commented": "Skomentuj",
"userAnonym": "Anonimowy", "userAnonym": "Anonymous",
"socialMedia": "Gdzie indziej mogę znaleźć", "socialMedia": "Gdzie jeszcze mogę znaleźć",
"network": { "network": {
"title": "Sieć", "title": "Sieć",
"following": "jest następująca:", "following": "obserwuje:",
"followingNobody": "nie podąża za nikim.", "followingNobody": "nikogo nie obserwuje.",
"followedBy": "po którym następuje:", "followedBy": "jest obserwowany:",
"followedByNobody": "nie jest śledzona przez nikogo.", "followedByNobody": "nikt go nie obserwuje.",
"and": "i", "and": "i",
"more": "więcej" "more": "więcej"
} }
}, },
"notifications": { "notifications": {
"menu": { "menu": {
"mentioned": "wspomniała o tobie na posterunku." "mentioned": "wspomiał o Tobie we wpisie"
} }
}, },
"search": { "search": {
"placeholder": "Wyszukiwanie", "placeholder": "Szukaj",
"hint": "Czego szukasz?", "hint": "Czego szukasz?",
"failed": "Nic nie znaleziono" "failed": "Niczego nie znaleziono"
}, },
"settings": { "settings": {
"name": "Ustawienia", "name": "Ustawienia",
@ -97,20 +96,20 @@
"name": "Bezpieczeństwo", "name": "Bezpieczeństwo",
"change-password": { "change-password": {
"button": "Zmień hasło", "button": "Zmień hasło",
"success": "Hasło zostało pomyślnie zmienione!", "success": "Hasło zostało zmienione!",
"label-old-password": "Twoje stare hasło", "label-old-password": "Stare hasło",
"label-new-password": "Twoje nowe hasło", "label-new-password": "Nowe hasło",
"label-new-password-confirm": "Potwierdź nowe hasło", "label-new-password-confirm": "Potwierdź nowe hasło",
"message-old-password-required": "Wprowadź swoje stare hasło", "message-old-password-required": "Podaj stare hasło",
"message-new-password-required": "Wprowadź nowe hasło", "message-new-password-required": "Wprowadź nowe hasło",
"message-new-password-confirm-required": "Potwierdź nowe hasło.", "message-new-password-confirm-required": "Potwierdź nowe hasło",
"message-new-password-missmatch": "Wpisz ponownie to samo hasło.", "message-new-password-missmatch": "Wpisz to samo hasło ponownie",
"passwordSecurity": "Zabezpieczenie hasłem", "passwordSecurity": "Siła hasła",
"passwordStrength0": "Bardzo niepewne hasło", "passwordStrength0": "Hasło bardzo słabe",
"passwordStrength1": "Niepewne hasło", "passwordStrength1": "Hasło słabe",
"passwordStrength2": "Hasło pośredniczące", "passwordStrength2": "Hasło średnie",
"passwordStrength3": "Silne hasło", "passwordStrength3": "Hasło silne",
"passwordStrength4": "Bardzo mocne hasło" "passwordStrength4": "Hasło bardzo silne"
} }
}, },
"invites": { "invites": {
@ -120,26 +119,26 @@
"name": "Pobierz dane" "name": "Pobierz dane"
}, },
"deleteUserAccount": { "deleteUserAccount": {
"name": "Usuwanie danych", "name": "Usuń dane",
"contributionsCount": "Usuń moje stanowiska.", "contributionsCount": "Usuń {count} moich postów",
"commentsCount": "Usuń moje komentarze {liczba}.", "commentsCount": "Usuń {count} moich komentarzy",
"accountDescription": "Bądź świadomy, że Twój post i komentarze są ważne dla naszej społeczności. Jeśli nadal chcesz je usunąć, musisz zaznaczyć je poniżej.", "accountDescription": "Be aware that your Post and Comments are important to our community. If you still choose to delete them, you have to mark them below.",
"accountWarning": "<b>Nie możesz zarządzać</b> i <b>Nie możesz REKOVER</b> swoje konto, posty lub komentarze po usunięciu konta!", "accountWarning": "Po usunięcie Twojego konta, nie możesz <b>ZARZĄDZAĆ</b> ani <b>ODZYSKAĆ</b> danych, wpisów oraz komentarzy!",
"success": "Konto zostało pomyślnie usunięte", "success": "Konto zostało usunięte",
"pleaseConfirm": "<b class='is-danger'>Niszczycielskie działanie!</b> Typ <b>{potwierdź}</b> aby potwierdzić" "pleaseConfirm": "<b class='is-danger'>Uwaga, niebezpieczeństwo!</b> Wpisz <b>{confirm}</b>, aby potwierdzić"
}, },
"organizations": { "organizations": {
"name": "My Organizations" "name": "My Organizations"
}, },
"languages": { "languages": {
"name": "Languages" "name": "Języki"
}, },
"social-media": { "social-media": {
"name": "Social media", "name": "Media społecznościowe",
"placeholder": "Add social media url", "placeholder": "Dodaj link do mediów społecznościowych",
"submit": "Add link", "submit": "Dodaj link",
"successAdd": "Added social media. Updated user profile!", "successAdd": "Dodano społeczność. Profil zaktualizowany!",
"successDelete": "Deleted social media. Updated user profile!" "successDelete": "Usunięto społeczność. Profil zaktualizowany!"
} }
}, },
"admin": { "admin": {
@ -188,15 +187,15 @@
"name": "Więcej informacji" "name": "Więcej informacji"
}, },
"takeAction": { "takeAction": {
"name": "Podejmij działania" "name": "Podejmij działanie"
}, },
"menu": { "menu": {
"edit": "Edytuj Post", "edit": "Edytuj wpis",
"delete": "Usuń wpis" "delete": "Usuń wpis"
}, },
"comment": { "comment": {
"submit": "Komentarz", "submit": "Komentarz",
"submitted": "Przedłożony komentarz" "submitted": "Komentarz dodany"
} }
}, },
"comment": { "comment": {
@ -253,16 +252,16 @@
"moderation": { "moderation": {
"name": "Umiarkowanie", "name": "Umiarkowanie",
"reports": { "reports": {
"empty": "Gratulacje, nic do umiarkowanego.", "empty": "Gratulacje, moderacja nie jest potrzebna",
"name": "Sprawozdania", "name": "Raporty",
"submitter": "zgłaszane przez", "submitter": "zgłoszone przez",
"disabledBy": "niepełnosprawni przez" "disabledBy": "deaktywowane przez"
} }
}, },
"disable": { "disable": {
"submit": "Niepełnosprawność", "submit": "Deaktywuj",
"cancel": "Odwołaj", "cancel": "Anuluj",
"success": "Niepełnosprawni skutecznie", "success": "Zdeaktywowano",
"user": { "user": {
"title": "Wyłączenie użytkownika", "title": "Wyłączenie użytkownika",
"type": "Użytkownik", "type": "Użytkownik",
@ -283,38 +282,37 @@
"submit": "Usuń", "submit": "Usuń",
"cancel": "Odwołaj", "cancel": "Odwołaj",
"contribution": { "contribution": {
"title": "Usuń Post", "title": "Ukryj wpis",
"type": "Wkład", "type": "Wpis / Post",
"message": "Naprawdę chcesz usunąć post \"<b>{name}</b>\"?", "message": "Czy na pewno chcesz ukryć wpis \" <b> {name} </b> \"?",
"success": "Wyślij pomyślnie usunięty!" "success": "Wyślij pomyślnie usunięty!"
}, },
"comment": { "comment": {
"title": "Usuń komentarz", "title": "Usuń komentarz",
"type": "Komentarz", "type": "Komentarz",
"message": "Czy naprawdę chcesz usunąć komentarz \"<b>{name}</b>\"?", "message": "Czy na pewno chcesz ukryć komentarz użytkownika \" <b> {name} </b> \"?"
"success": "Komentarz został pomyślnie usunięty!"
} }
}, },
"report": { "report": {
"submit": "Sprawozdanie", "submit": "Wyślij raport",
"cancel": "Odwołaj", "cancel": "Anuluj",
"success": "Dzięki za zgłoszenie!", "success": "Dziękujemy za Twoje zgłoszenie!",
"user": { "user": {
"title": "Raport Użytkownik", "title": "Raport Użytkownik",
"type": "Użytkownik", "type": "Użytkownik",
"message": "Naprawdę chcesz zgłosić użytkownika \"<b>{name}</b>\"?", "message": "Czy na pewno chcesz zgłosić użytkownika \" <b> {name} </b> \"?",
"error": "Zgłosiłeś już użytkownika!" "error": "Zgłosiłeś już użytkownika!"
}, },
"contribution": { "contribution": {
"title": "Wkład w raport", "title": "Zgłoś wpis",
"type": "Wkład", "type": "Wpis / Post",
"message": "Naprawdę chcesz zgłosić wkład, jaki wniosłaś do programu \"<b>{name}</b>\"?", "message": "Czy na pewno chcesz zgłosić ten wpis użytkownika \" <b> {name} </b> \"?",
"error": "Zgłosiłeś już ten wkład!" "error": "Zgłosiłeś już ten wkład!"
}, },
"comment": { "comment": {
"title": "Sprawozdanie Komentarz", "title": "Sprawozdanie Komentarz",
"type": "Komentarz", "type": "Komentarz",
"message": "Naprawdę chcesz zgłosić komentarz od \"<b>{name}</b>\"?", "message": "Czy na pewno chcesz zgłosić komentarz użytkownika\" <b> {name} </b> \"?",
"error": "Zgłosiłeś już komentarz!" "error": "Zgłosiłeś już komentarz!"
} }
}, },
@ -323,38 +321,38 @@
"following": "w skutek" "following": "w skutek"
}, },
"shoutButton": { "shoutButton": {
"shouted": "wykrzyczany" "shouted": "krzyczeć"
}, },
"release": { "release": {
"submit": "Zwolnienie", "submit": "Zgłoś",
"cancel": "Odwołaj", "cancel": "Anuluj",
"success": "Wydany z powodzeniem!", "success": "Zgłoszono pomyślnie!",
"user": { "user": {
"title": "Zwolnienie użytkownika", "title": "Zgłoś użytkownika",
"type": "Użytkownik", "type": "Użytkownik",
"message": "Naprawdę chcesz uwolnić użytkownika \"<b>{name}</b>\"?" "message": "Czy na pewno chcesz zgłosić użytkownika \"<b> {name} </b>\"?"
}, },
"contribution": { "contribution": {
"title": "Zwolnienie Wkład", "title": "Zgłoś wpis",
"type": "Wkład", "type": "Wpis",
"message": "Naprawdę chcesz uwolnić swój wkład \"<b>{name}</b>\"?" "message": "Czy na pewno chcesz zgłosić wpis użytkownika \"<b> {name} </b>\"?"
}, },
"comment": { "comment": {
"title": "Zwolnienie komentarz", "title": "Zgłoś komentarz",
"type": "komentarz", "type": "Komentarz",
"message": "Czy naprawdę chcesz opublikować komentarz od \"<b>{name}</b>\"?" "message": "Czy na pewno chcesz zgłosić komentarz użytkownika \"<b> {name} </b>\"?"
} }
}, },
"user": { "user": {
"avatar": { "avatar": {
"submitted": "Przesłanie udane" "submitted": "Przesłano pomyślnie"
} }
}, },
"contribution": { "contribution": {
"newPost": "Utwórz nowy post", "newPost": "Utwórz nowy wpis",
"filterFollow": "Filtrowanie wkładu użytkowników, za którymi podążam", "filterFollow": "Pokaż wpisy użytkowników, których śledzę",
"filterALL": "Wyświetl wszystkie wkłady", "filterALL": "Pokaż wszystkie wpisy",
"success": "Zachowany!", "success": "Zapisano!",
"languageSelectLabel": "Język", "languageSelectLabel": "Język",
"categories": { "categories": {
"infoSelectedNoOfMaxCategories": "{chosen} z {max} wybrane kategorie" "infoSelectedNoOfMaxCategories": "{chosen} z {max} wybrane kategorie"

View File

@ -3,7 +3,7 @@
"made": "Com &#10084; feito", "made": "Com &#10084; feito",
"imprint": "Impressão", "imprint": "Impressão",
"termsAc": "termos e condições", "termsAc": "termos e condições",
"privacy": "protecção de dados", "data-privacy": "protecção de dados",
"changelog": "Mudanças e Histórico", "changelog": "Mudanças e Histórico",
"contact": "Contato", "contact": "Contato",
"tribunal": "tribunal", "tribunal": "tribunal",

View File

@ -3,12 +3,12 @@ const envWhitelist = ['NODE_ENV', 'MAINTENANCE', 'MAPBOX_TOKEN']
const dev = process.env.NODE_ENV !== 'production' const dev = process.env.NODE_ENV !== 'production'
const styleguidePath = '../Nitro-Styleguide' const styleguidePath = '../Nitro-Styleguide'
const styleguideStyles = process.env.STYLEGUIDE_DEV ? const styleguideStyles = process.env.STYLEGUIDE_DEV
[ ? [
`${styleguidePath}/src/system/styles/main.scss`, `${styleguidePath}/src/system/styles/main.scss`,
`${styleguidePath}/src/system/styles/shared.scss`, `${styleguidePath}/src/system/styles/shared.scss`,
] : ]
'@human-connection/styleguide/dist/shared.scss' : '@human-connection/styleguide/dist/shared.scss'
const buildDir = process.env.NUXT_BUILD || '.nuxt' const buildDir = process.env.NUXT_BUILD || '.nuxt'
@ -21,7 +21,7 @@ module.exports = {
modern: !dev ? 'server' : false, modern: !dev ? 'server' : false,
transition: { pageTransition: {
name: 'slide-up', name: 'slide-up',
mode: 'out-in', mode: 'out-in',
}, },
@ -39,6 +39,11 @@ module.exports = {
'registration-verify-code', 'registration-verify-code',
'registration-create-user-account', 'registration-create-user-account',
'pages-slug', 'pages-slug',
'imprint',
'terms-and-conditions',
'code-of-conduct',
'data-privacy',
'changelog',
], ],
// pages to keep alive // pages to keep alive
keepAlivePages: ['index'], keepAlivePages: ['index'],
@ -51,7 +56,8 @@ module.exports = {
head: { head: {
title: 'Human Connection', title: 'Human Connection',
titleTemplate: '%s - Human Connection', titleTemplate: '%s - Human Connection',
meta: [{ meta: [
{
charset: 'utf-8', charset: 'utf-8',
}, },
{ {
@ -64,11 +70,13 @@ module.exports = {
content: pkg.description, content: pkg.description,
}, },
], ],
link: [{ link: [
{
rel: 'icon', rel: 'icon',
type: 'image/x-icon', type: 'image/x-icon',
href: '/favicon.ico', href: '/favicon.ico',
}, ], },
],
}, },
/* /*
@ -95,37 +103,19 @@ module.exports = {
/* /*
** Plugins to load before mounting the App ** Plugins to load before mounting the App
*/ */
plugins: [{ plugins: [
{
src: `~/plugins/styleguide${process.env.STYLEGUIDE_DEV ? '-dev' : ''}.js`, src: `~/plugins/styleguide${process.env.STYLEGUIDE_DEV ? '-dev' : ''}.js`,
ssr: true, ssr: true,
}, },
{ { src: '~/plugins/i18n.js', ssr: true },
src: '~/plugins/i18n.js', { src: '~/plugins/axios.js', ssr: false },
ssr: true, { src: '~/plugins/keep-alive.js', ssr: false },
}, { src: '~/plugins/vue-directives.js', ssr: false },
{ { src: '~/plugins/v-tooltip.js', ssr: false },
src: '~/plugins/axios.js', { src: '~/plugins/izi-toast.js', ssr: false },
ssr: false, { src: '~/plugins/vue-filters.js' },
}, { src: '~/plugins/vue-sweetalert-icons.js' },
{
src: '~/plugins/keep-alive.js',
ssr: false,
},
{
src: '~/plugins/vue-directives.js',
ssr: false,
},
{
src: '~/plugins/v-tooltip.js',
ssr: false,
},
{
src: '~/plugins/izi-toast.js',
ssr: false,
},
{
src: '~/plugins/vue-filters.js',
},
], ],
router: { router: {
@ -303,7 +293,9 @@ module.exports = {
// Give apollo module options // Give apollo module options
apollo: { apollo: {
tokenName: 'human-connection-token', // optional, default: apollo-token tokenName: 'human-connection-token', // optional, default: apollo-token
tokenExpires: 3, // optional, default: 7 (days) cookieAttributes: {
expires: 3, // optional, default: 7 (days)
},
// includeNodeModules: true, // optional, default: false (this includes graphql-tag for node_modules folder) // includeNodeModules: true, // optional, default: false (this includes graphql-tag for node_modules folder)
// Watch loading state for all queries // Watch loading state for all queries
@ -350,7 +342,8 @@ module.exports = {
loader: 'vue-svg-loader', loader: 'vue-svg-loader',
options: { options: {
svgo: { svgo: {
plugins: [{ plugins: [
{
removeViewBox: false, removeViewBox: false,
}, },
{ {

View File

@ -12,6 +12,7 @@
"scripts": { "scripts": {
"dev": "cross-env NODE_ENV=development nodemon server/index.js --watch server", "dev": "cross-env NODE_ENV=development nodemon server/index.js --watch server",
"dev:styleguide": "cross-env STYLEGUIDE_DEV=true yarn dev", "dev:styleguide": "cross-env STYLEGUIDE_DEV=true yarn dev",
"storybook": "start-storybook -p 3002 -c storybook/",
"build": "nuxt build", "build": "nuxt build",
"start": "cross-env node server/index.js", "start": "cross-env node server/index.js",
"generate": "nuxt generate", "generate": "nuxt generate",
@ -52,19 +53,20 @@
"@human-connection/styleguide": "0.5.17", "@human-connection/styleguide": "0.5.17",
"@nuxtjs/apollo": "^4.0.0-rc10", "@nuxtjs/apollo": "^4.0.0-rc10",
"@nuxtjs/axios": "~5.5.4", "@nuxtjs/axios": "~5.5.4",
"@nuxtjs/dotenv": "~1.3.0", "@nuxtjs/dotenv": "~1.4.0",
"@nuxtjs/style-resources": "~0.1.2", "@nuxtjs/style-resources": "~0.1.2",
"accounting": "~0.4.1", "accounting": "~0.4.1",
"apollo-cache-inmemory": "~1.6.2", "apollo-cache-inmemory": "~1.6.2",
"apollo-client": "~2.6.3", "apollo-client": "~2.6.3",
"cookie-universal-nuxt": "~2.0.17", "cookie-universal-nuxt": "~2.0.17",
"cross-env": "~5.2.0", "cross-env": "~5.2.0",
"date-fns": "2.0.0-beta.3", "date-fns": "2.0.0-beta.4",
"express": "~4.17.1", "express": "~4.17.1",
"graphql": "~14.4.2", "graphql": "~14.4.2",
"isemail": "^3.2.0", "isemail": "^3.2.0",
"jsonwebtoken": "~8.5.1", "jsonwebtoken": "~8.5.1",
"linkify-it": "~2.2.0", "linkify-it": "~2.2.0",
"node-fetch": "^2.6.0",
"nuxt": "~2.8.1", "nuxt": "~2.8.1",
"nuxt-dropzone": "^1.0.2", "nuxt-dropzone": "^1.0.2",
"nuxt-env": "~0.1.0", "nuxt-env": "~0.1.0",
@ -75,14 +77,17 @@
"v-tooltip": "~2.0.2", "v-tooltip": "~2.0.2",
"vue-count-to": "~1.0.13", "vue-count-to": "~1.0.13",
"vue-izitoast": "1.1.2", "vue-izitoast": "1.1.2",
"vue-sweetalert-icons": "~3.2.0", "vuex-i18n": "~1.13.1",
"vuex-i18n": "~1.13.0", "vue-sweetalert-icons": "~4.0.0",
"zxcvbn": "^4.4.2" "zxcvbn": "^4.4.2"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "~7.5.5", "@babel/core": "~7.5.5",
"@babel/plugin-syntax-dynamic-import": "^7.2.0", "@babel/plugin-syntax-dynamic-import": "^7.2.0",
"@babel/preset-env": "~7.5.5", "@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/cli-shared-utils": "~3.10.0",
"@vue/eslint-config-prettier": "~5.0.0", "@vue/eslint-config-prettier": "~5.0.0",
"@vue/server-test-utils": "~1.0.0-beta.29", "@vue/server-test-utils": "~1.0.0-beta.29",
@ -90,6 +95,10 @@
"babel-core": "~7.0.0-bridge.0", "babel-core": "~7.0.0-bridge.0",
"babel-eslint": "~10.0.2", "babel-eslint": "~10.0.2",
"babel-jest": "~24.8.0", "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": "~5.16.0",
"eslint-config-prettier": "~6.0.0", "eslint-config-prettier": "~6.0.0",
"eslint-config-standard": "~12.0.0", "eslint-config-standard": "~12.0.0",
@ -109,8 +118,12 @@
"nodemon": "~1.19.1", "nodemon": "~1.19.1",
"prettier": "~1.18.2", "prettier": "~1.18.2",
"sass-loader": "~7.1.0", "sass-loader": "~7.1.0",
"style-loader": "~0.23.1",
"style-resources-loader": "~1.2.1",
"tippy.js": "^4.3.5", "tippy.js": "^4.3.5",
"vue-jest": "~3.0.4", "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"
} }
} }

View File

@ -5,16 +5,78 @@
</ds-space> </ds-space>
<ds-container> <ds-container>
<ds-space margin-top="large"> <strong>
<ds-text>{{ $t('site.changelog') }}</ds-text> {{ Date.parse('2019-03') | date('MMMM yyyy') }}
<ds-text>...</ds-text> </strong>
</ds-space> <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> </ds-container>
</div> </div>
</template> </template>
<script> <script>
export default { export default {
layout: 'default',
head() { head() {
return { return {
title: this.$t('site.changelog'), title: this.$t('site.changelog'),

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

View File

@ -0,0 +1,395 @@
<template>
<div>
<ds-space>
<ds-heading tag="h2">{{ $t('site.data-privacy') }}</ds-heading>
</ds-space>
<ds-container>
<ds-space margin-top="large">
<p>
für
<a href="https://alpha.human-connection.org" target="_blank">
https://alpha.human-connection.org
</a>
</p>
<p>
Stand vom
<strong>17. Juni 2019</strong>
</p>
<br />
<p>
Human Connection gGmbH bietet über die Internetseite
<a href="https://alpha.human-connection.org" target="_blank">
https://alpha.human-connection.org
</a>
den Nutzern und Interessierten umfangreiche Informationen. Dabei liegt uns der
vertrauensvolle und sichere Umgang mit Deinen personenbezogenen Daten sehr am Herzen.
</p>
<br />
<p>
Auf Grund von rechtlichen und technischen Veränderungen, passen wir die
Datenschutzerklärung bei Bedarf an. Es ist jeweils die aktuellste Fassung unserer hier
veröffentlichten Datenschutzerklärung gültig.
</p>
<h1>Verantwortlicher</h1>
<p>
Verantwortlicher im Sinne der Datenschutz-Grundverordnung, sonstiger in den
Mitgliedstaaten der Europäischen Union geltenden Datenschutzgesetze und anderer
Bestimmungen mit datenschutzrechtlichem Charakter ist die:
</p>
<br />
<p><strong>Human Connection gGmbH</strong></p>
<p>Bahnhofstr. 11</p>
<p>73235 Weilheim / Teck</p>
<p>
<strong>Geschäftsführer:</strong>
Dennis Hack
</p>
<p>
<strong>Telefon:</strong>
+49 151 43 80 42 22
</p>
<p>
<strong>E-Mail:</strong>
<a href="mailto:info@human-connection.org">info@human-connection.org</a>
</p>
<p>
<strong>Web:</strong>
<a href="https://human-connection.org" target="_blank">https://human-connection.org</a>
</p>
<br />
<h1>Datenschutzbeauftragter</h1>
<p><strong>Unser Datenschutzbeauftragte ist so erreichbar</strong></p>
<br />
<p>
<strong>E-Mail:</strong>
<a href="mailto:datenschutz@human-connection.org">datenschutz@human-connection.org</a>
</p>
<br />
<h1>Zwecke der Datenverarbeitung</h1>
<p>
Die bei der Registrierung oder später von Dir angegebenen personenbezogenen Daten
verarbeiten wir nur zu den weiter unten genannten Zwecken. Wir übermitteln sie nicht an
Dritte zu weiteren Zwecken, außer:
</p>
<ul>
<li>wir haben Deine ausdrückliche Einwilligung dazu,</li>
<li>die Verarbeitung ist zur Abwicklung eines Vertrages mit Dir erforderlich,</li>
<li>
die Verarbeitung ist zur Erfüllung einer rechtlichen Verpflichtung erforderlich oder
</li>
<li>
die Verarbeitung zur Wahrung berechtigter Interessen erforderlich ist und kein Grund zur
Annahme besteht, dass Du ein überwiegendes schutzwürdiges Interesse an der
Nichtweitergabe dieser Daten hast.
</li>
</ul>
<h1>Deine Registrierung</h1>
<p>
Im Sinne der Datenminimierung registrierst Du Dich bei unserer Webanwendung einzig mit
Deiner E-Mail-Adresse. Weitere personenbezogene Daten sind für die Registrierung nicht
nötig. Die Registrierung ist notwendig, um unser Netzwerk nutzen zu können.
</p>
<br />
<p>
Über diese E-Mail-Adresse bist Du uns bekannt und ebenso dient diese als Deine Identität.
D.h., alle Rechte, die Du ausübst, hängen mit dieser Identität zusammen und über sie
erkennen wir, dass Du wirklich Du bist und nicht ein anderer.
</p>
<br />
<p>
In der Kommunikation mit uns spielt die von Dir verwendete E-Mail-Adresse also eine
zentrale Rolle. Daher werden wir alle von Dir ausgeübten Rechte und ggf. Wünsche, die Du
an uns richtest, immer über Deine E-Mail-Adresse verifizieren. Niemals werden wir auf
Basis eines Anrufes oder einer sonstigen Information an Deinem Account etwas ändern, ihn
z.B. Löschen oder Stillegen, ohne diese Verifizierung außer, wir sind durch ein Gesetz
dazu gezwungen.
</p>
<br />
<p>
Gesichert ist Deine Identität in unserem Netzwerk über ein Passwort, was von Dir selbst
vergeben werden muss und jederzeit geändert werden kann. Weitere Daten, wie Dein
Pseudonym, ein Avatar-Bildchen oder weitere Angaben, die ggf. auch personenbezogene Daten
sein können, vergibst Du selbst. Sie sind für die Registrierung selbst nicht nötig.
</p>
<h1>Allgemeine Nutzungsinformationen</h1>
<p>
Beim Abruf unserer Website werden von Deinem Browser Daten übertragen, die für den Abruf
und die Übermittlung technisch notwendig sind, zum Beispiel Deine IP-Adresse. Die
IP-Adresse kann zum Beispiel dazu dienen, Dich zu identifizieren und zählt daher zu den
personenbezogenen Daten.
</p>
<br />
<p>
Zusätzlich übermittelt Dein Browser mit jeder Anfrage an unseren Webserver weitere
Informationen ungefragt, zum Beispiel Informationen über Dein Betriebssystem oder Deinen
Webbrowser, die für eine Abfrage unserer Webseiten überflüssig sind und auch nicht
ausgewertet werden. Dies sind keine personenbezogenen Daten und lassen keinen Rückschluss
auf Deine Person zu.
</p>
<br />
<p>
Letztere Daten werden wir u.U. statistisch auswerten, um für die Nutzer unserer Plattform
Statistiken bereit zu stellen oder um unsere Plattform weiter zu optimieren,
beispielsweise um sie für besonders oft genutzte Browser oder Betriebssysteme optimal
anzupassen.
</p>
<h1>Löschung bzw. Sperrung</h1>
<p>
Wir folgen dem Grundsatz der Datenvermeidung und Datensparsamkeit. Daher speichern wir
Deine personenbezogenen Daten nur so lange, wie dies zur Erreichung der hier genannten
Zwecke erforderlich ist oder wie es die ggf. gesetzlich vorgesehenen Speicherfristen
vorsehen. Ist die Frist abgelaufen oder der Zweck nicht mehr gegeben, sperren oder löschen
wir diese Daten entsprechend den gesetzlichen Vorschriften.
</p>
<h1>Log-Files</h1>
<p>
Unsere Webserver schreiben Protokolle, um ggf. vorkommende Anwendungsfehler erkennen zu
können. Diese Protokolle enthalten keine IP-Adressen und werden von uns spätestens nach 7
Tagen gelöscht.
</p>
<h1>Cookies</h1>
<p>
Wie die meisten anderen Webseiten verwenden auch wir so genannte Cookies. Cookies sind
kleine Textdateien, die von einem Webserver in Deinem Browser gespeichert werden, um dort
bestimmte Informationen zu hinterlegen. Zum Beispiel, dass Du angemeldet bist, dass Du die
Website in einer bestimmten Sprache sehen möchtest oder um auf der Website zu navigieren.
Diese Daten sind nur in Deinem Browser gespeichert und werden von uns nicht weiter
gegeben. Für die Funktionalität unserer Website sind diese Cookies erforderlich. Sie
können aber trotzdem jederzeit von Dir gelöscht werden.
</p>
<br />
<p>
Desweiteren gibt es sogenannte Drittanbieter-Cookies. Cookies von Drittanbietern sind
Cookies, die durch eingebundene Funktionalitäten anderer Websites angelegt werden, zum
Beispiel bei Like-Buttons, Landkarten oder Videos-Plugins, obwohl Du nur unsere Website
besuchst. Diese Drittanbieter können dann Cookies lesen und speichern, genauso, als wenn
Du deren Seite besuchen würdest.
</p>
<br />
<p>
Da wir als Social-Network bestimmte Funktionalitäten, wie zum Beispiel eingebettete
Videos, für die Nutzer zur Verfügung stellen möchten, kommen wir ohne diese Cookies leider
nicht aus. Cookies von Drittanbietern ermöglichen das sogenannte Tracking. D.h., dass der
Drittanbieter über Deinen Aufruf unserer Website informiert wird und diese Daten auswerten
kann.
</p>
<br />
<p>
Auch diese Cookies können jederzeit gelöscht werden. Auch kannst Du im Browser das Setzen
von Drittanbietercookies verbieten. Dadurch wird unsere Website aber unter Umständen in
Ihrer Funktion eingeschränkt sein. Dein Browser kann auch so eingestellt werden, dass Du
über Cookies informiert wirst und das Speichern einzeln bestätigen oder ablehnen kannst.
Informationen zu den oft genutzten Browsern findest Du hier:
</p>
<br />
<ul>
<li>
Firefox:
<a
href="https://support.mozilla.org/de/kb/cookies-erlauben-und-ablehnen"
target="_blank"
>
https://support.mozilla.org/de/kb/cookies-erlauben-und-ablehnen
</a>
</li>
<li>
<strong>Chrome:</strong>
<a
href="http://support.google.com/chrome/bin/answer.py?hl=de&amp;hlrm=en&amp;answer=95647"
target="_blank"
>
http://support.google.com/chrome/bin/answer.py?hl=de&amp;hlrm=en&amp;answer=95647
</a>
</li>
<li>
<strong>Safari:</strong>
<a href="https://support.apple.com/kb/ph21411?locale=de_DE" target="_blank">
https://support.apple.com/kb/ph21411?locale=de_DE
</a>
</li>
<li>
<strong>Opera:</strong>
<a href="http://help.opera.com/Windows/10.20/de/cookies.html" target="_blank">
http://help.opera.com/Windows/10.20/de/cookies.html
</a>
</li>
<li>
<strong>Edge:</strong>
<a
href="https://privacy.microsoft.com/en-us/windows-10-microsoft-edge-and-privacy"
target="_blank"
>
https://privacy.microsoft.com/en-us/windows-10-microsoft-edge-and-privacy
</a>
</li>
</ul>
<h1>Die Öffentlichkeit</h1>
<p>
Unser Netzwerk und alle darin von Dir oder anderen veröffentlichten Daten stehen der
Öffentlichkeit zur Verfügung, solange Inhalte bzw. Funktionen nicht explizit anders
gekennzeichnet sind. Überdenke daher, was Du schreibst und wie Du Dich äußerst, denn was
einmal in der Öffentlichkeit ist, lässt sich nur schwer wieder zurücknehmen, selbst, wenn
Du oder wir es löschen.
</p>
<h1>Du hast Rechte</h1>
<p>
Damit Du unsere Webanwendung nutzen kannst, verarbeiten wir u.a. personenbezogene Daten
von Dir. Du bist also eine von dieser Verarbeitung betroffene Person. Gemäß der
europäischen Datenschutzgrundverordnung kannst Du daher uns gegenüber folgende Rechte
ausüben:
</p>
<ul>
<li>Auskunft über Deine ggf. bei uns verarbeiteten personenbezogenen Daten,</li>
<li>Berichtigung unrichtiger personenbezogener Daten,</li>
<li>Löschung Deiner bei uns gespeicherten personenbezogenen Daten,</li>
<li>
Einschränkung der Datenverarbeitung, sofern wir Deine Daten aufgrund gesetzlicher
Pflichten noch nicht löschen dürfen,
</li>
<li>Widerspruch gegen die Verarbeitung Deiner Daten bei uns und</li>
<li>Datenübertragbarkeit</li>
</ul>
<br />
<p>
<strong>
Sofern Du uns eine Einwilligung zur Verarbeitung Deiner personenbezogenen Daten erteilt
hast (bei der Registrierung), kannst Du diese jederzeit mit Wirkung für die Zukunft
widerrufen.
</strong>
</p>
<br />
<p>
<strong>
Du kannst Dich auch jederzeit bei einer Aufsichtsbehörde über uns beschweren, solltest
Du der Ansicht sein, dass die Verarbeitung Deiner personenbezogenen Daten gegen die
europäische Datenschutz-Grundverodnung oder das Bundesdatenschutzgesetz verstößt. Eine
Möglichkeit ist hier:
</strong>
</p>
<br />
<p>
<strong>
Landesbeauftragter für Datenschutz und Informationsfreiheit Baden-Württemberg
</strong>
</p>
<p>
<a href="https://www.baden-wuerttemberg.datenschutz.de/kontakt/" target="_blank">
https://www.baden-wuerttemberg.datenschutz.de/kontakt/
</a>
</p>
<h1>Verwendung von Youtube-Videos</h1>
<p>
Wir nutzen die Youtube-Einbettungsfunktion zur Anzeige und Wiedergabe von Videos des
Anbieters Youtube, der zu der Google LLC., 1600 Amphitheatre Parkway, Mountain View, CA
94043, USA (Google) gehört.
</p>
<br />
<p>
Dabei wird der erweiterte Datenschutzmodus verwendet, der nach Angaben von Google eine
Speicherung von Nutzerinformationen erst bei Wiedergabe des/der Videos in Gang setzt.
</p>
<br />
<p>
Wenn Du die Wiedergabe eingebetteter Youtube-Videos startest, setzt der Google Youtube
Cookies ein, um Informationen über Dein Nutzungsverhalten zu sammeln. Youtube nach
dienen diese unter anderem dazu, Videostatistiken zu erfassen, die Nutzerfreundlichkeit zu
verbessern und Missbrauch zu unterbinden. Bist Du gleichzeitig bei YouTube eingeloggt,
werden diese Informationen Deinem Mitgliedskonto bei YouTube zugeordnet. Wenn Du die
Zuordnung mit Deinem Profil bei YouTube nicht wünschst, musst Du Dich vor Aktivierung des
Buttons ausloggen. Google speichert Deine Daten (selbst für nicht eingeloggte Nutzer) als
Nutzungsprofile und wertet diese aus. Eine solche Auswertung erfolgt insbesondere gemäß
Art. 6 Abs. 1 lit.f DSGVO auf Basis der berechtigten Interessen von Google an der
Einblendung personalisierter Werbung, Marktforschung und/oder bedarfsgerechten Gestaltung
seiner Website. Dir steht ein Widerspruchsrecht zu gegen die Bildung dieser Nutzerprofile,
wobei Du Dich zur Ausübung dessen an YouTube richten musst.
</p>
<br />
<p>
Unabhängig von einer Wiedergabe der eingebetteten Videos wird bei jedem Aufruf dieser
Website eine Verbindung zum Google-Netzwerk DoubleClick aufgenommen, was ohne unseren
Einfluss weitere Datenverarbeitungsvorgänge auslösen kann.
</p>
<br />
<p>
Google LLC mit Sitz in den USA ist für das us-europäische Datenschutzübereinkommen
Privacy Shield zertifiziert, welches die Einhaltung des in der EU geltenden
Datenschutzniveaus gewährleistet.
</p>
<br />
<p>
Weitere Informationen zum Datenschutz bei YouTube findest Du in der Datenschutzerklärung
des Anbieters unter:
<a href="https://www.google.de/intl/de/policies/privacy" target="_blank">
https://www.google.de/intl/de/policies/privacy
</a>
</p>
<h1>Verwendung von Vimeo-Videos</h1>
<p>
Wir nutzen auch Plugins von Vimeo der Vimeo, LLC, 555 West 18th Street, New York, New York
10011, USA.
</p>
<p>
Rufst du eine Seite auf, die ein solches Plugin eingebunden hat, stellt Dein Browser eine
direkte Verbindung zu den Servern von Vimeo her. Durch diese Einbindung erhält Vimeo die
Information, dass Du unsere Seite aufgerufen hast, auch wenn Du keinen Vimeo-Account
besitzt oder gerade nicht bei Vimeo eingeloggt bist. Deine IP-Adresse und noch einige
Daten, die Dein Browser liefert, werden von Deinem Browser direkt an einen Server von
Vimeo in die USA übermittelt und dort gespeichert.
</p>
<p>
Wenn Du gerade bei Vimeo eingeloggt bist, kann Vimeo die Nutzung unserer webanwendung
Deinem Vimeo-Account zuordnen. Wenn Du das Plugin benutzt, wie z.B. beim Start eines
Videos, werden diese Informationen ebenso an einen Server von Vimeo gesendet und dort
verarbeitet.
</p>
<br />
<p>
Diese Verarbeitungsvorgänge erfolgen gem. Art. 6 Abs. 1 lit. f DSGVO auf Grundlage des
berechtigten Interesses von Vimeo an Marktforschung und der bedarfsgerechten Gestaltung
des Dienstes.
</p>
<br />
<p>Wenn Du das nicht möchtest, musst Du Dich vorher bei Vimeo ausloggen.</p>
<br />
<p>
Vimeo informiert über den Zweck, Umfang und die weitere Verarbeitung und Nutzung der
Daten, sowie Ihre diesbezüglichen Rechte und Einstellungsmöglichkeiten zum Schutz Deiner
Privatsphäre in den Datenschutzhinweisen:
<a href="https://vimeo.com/privacy" target="_blank">https://vimeo.com/privacy</a>
</p>
<br />
<p>
Bei Vimeo-Videos, die bei uns in Beiträgen eingebunden sind, ist das Trackingtool Google
Analytics integriert. Auf dieses Tracking seitens Vimeo haben wir leider keinen Einfluss.
Google Analytics verwendet für das Tracking Drittanbieter-Cookies, wie schon oben
beschrieben. Die erhobenen Daten über Deine Benutzung unserer Website werden an einen
Server von Google übertragen und dort gespeichert, in der Regel in die USA.
</p>
<br />
<p>
Dies erfolgt gem. Art. 6 Abs. 1 lit. f DSGVO auf Grundlage des berechtigten Interesses von
Vimeo an der statistischen Analyse des Nutzerverhaltens zu Optimierungs- und
Marketingzwecken.
</p>
<h1>Sicherheit des Web-Zugriffs</h1>
<p>
Um die Datenübertragung von unserem Webserver zu Dir und umgekehrt zu schützen, verwenden
wir eine TLS 1.3 verschlüsselte Verbindung. Dass erkennst Du am grünen Schloss in Deinem
Browser bzw. daran, dass Deine URL mit https beginnt, statt mit http. So kann niemand
mitlesen, was Du siehst oder was Du eingibst.
</p>
</ds-space>
</ds-container>
</div>
</template>
<script>
export default {
layout: 'default',
head() {
return {
title: this.$t('site.data-privacy'),
}
},
}
</script>

View File

@ -23,9 +23,7 @@
</ds-flex-item> </ds-flex-item>
<ds-flex-item :width="{ base: '100%', sm: '50%' }" centered> <ds-flex-item :width="{ base: '100%', sm: '50%' }" centered>
<ds-space margin="small"> <ds-space margin="small">
<ds-text size="small"> <ds-text size="small">{{ $t('login.copy') }}</ds-text>
{{ $t('login.copy') }}
</ds-text>
</ds-space> </ds-space>
<form :disabled="pending" @submit.prevent="onSubmit"> <form :disabled="pending" @submit.prevent="onSubmit">
<ds-input <ds-input
@ -46,9 +44,7 @@
type="password" type="password"
/> />
<ds-space class="password-reset-link" margin-bottom="large"> <ds-space class="password-reset-link" margin-bottom="large">
<nuxt-link to="/password-reset/request"> <nuxt-link to="/password-reset/request">{{ $t('login.forgotPassword') }}</nuxt-link>
{{ $t('login.forgotPassword') }}
</nuxt-link>
</ds-space> </ds-space>
<ds-button <ds-button
:loading="pending" :loading="pending"
@ -84,7 +80,7 @@ export default {
components: { components: {
LocaleSwitch, LocaleSwitch,
}, },
layout: 'blank', layout: 'default',
data() { data() {
return { return {
ready: false, ready: false,

View File

@ -6,9 +6,7 @@
<img style="width: 200px;" src="/img/sign-up/onourjourney.png" alt="Human Connection" /> <img style="width: 200px;" src="/img/sign-up/onourjourney.png" alt="Human Connection" />
</ds-space> </ds-space>
<ds-space style="text-align: center;" margin-top="small" margin-bottom="xxx-small" centered> <ds-space style="text-align: center;" margin-top="small" margin-bottom="xxx-small" centered>
<ds-heading tag="h3" soft> <ds-heading tag="h3" soft>Logging out...</ds-heading>
Logging out...
</ds-heading>
</ds-space> </ds-space>
</ds-flex-item> </ds-flex-item>
</ds-flex> </ds-flex>
@ -17,7 +15,7 @@
<script> <script>
export default { export default {
layout: 'blank', layout: 'default',
async beforeCreate() { async beforeCreate() {
await this.$store.dispatch('auth/logout') await this.$store.dispatch('auth/logout')
this.$router.replace('/') this.$router.replace('/')

View File

@ -20,10 +20,7 @@
<ds-space margin-bottom="small" /> <ds-space margin-bottom="small" />
<ds-heading tag="h3" no-margin>{{ post.title }}</ds-heading> <ds-heading tag="h3" no-margin>{{ post.title }}</ds-heading>
<ds-space margin-bottom="small" /> <ds-space margin-bottom="small" />
<!-- Content --> <content-viewer class="content" :content="post.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" />
<!-- eslint-enable vue/no-v-html --> <!-- eslint-enable vue/no-v-html -->
<ds-space margin="xx-large" /> <ds-space margin="xx-large" />
<!-- Categories --> <!-- Categories -->
@ -62,6 +59,7 @@
</template> </template>
<script> <script>
import ContentViewer from '~/components/Editor/ContentViewer'
import HcCategory from '~/components/Category' import HcCategory from '~/components/Category'
import HcTag from '~/components/Tag' import HcTag from '~/components/Tag'
import ContentMenu from '~/components/ContentMenu' import ContentMenu from '~/components/ContentMenu'
@ -86,6 +84,7 @@ export default {
ContentMenu, ContentMenu,
HcCommentForm, HcCommentForm,
HcCommentList, HcCommentList,
ContentViewer,
}, },
head() { head() {
return { return {

View File

@ -1,23 +0,0 @@
<template>
<div>
<ds-space>
<ds-heading tag="h2">{{ $t('site.privacy') }}</ds-heading>
</ds-space>
<ds-container>
<ds-space margin-top="large">
<ds-text>{{ $t('site.privacy') }}</ds-text>
<ds-text>...</ds-text>
</ds-space>
</ds-container>
</div>
</template>
<script>
export default {
head() {
return {
title: this.$t('site.privacy'),
}
},
}
</script>

View File

@ -1,22 +1,46 @@
<template> <template>
<div> <div>
<ds-space> <ds-space>
<ds-heading tag="h2">{{ $t('site.termsAc') }}</ds-heading> <ds-heading tag="h2">{{ $t('site.termsAndConditions') }}</ds-heading>
</ds-space> </ds-space>
<ds-container> <ds-container>
<ds-space margin-top="large"> <div>
<ds-text>{{ $t('site.termsAc') }}</ds-text> <ol>
<ds-text v-html="$t('terms.text')"></ds-text> <li v-for="section in sections" :key="section">
</ds-space> <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> </ds-container>
</div> </div>
</template> </template>
<script> <script>
export default { export default {
layout: 'default',
head() { head() {
return { 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',
],
} }
}, },
} }

View File

@ -0,0 +1,4 @@
import Vue from 'vue'
import VueSweetalertIcons from 'vue-sweetalert-icons'
Vue.use(VueSweetalertIcons)

View File

@ -0,0 +1,3 @@
import '@storybook/addon-actions/register'
import '@storybook/addon-a11y/register'
// import '@storybook/addon-links/register'

View 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)

View 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

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

View 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
}

File diff suppressed because it is too large Load Diff

View File

@ -3579,10 +3579,10 @@ neo4j-driver@^1.6.3, neo4j-driver@^1.7.5:
text-encoding-utf-8 "^1.0.2" text-encoding-utf-8 "^1.0.2"
uri-js "^4.2.2" uri-js "^4.2.2"
neode@^0.3.0: neode@^0.3.1:
version "0.3.0" version "0.3.1"
resolved "https://registry.yarnpkg.com/neode/-/neode-0.3.0.tgz#a4a41234fac23236db6b589ec2b505ad6e5fd832" resolved "https://registry.yarnpkg.com/neode/-/neode-0.3.1.tgz#d40147bf20d6951b69c9d392fbdd322aeca07816"
integrity sha512-V6uQhap7FDwbeC+mH6JEI352QSou4Ukj7vs/bGZSrVlMZKVS8vs/mbQYXoFdCkmQJuUtJWqO9wmtWg5GjCaNDQ== integrity sha512-SdaJmdjQ3PWOH6W1H8Xgd2CLyJs+BPPXPt0jOVNs7naeQH8nWPP6ixDqI6NWDCxwecTdNl//fpAicB9I6hCwEw==
dependencies: dependencies:
"@hapi/joi" "^15.1.0" "@hapi/joi" "^15.1.0"
dotenv "^4.0.0" dotenv "^4.0.0"